diff --git a/vibn-frontend/app/[workspace]/project/[projectId]/(home)/analytics/page.tsx b/vibn-frontend/app/[workspace]/project/[projectId]/(home)/analytics/page.tsx deleted file mode 100644 index 5cd3740e..00000000 --- a/vibn-frontend/app/[workspace]/project/[projectId]/(home)/analytics/page.tsx +++ /dev/null @@ -1,75 +0,0 @@ -"use client"; - -import { BarChart2 } from "lucide-react"; - -export default function AnalyticsPage() { - return ( -
- Track traffic, usage, and events. -
-- Once your app is live and receiving traffic, your analytics metrics - will appear here. -
-- Connect external services to your application. -
-- Buy, connect and manage your domains.{" "} - - Learn more - -
-- Custom domains are available on our Builder plan and above. Upgrade to - continue working to this app. -
- -{db.consumerEnvKey}={""}
+
+ {db.consumerEnvKey}={""}
+
{k})}
+ {att.keys.map((k) => (
+
+ {k}
+
+ ))}
-{`STORAGE_ENDPOINT=https://storage.googleapis.com
+ {`STORAGE_ENDPOINT=https://storage.googleapis.com
STORAGE_REGION=${s.region ?? "auto"}
STORAGE_BUCKET=${s.bucketName ?? ""}
STORAGE_ACCESS_KEY_ID=${s.hmacAccessId ?? ""}
@@ -637,7 +853,10 @@ STORAGE_SECRET_ACCESS_KEY=`}
{s.errorMessage && (
-
+
{s.errorMessage}
)}
@@ -646,19 +865,28 @@ STORAGE_SECRET_ACCESS_KEY=`}
}
function SecretsDetail({
- resourceUuid, anatomy,
-}: { resourceUuid: string; anatomy: Anatomy }) {
- const r = anatomy.infrastructure.secrets.byResource.find(x => x.resourceUuid === resourceUuid);
+ resourceUuid,
+ anatomy,
+}: {
+ resourceUuid: string;
+ anatomy: Anatomy;
+}) {
+ const r = anatomy.infrastructure.secrets.byResource.find(
+ (x) => x.resourceUuid === resourceUuid,
+ );
if (!r) return This resource is no longer in the project. ;
// Group keys by detected provider so the user sees Stripe / Resend /
// OpenAI bunched together with an Other catch-all for unrecognised keys.
const providerByKey = new Map();
for (const p of anatomy.infrastructure.providers) {
- const att = p.attachments.find(a => a.resourceUuid === resourceUuid);
+ const att = p.attachments.find((a) => a.resourceUuid === resourceUuid);
if (!att) continue;
for (const k of att.keys) {
- providerByKey.set(k, { vendor: p.vendor, category: categoryDef(p.category as CategoryKey).label });
+ providerByKey.set(k, {
+ vendor: p.vendor,
+ category: categoryDef(p.category as CategoryKey).label,
+ });
}
}
@@ -667,7 +895,12 @@ function SecretsDetail({
const tag = providerByKey.get(k);
const groupKey = tag ? `${tag.vendor}` : "Other";
if (!groups.has(groupKey)) {
- groups.set(groupKey, { label: tag ? `${tag.vendor} · ${tag.category}` : "Other (project-defined)", keys: [] });
+ groups.set(groupKey, {
+ label: tag
+ ? `${tag.vendor} · ${tag.category}`
+ : "Other (project-defined)",
+ keys: [],
+ });
}
groups.get(groupKey)!.keys.push(k);
}
@@ -675,25 +908,45 @@ function SecretsDetail({
return (
-
+
Keys
- {[...groups.values()].map(g => (
-
+ {[...groups.values()].map((g) => (
+
{g.label}
- {g.keys.map(k => (
+ {g.keys.map((k) => (
-
+
{k}
-
+
-
+
@@ -715,11 +968,15 @@ function paneHeading(s: Selection, a: Anatomy | null): string {
if (!a) return "Details";
if (!s) return "Overview";
if (s.kind === "category") return `About · ${categoryDef(s.category).label}`;
- if (s.kind === "database") return `Database · ${a.infrastructure.databases.find(x => x.uuid === s.uuid)?.name ?? ""}`;
- if (s.kind === "table") return `Preview · ${s.schema === "public" ? s.name : `${s.schema}.${s.name}`}`;
- if (s.kind === "provider") return `Provider · ${a.infrastructure.providers.find(x => x.id === s.id)?.vendor ?? ""}`;
- if (s.kind === "storage") return "Storage · Workspace bucket";
- if (s.kind === "secrets") return `Secrets · ${a.infrastructure.secrets.byResource.find(x => x.resourceUuid === s.resourceUuid)?.resourceName ?? ""}`;
+ if (s.kind === "database")
+ return `Database · ${a.infrastructure.databases.find((x) => x.uuid === s.uuid)?.name ?? ""}`;
+ if (s.kind === "table")
+ return `Preview · ${s.schema === "public" ? s.name : `${s.schema}.${s.name}`}`;
+ if (s.kind === "provider")
+ return `Provider · ${a.infrastructure.providers.find((x) => x.id === s.id)?.vendor ?? ""}`;
+ if (s.kind === "storage") return "Storage · Workspace bucket";
+ if (s.kind === "secrets")
+ return `Secrets · ${a.infrastructure.secrets.byResource.find((x) => x.resourceUuid === s.resourceUuid)?.resourceName ?? ""}`;
return "Details";
}
@@ -727,7 +984,8 @@ function statusColor(status: string) {
const s = (status ?? "").toLowerCase();
if (s.includes("running") || s.includes("healthy")) return "#2e7d32";
if (s.includes("starting") || s.includes("deploying")) return "#d4a04a";
- if (s.includes("exit") || s.includes("fail") || s.includes("unhealthy")) return "#c5392b";
+ if (s.includes("exit") || s.includes("fail") || s.includes("unhealthy"))
+ return "#c5392b";
return "#a09a90";
}
function storageColor(status: string) {
@@ -741,42 +999,40 @@ function tileButtonStyle(active: boolean): React.CSSProperties {
return {
...railItem,
borderColor: active ? INK.ink : INK.borderSoft,
- boxShadow: active ? `0 0 0 1px ${INK.ink}` : "none",
- background: active ? "#fffdf8" : INK.cardBg,
+ boxShadow: active ? `0 0 0 1px ${INK.ink}` : "none",
+ background: active ? "#fffdf8" : INK.cardBg,
};
}
-function KvRow({
- label, value, dot, mono,
-}: { label: string; value: string; dot?: string; mono?: boolean }) {
- return (
-
- {label}
-
- {dot && }
-
- {value}
-
-
-
- );
-}
-
function SectionTitle({ children }: { children: React.ReactNode }) {
return {children};
}
-function Para({ children, style }: { children: React.ReactNode; style?: React.CSSProperties }) {
+function Para({
+ children,
+ style,
+}: {
+ children: React.ReactNode;
+ style?: React.CSSProperties;
+}) {
return {children}
;
}
function Inline({ children }: { children: React.ReactNode }) {
return (
-
+
{children}
);
@@ -784,18 +1040,32 @@ function Inline({ children }: { children: React.ReactNode }) {
function Empty({ children }: { children: React.ReactNode }) {
return (
-
+
{children}
);
}
function OverviewStat({
- label, value, onClick,
-}: { label: string; value: number; onClick?: () => void }) {
+ label,
+ value,
+ onClick,
+}: {
+ label: string;
+ value: number;
+ onClick?: () => void;
+}) {
return (
-
-
- Integrations
-
-
-
-
-
-
- My Integrations
-
-
- Browse
-
-
-
-
-
-
-
- Stripe
-
- Sell products or subscriptions and get paid online.
-
-
-
- Manage
-
-
-
-
-
- Connectors
-
-
- Connect your app to popular services.
-
-
-
- {[1, 2, 3, 4].map((i) => (
-
-
-
-
-
- Connector {i}
-
-
- Connect with external service for app data.
-
-
- ))}
-
-
-
-
-
-
-
-
-
- Unlock this feature
-
-
- This feature is only available on the Builder plan or higher.
- Upgrade to continue working without limits.
-
-
- Upgrade
-
-
-
-
- );
-}
diff --git a/vibn-frontend/app/[workspace]/project/[projectId]/(home)/layout.tsx b/vibn-frontend/app/[workspace]/project/[projectId]/(home)/layout.tsx
index b58157f6..98e626b9 100644
--- a/vibn-frontend/app/[workspace]/project/[projectId]/(home)/layout.tsx
+++ b/vibn-frontend/app/[workspace]/project/[projectId]/(home)/layout.tsx
@@ -42,6 +42,6 @@ const pageWrap: React.CSSProperties = {
flex: 1,
minHeight: 0,
height: "100vh",
- background: "#faf8f5",
+ background: "#f9fafb",
overflow: "hidden",
};
diff --git a/vibn-frontend/app/[workspace]/project/[projectId]/(home)/logs/page.tsx b/vibn-frontend/app/[workspace]/project/[projectId]/(home)/logs/page.tsx
index 6b2de0e6..fd89ab40 100644
--- a/vibn-frontend/app/[workspace]/project/[projectId]/(home)/logs/page.tsx
+++ b/vibn-frontend/app/[workspace]/project/[projectId]/(home)/logs/page.tsx
@@ -1,97 +1,217 @@
"use client";
-import { Search } from "lucide-react";
+import { useState, useEffect } from "react";
+import { useParams } from "next/navigation";
+import { Activity, Loader2, RefreshCw } from "lucide-react";
+import { useAnatomy, type Anatomy } from "@/components/project/use-anatomy";
+import {
+ THEME,
+ PageHeader,
+ Card,
+ EmptyState,
+ SecondaryButton,
+} from "@/components/project/dashboard-ui";
+
+type LiveApp = Anatomy["hosting"]["live"][number];
export default function LogsPage() {
+ const params = useParams();
+ const projectId = params.projectId as string;
+ const { anatomy, loading } = useAnatomy(projectId, { pollMs: 8000 });
+ const live = anatomy?.hosting.live ?? [];
+
+ const [activeUuid, setActiveUuid] = useState(null);
+ const [logs, setLogs] = useState(null);
+ const [logsLoading, setLogsLoading] = useState(false);
+
+ // Auto-select first app if none selected
+ useEffect(() => {
+ if (live.length > 0 && !activeUuid) {
+ setActiveUuid(live[0].uuid);
+ }
+ }, [live, activeUuid]);
+
+ const fetchLogs = async (uuid: string) => {
+ setLogsLoading(true);
+ try {
+ const r = await fetch(`/api/mcp`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ action: "apps.logs",
+ params: { uuid, lines: 100 },
+ }),
+ });
+ const d = await r.json();
+ setLogs(
+ typeof d.result === "string"
+ ? d.result
+ : JSON.stringify(d.result ?? d.error, null, 2),
+ );
+ } catch {
+ setLogs("Failed to load logs. Is the container running?");
+ } finally {
+ setLogsLoading(false);
+ }
+ };
+
+ // Fetch when active app changes
+ useEffect(() => {
+ if (activeUuid) fetchLogs(activeUuid);
+ }, [activeUuid]);
+
return (
-
-
- Logs
-
-
- View application and server logs.
-
-
-
-
-
-
-
+
+ {loading && !anatomy ? (
+
+
+ >
+ Loading…
+
+
+ ) : live.length === 0 ? (
+ }
+ title="No apps running"
+ hint="Once you deploy an app, its runtime logs will appear here."
+ />
+ ) : (
+
+ {/* App Picker Column */}
+
+ {live.map((app) => (
+ setActiveUuid(app.uuid)}
+ style={{
+ textAlign: "left",
+ padding: "10px 14px",
+ background:
+ activeUuid === app.uuid ? THEME.subtleBg : THEME.cardBg,
+ border: `1px solid ${activeUuid === app.uuid ? THEME.border : "transparent"}`,
+ borderRadius: THEME.radiusSm,
+ cursor: "pointer",
+ fontSize: "0.875rem",
+ fontWeight: 500,
+ color: activeUuid === app.uuid ? THEME.ink : THEME.mid,
+ transition: "all 0.15s ease",
+ }}
+ >
+ {app.name}
+
+ ))}
+
+
+ {/* Log Viewer Column */}
+
+
+
+ {live.find((a) => a.uuid === activeUuid)?.name ?? "Logs"}
+
+
+ ) : (
+
+ )
+ }
+ onClick={() => activeUuid && fetchLogs(activeUuid)}
+ disabled={logsLoading}
+ >
+ Refresh
+
+
+
+
+ {logsLoading && !logs
+ ? "Loading..."
+ : logs || "No logs available."}
+
+
+
-
-
-
- 14:32:01
- [info]
- Server started on port 3000
-
-
- 14:32:05
- [info]
- Database connected successfully
-
-
- 14:45:12
- [http]
- GET /api/users 200 OK - 45ms
-
-
+ )}
);
diff --git a/vibn-frontend/app/[workspace]/project/[projectId]/(home)/marketing/page.tsx b/vibn-frontend/app/[workspace]/project/[projectId]/(home)/marketing/page.tsx
deleted file mode 100644
index ec0ee46f..00000000
--- a/vibn-frontend/app/[workspace]/project/[projectId]/(home)/marketing/page.tsx
+++ /dev/null
@@ -1,6 +0,0 @@
-import { redirect } from "next/navigation";
-
-export default async function MarketingPage({ params }: { params: Promise<{ workspace: string; projectId: string }> }) {
- const { workspace, projectId } = await params;
- redirect(`/${workspace}/project/${projectId}/marketing/seo`);
-}
diff --git a/vibn-frontend/app/[workspace]/project/[projectId]/(home)/marketing/seo/page.tsx b/vibn-frontend/app/[workspace]/project/[projectId]/(home)/marketing/seo/page.tsx
deleted file mode 100644
index 28dc940d..00000000
--- a/vibn-frontend/app/[workspace]/project/[projectId]/(home)/marketing/seo/page.tsx
+++ /dev/null
@@ -1,366 +0,0 @@
-"use client";
-
-import { ListFilter } from "lucide-react";
-
-export default function SeoPage() {
- return (
-
-
-
-
- SEO & GEO
-
-
- Improve how your app appears in search results and AI answers.
-
-
-
-
- Enable SEO for this app
-
-
-
-
-
-
-
-
-
- Overview
-
-
- Meta tags
-
-
- Advanced Settings
-
-
-
-
-
-
-
-
- Run an SEO & GEO scan
-
-
- Scan your app for SEO basics and GEO details. Get a prioritized
- checklist to fix issues in minutes.
-
-
- Run Scan
-
-
-
-
-
-
-
- AI Assistant Discovery
-
-
- Help AI search engines understand and recommend your app
-
-
-
-
-
-
-
-
-
-
- Generate robots.txt
-
-
- Off: serve your deployed public/robots.txt if shipped, otherwise
- return 404.
-
-
-
-
-
-
-
-
-
-
- Generate sitemap.xml
-
-
- Off: serve your deployed public/sitemap.xml if shipped, otherwise
- return 404.
-
-
-
-
-
-
-
-
-
-
- Auto-generate per-page breadcrumbs
-
-
- Build a fresh BreadcrumbList for each route instead of using the
- same persisted list site-wide. Turn off if you hand crafted your
- breadcrumb schema and want it served verbatim.
-
-
-
-
-
-
-
-
- );
-}
diff --git a/vibn-frontend/app/[workspace]/project/[projectId]/(home)/marketing/social/page.tsx b/vibn-frontend/app/[workspace]/project/[projectId]/(home)/marketing/social/page.tsx
deleted file mode 100644
index bce3688d..00000000
--- a/vibn-frontend/app/[workspace]/project/[projectId]/(home)/marketing/social/page.tsx
+++ /dev/null
@@ -1,89 +0,0 @@
-"use client";
-
-import { Share2 } from "lucide-react";
-
-export default function SocialPage() {
- return (
-
-
-
- Social Content
-
-
- Manage social sharing campaigns and meta tags.
-
-
-
-
-
-
-
-
- Social Campaign Manager
-
-
- Automatically generate and schedule social media content across
- platforms based on your app's pages.
-
-
- Connect Social Accounts
-
-
-
- );
-}
diff --git a/vibn-frontend/app/[workspace]/project/[projectId]/(home)/overview/page.tsx b/vibn-frontend/app/[workspace]/project/[projectId]/(home)/overview/page.tsx
index 126d5cbc..d6e7169b 100644
--- a/vibn-frontend/app/[workspace]/project/[projectId]/(home)/overview/page.tsx
+++ b/vibn-frontend/app/[workspace]/project/[projectId]/(home)/overview/page.tsx
@@ -16,7 +16,15 @@ import {
Terminal,
Server,
} from "lucide-react";
-import { useAnatomy, type Anatomy } from "@/components/project/use-anatomy";
+import {
+ THEME,
+ PageHeader,
+ Card,
+ SectionHeader,
+ EmptyState,
+ Badge,
+ SecondaryButton,
+} from "@/components/project/dashboard-ui";
/**
* Hosting tab — user-facing: "Is my thing live? How do I reach it?"
@@ -51,67 +59,96 @@ export default function OverviewTab() {
const showLoading = loading && !anatomy;
return (
-
- {showLoading && (
-
-
-
- Loading…
-
-
- )}
- {error && !showLoading && (
-
-
- {error}
-
- )}
+
+
+
- {anatomy && (
- <>
- {/* ── Live endpoints ── */}
-
-
- {anatomy.hosting.live.length === 0 ? (
- }
- title="Nothing deployed yet"
- hint="Ask the AI to deploy your app and it will appear here."
- promptSuggestion="Deploy my app to production"
- />
- ) : (
-
- {anatomy.hosting.live.map((item) => (
-
- ))}
-
- )}
-
+ {showLoading && (
+
+
+ Loading…
+
+
+ )}
+ {error && !showLoading && (
+
+
+ {error}
+
+
+ )}
- {/* ── Previews ── */}
- {anatomy.hosting.previews.length > 0 && (
-
-
-
- {anatomy.hosting.previews.map((p) => (
-
- ))}
-
+ {anatomy && (
+ <>
+ {/* ── Live endpoints ── */}
+
+
+ {anatomy.hosting.live.length === 0 ? (
+ }
+ title="Nothing deployed yet"
+ hint="Ask the AI to deploy your app and it will appear here."
+ />
+ ) : (
+
+ {anatomy.hosting.live.map((item) => (
+
+ ))}
+
+ )}
- )}
- >
- )}
+
+ {/* ── Previews ── */}
+ {anatomy.hosting.previews.length > 0 && (
+
+
+
+ {anatomy.hosting.previews.map((p) => (
+
+ ))}
+
+
+ )}
+ >
+ )}
+
);
}
@@ -130,6 +167,14 @@ function LiveCard({ item, projectId }: { item: LiveItem; projectId: string }) {
const primaryUrl = item.fqdn ? `https://${item.fqdn}` : null;
const phase = classifyPhase(item.status);
const { color: statusColor, label: statusLabel } = phaseDisplay(phase, item);
+ const statusTheme =
+ phase === "healthy"
+ ? "success"
+ : phase === "building"
+ ? "warning"
+ : phase === "failed"
+ ? "danger"
+ : "neutral";
const redeploy = async () => {
if (deploying) return;
@@ -185,77 +230,91 @@ function LiveCard({ item, projectId }: { item: LiveItem; projectId: string }) {
};
return (
-
- {/* ── Card header ── */}
-
-
-
- {item.name}
-
- {item.source === "repo" ? "built" : "image"}
+
+
+
+
+
+ {item.name}
+
+ {item.source === "repo" ? "built" : "image"}
+
-
-
- {deploying ? (
-
+
) : (
-
- )}
- {deploying ? "Deploying…" : "Redeploy"}
-
-
+
+ )
+ }
+ >
+ {deploying ? "Deploying…" : "Redeploy"}
+
- {/* ── Status line ── */}
-
+
{statusLabel}
{item.lastBuild && (
-
+
· Last build {item.lastBuild.status}{" "}
{formatRelative(item.lastBuild.finishedAt)}
)}
- {/* ── Live URL ── */}
{primaryUrl ? (
-
-
-
+
) : (
-
-
+
+
@@ -264,15 +323,14 @@ function LiveCard({ item, projectId }: { item: LiveItem; projectId: string }) {
)}
- {/* ── Extra domains ── */}
{item.domains.length > 1 && (
{item.domains.slice(1).map((d) => (
@@ -281,45 +339,78 @@ function LiveCard({ item, projectId }: { item: LiveItem; projectId: string }) {
href={`https://${d}`}
target="_blank"
rel="noreferrer"
- style={{ ...urlLink, fontSize: "0.78rem", color: INK.mid }}
+ style={{
+ fontSize: "0.8rem",
+ color: THEME.mid,
+ textDecoration: "none",
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
+ }}
>
- {d}{" "}
-
+ {d}
))}
)}
- {/* ── Logs toggle ── */}
-
+
{logsOpen ? "Hide logs" : "Show recent logs"}
{logsOpen ? : }
{logsOpen && (
-
+
{logsLoading ? (
-
+
Loading…
) : (
- {logs || "(no logs)"}
+
+ {logs || "(no logs)"}
+
)}
)}
-
+
);
}
@@ -330,43 +421,46 @@ function LiveCard({ item, projectId }: { item: LiveItem; projectId: string }) {
function PreviewRow({ preview }: { preview: Preview }) {
const running = preview.state === "running";
return (
-
-
-
-
- {preview.name}
-
-
- port {preview.port}
-
- {preview.url && running && (
-
+
+
+ {preview.name}
+
+
+ port {preview.port}
+
+ {preview.url && running && (
+
- )}
-
-
+ {preview.url.replace(/^https?:\/\//, "")}{" "}
+
+
+
+ )}
+
);
}
@@ -427,15 +521,6 @@ function formatRelative(iso: string | undefined) {
// Sub-components
// ──────────────────────────────────────────────────
-function SectionHeader({ title, count }: { title: string; count: number }) {
- return (
-
- {title}
- {count}
-
- );
-}
-
function EmptySection({
icon,
title,
@@ -492,13 +577,13 @@ function EmptySection({
// ──────────────────────────────────────────────────
const INK = {
- ink: "#1a1a1a",
- mid: "#5f5e5a",
- muted: "#a09a90",
- border: "#e8e4dc",
- borderSoft: "#efebe1",
+ ink: "#111827",
+ mid: "#4b5563",
+ muted: "#9ca3af",
+ border: "#e5e7eb",
+ borderSoft: "#f3f4f6",
cardBg: "#fff",
- fontSans: '"Outfit", "Inter", ui-sans-serif, sans-serif',
+ fontSans: '"Inter", ui-sans-serif, system-ui, -apple-system, sans-serif',
} as const;
const GREEN = "#10b981";
const AMBER = "#f59e0b";
diff --git a/vibn-frontend/app/[workspace]/project/[projectId]/(home)/plan/page.tsx b/vibn-frontend/app/[workspace]/project/[projectId]/(home)/plan/page.tsx
index 094b4467..9e8ee865 100644
--- a/vibn-frontend/app/[workspace]/project/[projectId]/(home)/plan/page.tsx
+++ b/vibn-frontend/app/[workspace]/project/[projectId]/(home)/plan/page.tsx
@@ -21,6 +21,16 @@ import {
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import useSWR from "swr";
+import {
+ THEME,
+ PageHeader,
+ Card,
+ SectionHeader,
+ PrimaryButton,
+ SecondaryButton,
+ Badge,
+ StatusDot,
+} from "@/components/project/dashboard-ui";
// ──────────────────────────────────────────────────
// Types & Fetcher
@@ -88,101 +98,125 @@ export default function PlanTab() {
const showLoading = !plan && !error;
return (
-
- {showLoading && (
-
-
-
- Loading plan…
-
-
- )}
- {error && !showLoading && (
-
-
-
- {error.message || "Failed to load plan"}
-
-
- )}
+
+
+
- {plan && (
-
- {/* ── Left Rail (Master Index) ── */}
-
-
-
- Scope
-
-
- }
- selectedId={selectedId}
- onClick={setSelectedId}
- />
-
+ {showLoading && (
+
+
+ Loading plan…
+
+ )}
+ {error && !showLoading && (
+
+
+ {error.message || "Failed to load plan"}
+
+
+ )}
-
-
- Blueprint
-
-
- {BLUEPRINT_DOCS.map((doc) => (
+ {plan && (
+
+ {/* ── Left Rail (Master Index) ── */}
+
+
+
+ Scope
+
+
}
selectedId={selectedId}
onClick={setSelectedId}
/>
- ))}
+
-
-
-
- Delegate to AI
+
+
+ Blueprint
+
+
+ {BLUEPRINT_DOCS.map((doc) => (
+
+ ))}
+
-
- }
- selectedId={selectedId}
- onClick={setSelectedId}
+
+
+
+ Delegate to AI
+
+
+ }
+ selectedId={selectedId}
+ onClick={setSelectedId}
+ />
+
+
+
+
+ {/* ── Right Rail (Detail Viewer) ── */}
+
+ {selectedId === "objective" && (
+
-
-
-
+ )}
- {/* ── Right Rail (Detail Viewer) ── */}
-
- {selectedId === "objective" && (
-
- )}
+ {BLUEPRINT_DOCS.some((d) => d.id === selectedId) && (
+
+ )}
- {BLUEPRINT_DOCS.some((d) => d.id === selectedId) && (
-
- )}
-
- {selectedId === "kanban" && (
-
- )}
-
-
- )}
+ {selectedId === "kanban" && (
+
+ )}
+
+
+ )}
+
);
}
@@ -210,15 +244,15 @@ function RailItem({
onClick={() => onClick(id)}
style={{
...flatTile,
- background: isActive ? INK.cardBg : "transparent",
- borderColor: isActive ? INK.border : "transparent",
- boxShadow: isActive ? "0 1px 3px rgba(0,0,0,0.02)" : "none",
- color: isActive ? INK.ink : INK.muted,
+ background: isActive ? THEME.cardBg : "transparent",
+ borderColor: isActive ? THEME.border : "transparent",
+ boxShadow: isActive ? THEME.shadow : "none",
+ color: isActive ? THEME.ink : THEME.muted,
}}
>
{React.cloneElement(icon, {
size: 15,
- color: isActive ? INK.ink : INK.muted,
+ color: isActive ? THEME.ink : THEME.muted,
} as React.SVGProps & { size?: number | string })}
{label}
@@ -279,125 +313,156 @@ function ObjectivePanel({
};
return (
-
-
+
+
- Product Brief
-
+
+ Product Brief
+
+
The high-level business case and elevator pitch.
- {saving && (
-
- Saving...
-
- )}
{!editing && (
- setEditing(true)} style={actionBtn}>
- Edit Objective
-
+ setEditing(true)}
+ icon={ }
+ >
+ Edit Objective
+
)}
{editing && (
<>
- save(draft)}
disabled={!dirty || saving}
- className="btn-primary"
>
- Save Changes
-
-
- Cancel
-
+ {saving ? "Saving…" : "Save Changes"}
+
+ Cancel
>
)}
-
- {editing ? (
- <>
-
- setEditorView("write")}
- style={
- editorView === "write" ? editorTabActive : editorTabInactive
- }
- >
- Write
-
- setEditorView("preview")}
- style={
- editorView === "preview" ? editorTabActive : editorTabInactive
- }
- >
- Preview
-
-
- {dirty && (
-
+
+ {editing ? (
+ <>
+
+ setEditorView("write")}
+ style={
+ editorView === "write" ? editorTabActive : editorTabInactive
+ }
>
- ● Unsaved
-
- )}
-
-
- {editorView === "write" ? (
-
-
+
);
}
@@ -414,7 +479,15 @@ function DocumentPanel({ plan, docId }: { plan: Plan; docId: string }) {
const content = plan.blueprint?.[docId as keyof typeof plan.blueprint];
return (
-
+
{content ? (
{content}
) : (
- <>
-
-
- {docConfig.label}
- This document is currently empty.
-
+
+
+ {docConfig.icon}
-
- {React.cloneElement(
- docConfig.icon as React.ReactElement,
- {
- size: 32,
- color: INK.muted,
- style: { marginBottom: 16, opacity: 0.5 },
- } as React.SVGProps & { size?: number | string },
- )}
-
- Not generated yet.
-
-
+
- This document is generated and maintained by the AI agent. Chat
- with your agent to update the scope and blueprint.
+ {docConfig.label}
+
+
+ This document is currently empty. Ask the AI to draft it.
- >
+
)}
-
+
);
}
@@ -507,43 +584,38 @@ function DelegatePanel({ plan, projectId }: { plan: Plan; projectId: string }) {
};
const TaskCard = ({ t }: { t: Plan["tasks"][number] }) => (
-
+
-
+
- {t.status === "done" && (
-
- )}
+ ? "neutral"
+ : "warning"
+ }
+ />
{t.title}
@@ -552,12 +624,10 @@ function DelegatePanel({ plan, projectId }: { plan: Plan; projectId: string }) {
{t.description}
@@ -565,43 +635,66 @@ function DelegatePanel({ plan, projectId }: { plan: Plan; projectId: string }) {
)}
-
-
+
+
{t.status === "open"
? "Queued"
: t.status === "done"
? "Completed"
: "In Progress"}
-
+
-
+
);
return (
-
+
- Execution Plan
-
+
+ Execution Plan
+
+
The prioritized roadmap for the AI background runner to execute.
-
{delegating ? "Starting Jarvis..." : "Delegate Build"}
-
+
>(new Set());
const [selection, setSelection] = useState(null);
useEffect(() => {
if (codebases && codebases[0]) {
- setExpanded(prev => (prev.size === 0 ? new Set([codebases[0].id]) : prev));
+ setExpanded((prev) =>
+ prev.size === 0 ? new Set([codebases[0].id]) : prev,
+ );
}
}, [codebases]);
@@ -52,7 +59,7 @@ export default function ProductTab() {
}, [projectId]);
const toggleCodebase = (id: string) => {
- setExpanded(prev => {
+ setExpanded((prev) => {
const next = new Set(prev);
if (next.has(id)) next.delete(id);
else next.add(id);
@@ -68,10 +75,14 @@ export default function ProductTab() {
{/* ── Left rail ── */}
{showLoading && (
- Loading…
+
+ Loading…
+
)}
{error && !showLoading && (
- {error}
+
+ {error}
+
)}
{anatomy && (
@@ -80,12 +91,24 @@ export default function ProductTab() {
{codebases && codebases.length === 0 && (
- {reason === "no_repo"
- ? <>No codebase yet. Try: "Start building my app">
- : <>Repo is empty — push a first commit. Try: "Scaffold a Next.js app">}
+ {reason === "no_repo" ? (
+ <>
+ No codebase yet.{" "}
+
+ Try: "Start building my app"
+
+ >
+ ) : (
+ <>
+ Repo is empty — push a first commit.{" "}
+
+ Try: "Scaffold a Next.js app"
+
+ >
+ )}
)}
- {codebases?.map(cb => {
+ {codebases?.map((cb) => {
const isOpen = expanded.has(cb.id);
return (
@@ -96,11 +119,19 @@ export default function ProductTab() {
aria-expanded={isOpen}
>
- {isOpen
- ?
- : }
+ {isOpen ? (
+
+ ) : (
+
+ )}
-
+
{cb.label}
{cb.hint && {cb.hint}}
@@ -112,12 +143,17 @@ export default function ProductTab() {
projectId={projectId}
rootPath={cb.path}
selectedPath={
- selection?.type === "file" && selection.codebaseId === cb.id
+ selection?.type === "file" &&
+ selection.codebaseId === cb.id
? selection.path
: undefined
}
onSelectFile={(p) =>
- setSelection({ type: "file", codebaseId: cb.id, path: p })
+ setSelection({
+ type: "file",
+ codebaseId: cb.id,
+ path: p,
+ })
}
/>
@@ -131,31 +167,62 @@ export default function ProductTab() {
{images && images.length === 0 && (
- Self-hosted tools (Twenty CRM, n8n, Plausible…) you run appear here.
- Try: "Install Twenty CRM for my project"
+ Self-hosted tools (Twenty CRM, n8n, Plausible…) you run
+ appear here.
+
+ Try: "Install Twenty CRM for my project"
+
)}
- {images?.map(img => (
+ {images?.map((img) => (
setSelection({ type: "image", uuid: img.uuid })}
+ onClick={() =>
+ setSelection({ type: "image", uuid: img.uuid })
+ }
style={{
...flatTile,
- borderColor: selection?.type === "image" && selection.uuid === img.uuid ? INK.ink : INK.borderSoft,
- boxShadow: selection?.type === "image" && selection.uuid === img.uuid ? `0 0 0 1px ${INK.ink}` : "none",
- background: selection?.type === "image" && selection.uuid === img.uuid ? "#fffdf8" : INK.cardBg,
+ borderColor:
+ selection?.type === "image" &&
+ selection.uuid === img.uuid
+ ? INK.ink
+ : INK.borderSoft,
+ boxShadow:
+ selection?.type === "image" &&
+ selection.uuid === img.uuid
+ ? `0 0 0 1px ${INK.ink}`
+ : "none",
+ background:
+ selection?.type === "image" &&
+ selection.uuid === img.uuid
+ ? "#fffdf8"
+ : INK.cardBg,
}}
- aria-pressed={selection?.type === "image" && selection.uuid === img.uuid}
+ aria-pressed={
+ selection?.type === "image" && selection.uuid === img.uuid
+ }
>
-
+
{img.name}
- {img.image}{img.version ? `:${img.version}` : ""}
+ {img.image}
+ {img.version ? `:${img.version}` : ""}
- {img.status && }
+ {img.status && (
+
+ )}
))}
@@ -188,22 +255,26 @@ export default function ProductTab() {
// ──────────────────────────────────────────────────
function ImageDetail({ uuid, anatomy }: { uuid: string; anatomy: Anatomy }) {
- const img = anatomy.product.images.find(i => i.uuid === uuid);
+ const img = anatomy.product.images.find((i) => i.uuid === uuid);
if (!img) return This image is no longer in the project. ;
- const live = anatomy.hosting.live.find(l => l.uuid === uuid);
+ const live = anatomy.hosting.live.find((l) => l.uuid === uuid);
return (
-
+
-
+
{live?.fqdn && (
-
+
)}
);
@@ -214,8 +285,14 @@ function ImageDetail({ uuid, anatomy }: { uuid: string; anatomy: Anatomy }) {
// ──────────────────────────────────────────────────
function RailGroup({
- title, count, children,
-}: { title: string; count: number; children: React.ReactNode }) {
+ title,
+ count,
+ children,
+}: {
+ title: string;
+ count: number;
+ children: React.ReactNode;
+}) {
return (
@@ -232,16 +309,28 @@ function RailEmpty({ children }: { children: React.ReactNode }) {
}
function DetailRow({
- label, value, dot, href,
-}: { label: string; value: string; dot?: string; href?: string }) {
+ label,
+ value,
+ dot,
+ href,
+}: {
+ label: string;
+ value: string;
+ dot?: string;
+ href?: string;
+}) {
return (
{label}
{dot && }
{href ? (
- {value}
- ) : value}
+
+ {value}
+
+ ) : (
+ value
+ )}
);
@@ -249,11 +338,19 @@ function DetailRow({
function Inline({ children }: { children: React.ReactNode }) {
return (
-
+
{children}
);
@@ -261,10 +358,18 @@ function Inline({ children }: { children: React.ReactNode }) {
function Empty({ children }: { children: React.ReactNode }) {
return (
-
+
{children}
);
@@ -286,7 +391,8 @@ function statusColor(status: string) {
const s = status.toLowerCase();
if (s.includes("running") || s.includes("healthy")) return "#2e7d32";
if (s.includes("starting") || s.includes("deploying")) return "#d4a04a";
- if (s.includes("exit") || s.includes("fail") || s.includes("unhealthy")) return "#c5392b";
+ if (s.includes("exit") || s.includes("fail") || s.includes("unhealthy"))
+ return "#c5392b";
return "#a09a90";
}
@@ -295,13 +401,13 @@ function statusColor(status: string) {
// ──────────────────────────────────────────────────
const INK = {
- ink: "#1a1a1a",
- mid: "#5f5e5a",
- muted: "#a09a90",
- border: "#e8e4dc",
- borderSoft: "#efebe1",
+ ink: "#111827",
+ mid: "#4b5563",
+ muted: "#9ca3af",
+ border: "#e5e7eb",
+ borderSoft: "#f3f4f6",
cardBg: "#fff",
- fontSans: '"Outfit", "Inter", ui-sans-serif, sans-serif',
+ fontSans: '"Inter", ui-sans-serif, system-ui, -apple-system, sans-serif',
} as const;
const pageWrap: React.CSSProperties = {
@@ -318,79 +424,157 @@ const grid: React.CSSProperties = {
alignItems: "stretch",
};
const leftCol: React.CSSProperties = {
- minWidth: 0, display: "flex", flexDirection: "column", gap: 18,
+ minWidth: 0,
+ display: "flex",
+ flexDirection: "column",
+ gap: 18,
};
const rightCol: React.CSSProperties = {
- minWidth: 0, display: "flex", flexDirection: "column",
+ minWidth: 0,
+ display: "flex",
+ flexDirection: "column",
};
const heading: React.CSSProperties = {
- fontSize: "0.72rem", fontWeight: 600, letterSpacing: "0.12em",
- textTransform: "uppercase", color: INK.muted, margin: "0 0 14px",
+ fontSize: "0.72rem",
+ fontWeight: 600,
+ letterSpacing: "0.12em",
+ textTransform: "uppercase",
+ color: INK.muted,
+ margin: "0 0 14px",
+};
+const railGroup: React.CSSProperties = {
+ display: "flex",
+ flexDirection: "column",
};
-const railGroup: React.CSSProperties = { display: "flex", flexDirection: "column" };
const railGroupHeader: React.CSSProperties = {
- display: "flex", alignItems: "center", justifyContent: "space-between",
+ display: "flex",
+ alignItems: "center",
+ justifyContent: "space-between",
padding: "0 4px 8px",
};
const railGroupTitle: React.CSSProperties = {
- fontSize: "0.68rem", fontWeight: 600, letterSpacing: "0.12em",
- textTransform: "uppercase", color: INK.muted,
+ fontSize: "0.68rem",
+ fontWeight: 600,
+ letterSpacing: "0.12em",
+ textTransform: "uppercase",
+ color: INK.muted,
};
const countPill: React.CSSProperties = {
- fontSize: "0.7rem", fontWeight: 600, color: INK.mid,
- padding: "1px 7px", borderRadius: 999, background: "#f3eee4",
+ fontSize: "0.7rem",
+ fontWeight: 600,
+ color: INK.mid,
+ padding: "1px 7px",
+ borderRadius: 999,
+ background: "#f3eee4",
+};
+const railItems: React.CSSProperties = {
+ display: "flex",
+ flexDirection: "column",
+ gap: 10,
};
-const railItems: React.CSSProperties = { display: "flex", flexDirection: "column", gap: 10 };
const railEmpty: React.CSSProperties = {
- padding: "10px 12px", fontSize: "0.74rem", color: INK.muted,
- border: `1px dashed ${INK.borderSoft}`, borderRadius: 8,
+ padding: "10px 12px",
+ fontSize: "0.74rem",
+ color: INK.muted,
+ border: `1px dashed ${INK.borderSoft}`,
+ borderRadius: 8,
lineHeight: 1.6,
};
const nudge: React.CSSProperties = {
- display: "block", marginTop: 6, fontStyle: "normal",
- background: "#f3eee4", borderRadius: 4, padding: "3px 8px",
- fontSize: "0.72rem", color: "#7a6a50",
+ display: "block",
+ marginTop: 6,
+ fontStyle: "normal",
+ background: "#f3eee4",
+ borderRadius: 4,
+ padding: "3px 8px",
+ fontSize: "0.72rem",
+ color: "#7a6a50",
};
const flatTile: React.CSSProperties = {
- display: "flex", alignItems: "center", gap: 10,
- width: "100%", padding: "12px 14px",
- background: INK.cardBg, border: `1px solid ${INK.borderSoft}`, borderRadius: 10,
- cursor: "pointer", font: "inherit", color: "inherit",
+ display: "flex",
+ alignItems: "center",
+ gap: 10,
+ width: "100%",
+ padding: "12px 14px",
+ background: INK.cardBg,
+ border: `1px solid ${INK.borderSoft}`,
+ borderRadius: 10,
+ cursor: "pointer",
+ font: "inherit",
+ color: "inherit",
transition: "border-color 0.12s, background 0.12s, box-shadow 0.12s",
};
const codebaseTile: React.CSSProperties = {
- background: INK.cardBg, border: `1px solid ${INK.borderSoft}`, borderRadius: 10, overflow: "hidden",
+ background: INK.cardBg,
+ border: `1px solid ${INK.borderSoft}`,
+ borderRadius: 10,
+ overflow: "hidden",
};
const tileHeader: React.CSSProperties = {
- display: "flex", alignItems: "center", gap: 8, width: "100%",
- padding: "12px 14px", background: "transparent", border: "none",
- cursor: "pointer", font: "inherit", color: "inherit",
+ display: "flex",
+ alignItems: "center",
+ gap: 8,
+ width: "100%",
+ padding: "12px 14px",
+ background: "transparent",
+ border: "none",
+ cursor: "pointer",
+ font: "inherit",
+ color: "inherit",
};
const tileLabel: React.CSSProperties = {
- fontSize: "0.85rem", fontWeight: 600, color: INK.ink, marginBottom: 2,
+ fontSize: "0.85rem",
+ fontWeight: 600,
+ color: INK.ink,
+ marginBottom: 2,
+};
+const tileHint: React.CSSProperties = {
+ fontSize: "0.74rem",
+ color: INK.mid,
+ lineHeight: 1.4,
};
-const tileHint: React.CSSProperties = { fontSize: "0.74rem", color: INK.mid, lineHeight: 1.4 };
const tileBody: React.CSSProperties = {
- padding: "8px 10px 12px", borderTop: `1px solid ${INK.borderSoft}`,
+ padding: "8px 10px 12px",
+ borderTop: `1px solid ${INK.borderSoft}`,
};
const chevronCell: React.CSSProperties = {
- width: 14, display: "inline-flex", alignItems: "center", justifyContent: "center", flexShrink: 0,
+ width: 14,
+ display: "inline-flex",
+ alignItems: "center",
+ justifyContent: "center",
+ flexShrink: 0,
};
const panel: React.CSSProperties = {
- background: INK.cardBg, border: `1px solid ${INK.border}`, borderRadius: 10,
- padding: 16, flex: 1, minHeight: 0, display: "flex", flexDirection: "column",
+ background: INK.cardBg,
+ border: `1px solid ${INK.border}`,
+ borderRadius: 10,
+ padding: 16,
+ flex: 1,
+ minHeight: 0,
+ display: "flex",
+ flexDirection: "column",
};
const detailRow: React.CSSProperties = {
- display: "flex", alignItems: "center", justifyContent: "space-between",
- padding: "12px 4px", borderBottom: `1px solid ${INK.borderSoft}`,
+ display: "flex",
+ alignItems: "center",
+ justifyContent: "space-between",
+ padding: "12px 4px",
+ borderBottom: `1px solid ${INK.borderSoft}`,
};
const detailLabel: React.CSSProperties = {
- fontSize: "0.72rem", fontWeight: 600, letterSpacing: "0.06em",
- textTransform: "uppercase", color: INK.muted,
+ fontSize: "0.72rem",
+ fontWeight: 600,
+ letterSpacing: "0.06em",
+ textTransform: "uppercase",
+ color: INK.muted,
};
const detailValue: React.CSSProperties = {
- fontSize: "0.85rem", color: INK.ink, display: "inline-flex", alignItems: "center",
+ fontSize: "0.85rem",
+ color: INK.ink,
+ display: "inline-flex",
+ alignItems: "center",
};
const detailLink: React.CSSProperties = {
- color: INK.ink, textDecoration: "underline",
+ color: INK.ink,
+ textDecoration: "underline",
};
diff --git a/vibn-frontend/app/[workspace]/project/[projectId]/(home)/security/page.tsx b/vibn-frontend/app/[workspace]/project/[projectId]/(home)/security/page.tsx
deleted file mode 100644
index a76f5e7c..00000000
--- a/vibn-frontend/app/[workspace]/project/[projectId]/(home)/security/page.tsx
+++ /dev/null
@@ -1,119 +0,0 @@
-"use client";
-
-import { Shield, Settings } from "lucide-react";
-
-export default function SecurityPage() {
- return (
-
-
-
-
- Security
-
-
- Manage your permissions and security rules.{" "}
-
- Learn more
-
-
-
-
-
-
-
-
-
-
-
-
-
- Check the security of your app
-
-
- Review your configuration, identify potential risks, and learn how to
- strengthen your app's protection
-
-
- Check Security
-
-
-
- );
-}
diff --git a/vibn-frontend/app/[workspace]/project/[projectId]/(home)/services/page.tsx b/vibn-frontend/app/[workspace]/project/[projectId]/(home)/services/page.tsx
index ab25c71d..e67e2c25 100644
--- a/vibn-frontend/app/[workspace]/project/[projectId]/(home)/services/page.tsx
+++ b/vibn-frontend/app/[workspace]/project/[projectId]/(home)/services/page.tsx
@@ -440,15 +440,6 @@ function formatRelative(iso: string | undefined) {
// Sub-components
// ──────────────────────────────────────────────────
-function SectionHeader({ title, count }: { title: string; count: number }) {
- return (
-
- {title}
- {count}
-
- );
-}
-
function EmptySection({
icon,
title,
@@ -505,13 +496,13 @@ function EmptySection({
// ──────────────────────────────────────────────────
const INK = {
- ink: "#1a1a1a",
- mid: "#5f5e5a",
- muted: "#a09a90",
- border: "#e8e4dc",
- borderSoft: "#efebe1",
+ ink: "#111827",
+ mid: "#4b5563",
+ muted: "#9ca3af",
+ border: "#e5e7eb",
+ borderSoft: "#f3f4f6",
cardBg: "#fff",
- fontSans: '"Outfit", "Inter", ui-sans-serif, sans-serif',
+ fontSans: '"Inter", ui-sans-serif, system-ui, -apple-system, sans-serif',
} as const;
const GREEN = "#10b981";
const AMBER = "#f59e0b";
diff --git a/vibn-frontend/app/[workspace]/project/[projectId]/(home)/settings/app/page.tsx b/vibn-frontend/app/[workspace]/project/[projectId]/(home)/settings/app/page.tsx
index 438e8be3..4b5adcb8 100644
--- a/vibn-frontend/app/[workspace]/project/[projectId]/(home)/settings/app/page.tsx
+++ b/vibn-frontend/app/[workspace]/project/[projectId]/(home)/settings/app/page.tsx
@@ -1,99 +1,150 @@
"use client";
+import { useEffect, useState } from "react";
+import { useParams } from "next/navigation";
+import { Save, Loader2 } from "lucide-react";
+import { toast } from "sonner";
+import {
+ THEME,
+ PageHeader,
+ Card,
+ SettingCard,
+ SectionHeader,
+ PrimaryButton,
+ TextField,
+} from "@/components/project/dashboard-ui";
+
export default function AppSettingsPage() {
+ const params = useParams();
+ const projectId = params.projectId as string;
+
+ const [loading, setLoading] = useState(true);
+ const [saving, setSaving] = useState(false);
+ const [error, setError] = useState(null);
+ const [name, setName] = useState("");
+ const [description, setDescription] = useState("");
+
+ // ── Load real project metadata ──
+ useEffect(() => {
+ let cancelled = false;
+ (async () => {
+ try {
+ const r = await fetch(`/api/projects/${projectId}`, {
+ credentials: "include",
+ });
+ const d = await r.json();
+ if (!r.ok) throw new Error(d.error || `HTTP ${r.status}`);
+ if (cancelled) return;
+ setName(d.project?.name ?? "");
+ setDescription(d.project?.description ?? "");
+ } catch (e) {
+ if (!cancelled) setError(e instanceof Error ? e.message : String(e));
+ } finally {
+ if (!cancelled) setLoading(false);
+ }
+ })();
+ return () => {
+ cancelled = true;
+ };
+ }, [projectId]);
+
+ // ── Persist via PATCH ──
+ async function save() {
+ setSaving(true);
+ try {
+ const r = await fetch(`/api/projects/${projectId}`, {
+ method: "PATCH",
+ headers: { "Content-Type": "application/json" },
+ credentials: "include",
+ body: JSON.stringify({ name, description }),
+ });
+ const d = await r.json();
+ if (!r.ok) throw new Error(d.error || `HTTP ${r.status}`);
+ toast.success("App settings saved");
+ } catch (e) {
+ toast.error(e instanceof Error ? e.message : "Failed to save");
+ } finally {
+ setSaving(false);
+ }
+ }
+
return (
-
-
- App Settings
-
-
- General configuration for your application.
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+ {loading ? (
+
- Delete Application
-
-
- Permanently delete this app and all of its data.
-
-
-
- Delete App
-
+ Loading…
+
+ ) : error ? (
+
+ {error}
+
+ ) : (
+
+
+
+
+
+ ) : (
+
+ )
+ }
+ >
+ {saving ? "Saving…" : "Save changes"}
+
+
+
+ )}
+
+
+ {/* Danger zone — there is no project-delete endpoint, so we don't fake a
+ button that does nothing. Deletion is handled through the AI chat. */}
+
+
+
diff --git a/vibn-frontend/app/[workspace]/project/[projectId]/(home)/users/page.tsx b/vibn-frontend/app/[workspace]/project/[projectId]/(home)/users/page.tsx
index 3535ef5b..0f961078 100644
--- a/vibn-frontend/app/[workspace]/project/[projectId]/(home)/users/page.tsx
+++ b/vibn-frontend/app/[workspace]/project/[projectId]/(home)/users/page.tsx
@@ -1,246 +1,33 @@
"use client";
-import { Search, ChevronDown, ListFilter } from "lucide-react";
+import { Users } from "lucide-react";
+import {
+ THEME,
+ PageHeader,
+ EmptyState,
+} from "@/components/project/dashboard-ui";
export default function UsersPage() {
return (
-
-
-
- Users
-
-
- Manage the app's users and their roles
-
-
-
-
-
-
-
- Invite User
-
-
-
+
+
-
-
- Users
-
-
- Pending requests
-
-
-
-
-
-
- Users
-
-
-
-
-
-
-
- all roles
-
-
-
-
-
-
-
- Name
-
-
- Role
-
-
- Email
-
-
-
-
-
-
-
- Mark Henderson
-
-
- Owner
-
-
-
- admin
-
-
- markhenderson1977@gmail.com
-
-
-
-
+ }
+ title="User management coming soon"
+ hint="A built-in user directory is in development. In the meantime, you can view your users directly in your database via the Data tab, or in your connected Auth provider's dashboard (e.g. Clerk, NextAuth)."
+ />
);
diff --git a/vibn-frontend/components/project/dashboard-sidebar.tsx b/vibn-frontend/components/project/dashboard-sidebar.tsx
index 2672083b..abac919d 100644
--- a/vibn-frontend/components/project/dashboard-sidebar.tsx
+++ b/vibn-frontend/components/project/dashboard-sidebar.tsx
@@ -8,10 +8,7 @@ import {
LayoutGrid,
ClipboardList,
Database,
- BarChart2,
Globe,
- Plug,
- ShieldCheck,
Code2,
Terminal,
Settings,
@@ -24,6 +21,15 @@ import {
import { useAnatomy } from "@/components/project/use-anatomy";
+type MenuItem = {
+ segment: string;
+ label: string;
+ Icon: React.ElementType;
+ badge?: string;
+ hasChildren?: boolean;
+ children?: { segment: string; label: string }[];
+};
+
export function DashboardSidebar({
workspace,
projectId,
@@ -61,7 +67,7 @@ export function DashboardSidebar({
}
};
- const menuItems = [
+ const menuItems: MenuItem[] = [
{ segment: "overview", label: "Overview", Icon: LayoutGrid },
{ segment: "plan", label: "Plan & Specs", Icon: ClipboardList },
{ segment: "code", label: "Code", Icon: Code2 },
@@ -78,27 +84,8 @@ export function DashboardSidebar({
{ 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",
@@ -125,7 +112,7 @@ export function DashboardSidebar({
{
@@ -248,8 +235,8 @@ export function DashboardSidebar({
{item.badge && (