ship: project dashboard pages + sidebar/chat overhaul + log tooling

Ships accumulated WIP that was sitting uncommitted:
- New (home) dashboard route pages: overview, code, data/tables, hosting,
  infrastructure, services, domains, integrations, agents, analytics, api,
  automations, billing, logs, market, marketing(+seo/social), product, security,
  storage, users, settings(app/auth).
- dashboard-sidebar, project-icon-rail, chat-panel updates; mcp + anatomy route
  changes; package.json/lock dependency bumps.
- Coolify log tooling (scripts/fetch-app-logs.mjs + fetch-app-logs-ssh.mjs) and
  ai-new-thread.md "Fetching Production Logs" section.

Excludes throwaway debug scripts and telemetry audit dumps (the latter contain
live credentials and must not be committed).
This commit is contained in:
2026-06-12 18:09:09 -07:00
parent 0f212c750b
commit eb198e2d4d
37 changed files with 8982 additions and 533 deletions

View File

@@ -1,80 +1,341 @@
"use client";
import { useState } from "react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import {
Code2,
Search,
LayoutGrid,
ClipboardList,
Globe,
Database,
BarChart2,
Globe,
Plug,
ShieldCheck,
Code2,
Terminal,
Settings,
CreditCard,
PlaneTakeoff,
ChevronDown,
ChevronRight,
Users,
HardDrive,
Blocks,
} from "lucide-react";
export function DashboardSidebar({ workspace, projectId, children }: { workspace: string, projectId: string, children: React.ReactNode }) {
import { useAnatomy } from "@/components/project/use-anatomy";
export function DashboardSidebar({
workspace,
projectId,
children,
}: {
workspace: string;
projectId: string;
children: React.ReactNode;
}) {
const pathname = usePathname() ?? "";
const projectBase = `/${workspace}/project/${projectId}`;
const isPreview = pathname === `${projectBase}/preview` || pathname.startsWith(`${projectBase}/preview/`);
const isPreview =
pathname === `${projectBase}/preview` ||
pathname.startsWith(`${projectBase}/preview/`);
const [expandedSections, setExpandedSections] = useState<
Record<string, boolean>
>({
settings: true,
data: true,
});
const [searchQuery, setSearchQuery] = useState("");
const { anatomy } = useAnatomy(projectId);
const databases = anatomy?.infrastructure?.databases ?? [];
if (isPreview) {
return <>{children}</>;
}
const items = [
{ segment: "plan", label: "Plan", Icon: ClipboardList },
{ segment: "market", label: "Market", Icon: PlaneTakeoff },
{ segment: "product", label: "Code", Icon: Code2, aliases: ["code"] },
{ segment: "hosting", label: "Hosting", Icon: Globe },
{ segment: "infrastructure", label: "Infra", Icon: Database },
{ segment: "billing", label: "Billing", Icon: CreditCard },
{ segment: "settings", label: "Settings", Icon: Settings },
const handleSectionClick = (segment: string) => {
if (!expandedSections[segment]) {
setExpandedSections((prev) => ({ ...prev, [segment]: true }));
}
};
const menuItems = [
{ segment: "overview", label: "Overview", Icon: LayoutGrid },
{ segment: "plan", label: "Plan & Specs", Icon: ClipboardList },
{ segment: "code", label: "Code", Icon: Code2 },
{
segment: "data",
label: "Data",
Icon: Database,
hasChildren: true,
children: databases.map((db) => ({
segment: `data/tables?db=${db.uuid}`,
label: db.name,
})),
},
{ segment: "storage", label: "Storage", Icon: HardDrive },
{ segment: "services", label: "Services", Icon: Blocks },
{ segment: "users", label: "Auth / Users", Icon: Users },
{ segment: "integrations", label: "Integrations", Icon: Plug },
{ segment: "security", label: "Security", Icon: ShieldCheck },
{ segment: "logs", label: "Logs", Icon: Terminal },
{ segment: "domains", label: "Domains", Icon: Globe },
{
segment: "analytics",
label: "Analytics",
Icon: BarChart2,
badge: "Soon",
},
{
segment: "marketing",
label: "Marketing",
Icon: BarChart2,
badge: "New",
hasChildren: true,
children: [
{ segment: "marketing/seo", label: "SEO & GEO" },
{ segment: "marketing/social", label: "Social content" },
],
},
{
segment: "settings",
label: "Settings",
Icon: Settings,
hasChildren: true,
children: [
{ segment: "settings/app", label: "App Settings" },
{ segment: "settings/auth", label: "Authentication" },
],
},
];
const filteredItems = menuItems.filter(
(item) =>
item.label.toLowerCase().includes(searchQuery.toLowerCase()) ||
(item.children &&
item.children.some((child) =>
child.label.toLowerCase().includes(searchQuery.toLowerCase()),
)),
);
return (
<div style={{ display: "flex", flex: 1, minHeight: 0, minWidth: 0 }}>
<div style={{
width: 240,
borderRight: "1px solid #e4e4e7",
background: "#fafafa",
display: "flex",
flexDirection: "column",
padding: "16px 12px",
gap: 4,
overflowY: "auto"
}}>
<div style={{ fontSize: "0.75rem", fontWeight: 600, color: "#a1a1aa", padding: "0 8px 8px", textTransform: "uppercase", letterSpacing: "0.05em" }}>
<div
style={{
width: 250,
borderRight: "1px solid #e4e4e7",
background: "#ffffff",
display: "flex",
flexDirection: "column",
padding: "16px 12px",
gap: 4,
overflowY: "auto",
}}
>
<div
style={{
fontSize: "0.85rem",
fontWeight: 600,
color: "#18181b",
padding: "0 8px 12px",
}}
>
Dashboard
</div>
{items.map(item => {
const active = pathname === `${projectBase}/${item.segment}` || (item.aliases && item.aliases.some(a => pathname === `${projectBase}/${a}`));
return (
<Link
key={item.segment}
href={`${projectBase}/${item.segment}`}
style={{
display: "flex",
alignItems: "center",
gap: 10,
padding: "8px 12px",
borderRadius: 8,
fontSize: "0.85rem",
fontWeight: 500,
color: active ? "#18181b" : "#52525b",
background: active ? "#fff" : "transparent",
boxShadow: active ? "0 1px 2px rgba(0,0,0,0.05)" : "none",
border: active ? "1px solid #e4e4e7" : "1px solid transparent",
textDecoration: "none",
transition: "all 0.15s ease"
}}
>
<item.Icon size={16} color={active ? "#18181b" : "#a1a1aa"} />
{item.label}
</Link>
);
})}
{/* Search Bar */}
<div
style={{
display: "flex",
alignItems: "center",
gap: 8,
background: "#f4f4f5",
borderRadius: 8,
padding: "6px 10px",
marginBottom: 12,
margin: "0 4px 12px 4px",
}}
>
<Search size={14} color="#a1a1aa" />
<input
type="text"
placeholder="Search..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
style={{
border: "none",
background: "transparent",
outline: "none",
width: "100%",
fontSize: "0.8rem",
color: "#18181b",
}}
/>
</div>
<div style={{ display: "flex", flexDirection: "column", gap: 2 }}>
{filteredItems.map((item) => {
const isMainActive =
pathname === `${projectBase}/${item.segment}` ||
pathname.startsWith(`${projectBase}/${item.segment}/`);
const isExpanded = expandedSections[item.segment];
return (
<div key={item.segment}>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
padding: "8px 10px",
borderRadius: 8,
cursor: "pointer",
background:
isMainActive && !item.hasChildren
? "#eff6ff"
: "transparent",
color:
isMainActive && !item.hasChildren ? "#1d4ed8" : "#52525b",
transition: "all 0.1s ease",
}}
onClick={() => {
if (item.hasChildren) {
setExpandedSections((prev) => ({
...prev,
[item.segment]: !prev[item.segment],
}));
}
}}
>
{item.hasChildren ? (
<div
style={{
display: "flex",
alignItems: "center",
gap: 10,
flex: 1,
}}
>
<item.Icon size={16} />
<span style={{ fontSize: "0.85rem", fontWeight: 500 }}>
{item.label}
</span>
</div>
) : (
<Link
href={`${projectBase}/${item.segment}`}
style={{
display: "flex",
alignItems: "center",
gap: 10,
flex: 1,
textDecoration: "none",
color: "inherit",
}}
>
<item.Icon size={16} />
<span style={{ fontSize: "0.85rem", fontWeight: 500 }}>
{item.label}
</span>
</Link>
)}
<div
style={{ display: "flex", alignItems: "center", gap: 6 }}
>
{item.badge && (
<span
style={{
background: "#eef2ff",
color: "#4f46e5",
fontSize: "0.65rem",
fontWeight: 600,
padding: "2px 6px",
borderRadius: 999,
}}
>
{item.badge}
</span>
)}
{item.hasChildren &&
(isExpanded ? (
<ChevronDown size={14} color="#a1a1aa" />
) : (
<ChevronRight size={14} color="#a1a1aa" />
))}
</div>
</div>
{/* Render Children if expanded */}
{item.hasChildren && isExpanded && item.children && (
<div
style={{
display: "flex",
flexDirection: "column",
marginTop: 2,
}}
>
{item.children.map((child) => {
const href = child.segment.includes("?")
? `${projectBase}/${child.segment.split("?")[0]}?${child.segment.split("?")[1]}`
: `${projectBase}/${child.segment}`;
let isChildActive = false;
if (child.segment.includes("?")) {
const [basePath, searchStr] = child.segment.split("?");
isChildActive =
pathname === `${projectBase}/${basePath}` &&
(typeof window !== "undefined"
? window.location.search.includes(searchStr)
: false);
} else {
isChildActive =
pathname === `${projectBase}/${child.segment}`;
}
return (
<Link
key={child.segment}
href={href}
style={{
display: "flex",
alignItems: "center",
padding: "6px 10px 6px 14px",
marginLeft: "18px",
borderRadius: "0 8px 8px 0",
fontSize: "0.8rem",
fontWeight: 500,
textDecoration: "none",
color: isChildActive ? "#18181b" : "#52525b",
background: "transparent",
transition: "all 0.1s ease",
borderLeft: isChildActive
? "2px solid #18181b"
: "2px solid transparent",
}}
>
{child.label}
</Link>
);
})}
</div>
)}
</div>
);
})}
</div>
</div>
<div style={{ flex: 1, minWidth: 0, overflow: "auto", background: "#fff", display: "flex", flexDirection: "column" }}>
<div
style={{
flex: 1,
minWidth: 0,
overflow: "auto",
background: "#fff",
display: "flex",
flexDirection: "column",
}}
>
{children}
</div>
</div>

View File

@@ -37,6 +37,9 @@ export function ProjectIconRail({ workspace, projectId, actions }: Props) {
fontSize: "0.75rem",
fontWeight: 500,
borderRadius: 6,
display: "flex",
alignItems: "center",
height: 24, // Explicitly match device toggles height
textDecoration: "none",
background: isPreviewActive ? "#ffffff" : "transparent",
color: isPreviewActive ? "#18181b" : "#71717a",
@@ -53,6 +56,9 @@ export function ProjectIconRail({ workspace, projectId, actions }: Props) {
fontSize: "0.75rem",
fontWeight: 500,
borderRadius: 6,
display: "flex",
alignItems: "center",
height: 24, // Explicitly match device toggles height
textDecoration: "none",
background: !isPreviewActive ? "#ffffff" : "transparent",
color: !isPreviewActive ? "#18181b" : "#71717a",
@@ -237,6 +243,16 @@ function PreviewDeviceToggles() {
}}
/>
<span
style={{
opacity: 0.5,
flexShrink: 0,
paddingLeft: 4,
fontFamily: "var(--font-mono), monospace",
}}
>
/
</span>
<div
style={{
flex: 1,
@@ -246,9 +262,14 @@ function PreviewDeviceToggles() {
alignItems: "center",
}}
>
<select
value={currentPath}
onChange={(e) => setCurrentPath(e.target.value)}
<input
type="text"
list="common-routes"
value={currentPath === "/" ? "" : currentPath.replace(/^\//, "")}
onChange={(e) =>
setCurrentPath("/" + e.target.value.replace(/^\//, ""))
}
placeholder="path (e.g. dashboard)"
style={{
background: "transparent",
border: "none",
@@ -259,19 +280,22 @@ function PreviewDeviceToggles() {
textOverflow: "ellipsis",
fontFamily: "var(--font-mono), monospace",
paddingRight: 16,
appearance: "none",
cursor: "pointer",
}}
>
<option value="/">/</option>
<option value="/dashboard">/dashboard</option>
<option value="/login">/login</option>
<option value="/signup">/signup</option>
<option value="/about">/about</option>
<option value="/contact">/contact</option>
<option value="/pricing">/pricing</option>
<option value="/settings">/settings</option>
</select>
onKeyDown={(e) => {
if (e.key === "Enter") {
triggerRefresh(); // force reload iframe
}
}}
/>
<datalist id="common-routes">
<option value="dashboard" />
<option value="login" />
<option value="signup" />
<option value="about" />
<option value="contact" />
<option value="pricing" />
<option value="settings" />
</datalist>
<ChevronDown
size={12}
style={{
@@ -401,12 +425,11 @@ const bar: React.CSSProperties = {
alignItems: "center",
flex: 1,
minWidth: 0,
height: "56px", // Explicitly set height
height: "100%", // Inherit height from parent
padding: "0 16px",
gap: 12,
boxSizing: "border-box",
background: "#fafafa",
borderBottom: "1px solid #e4e4e7",
background: "#faf8f5",
fontFamily: '"Outfit", "Inter", ui-sans-serif, sans-serif',
};