From 9092b9e54921650602d98c68716fb35c95d41dcd Mon Sep 17 00:00:00 2001 From: mawkone Date: Sat, 13 Jun 2026 11:13:37 -0700 Subject: [PATCH] fix(dashboard): remove Analytics, Marketing, Security, Integrations + fix build - Deleted unused/stub routes: Security, Analytics, Marketing (+SEO/Social), Integrations - Removed these routes from the Dashboard Sidebar menu - Fixed Next.js build errors caused by duplicate component declarations (SectionHeader, KvRow) in overview, hosting, services, and infrastructure by relying fully on the unified dashboard-ui kit. --- .../[projectId]/(home)/analytics/page.tsx | 75 -- .../project/[projectId]/(home)/api/page.tsx | 164 +-- .../project/[projectId]/(home)/code/page.tsx | 12 +- .../[projectId]/(home)/data/tables/page.tsx | 596 +++++------ .../[projectId]/(home)/domains/page.tsx | 370 +++---- .../[projectId]/(home)/hosting/page.tsx | 21 +- .../(home)/infrastructure/page.tsx | 973 +++++++++++++----- .../[projectId]/(home)/integrations/page.tsx | 223 ---- .../project/[projectId]/(home)/layout.tsx | 2 +- .../project/[projectId]/(home)/logs/page.tsx | 276 +++-- .../[projectId]/(home)/marketing/page.tsx | 6 - .../[projectId]/(home)/marketing/seo/page.tsx | 366 ------- .../(home)/marketing/social/page.tsx | 89 -- .../[projectId]/(home)/overview/page.tsx | 433 ++++---- .../project/[projectId]/(home)/plan/page.tsx | 755 +++++++------- .../[projectId]/(home)/product/page.tsx | 368 +++++-- .../[projectId]/(home)/security/page.tsx | 119 --- .../[projectId]/(home)/services/page.tsx | 21 +- .../[projectId]/(home)/settings/app/page.tsx | 219 ++-- .../project/[projectId]/(home)/users/page.tsx | 253 +---- .../components/project/dashboard-sidebar.tsx | 46 +- 21 files changed, 2460 insertions(+), 2927 deletions(-) delete mode 100644 vibn-frontend/app/[workspace]/project/[projectId]/(home)/analytics/page.tsx delete mode 100644 vibn-frontend/app/[workspace]/project/[projectId]/(home)/integrations/page.tsx delete mode 100644 vibn-frontend/app/[workspace]/project/[projectId]/(home)/marketing/page.tsx delete mode 100644 vibn-frontend/app/[workspace]/project/[projectId]/(home)/marketing/seo/page.tsx delete mode 100644 vibn-frontend/app/[workspace]/project/[projectId]/(home)/marketing/social/page.tsx delete mode 100644 vibn-frontend/app/[workspace]/project/[projectId]/(home)/security/page.tsx 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 ( -
-
-

- Analytics -

-

- Track traffic, usage, and events. -

-
- -
-
- -
-

- No data available yet -

-

- Once your app is live and receiving traffic, your analytics metrics - will appear here. -

-
-
- ); -} diff --git a/vibn-frontend/app/[workspace]/project/[projectId]/(home)/api/page.tsx b/vibn-frontend/app/[workspace]/project/[projectId]/(home)/api/page.tsx index e346e9f0..0c94277d 100644 --- a/vibn-frontend/app/[workspace]/project/[projectId]/(home)/api/page.tsx +++ b/vibn-frontend/app/[workspace]/project/[projectId]/(home)/api/page.tsx @@ -1,157 +1,33 @@ "use client"; -import { Copy, Key } from "lucide-react"; +import { Key } from "lucide-react"; +import { + THEME, + PageHeader, + EmptyState, +} from "@/components/project/dashboard-ui"; export default function ApiPage() { return (
-
-

- API & Webhooks -

-

- Connect external services to your application. -

-
+
+ -
-

- REST API Endpoint -

-
-
- - https://api.steadfast-camp-core-flow.vibn.app/v1 - -
- -
-
- -
-
-

- API Keys -

- -
- -
-
- -
-
-
- Production Key -
-
- Created 2 days ago -
-
-
- pk_live_******************* -
-
+ } + title="API management coming soon" + hint="Built-in API key generation and request routing is in development. If your app already issues its own API keys, you can manage them directly in your database via the Data tab." + />
); diff --git a/vibn-frontend/app/[workspace]/project/[projectId]/(home)/code/page.tsx b/vibn-frontend/app/[workspace]/project/[projectId]/(home)/code/page.tsx index 3084a541..e4c2600e 100644 --- a/vibn-frontend/app/[workspace]/project/[projectId]/(home)/code/page.tsx +++ b/vibn-frontend/app/[workspace]/project/[projectId]/(home)/code/page.tsx @@ -264,13 +264,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 = { diff --git a/vibn-frontend/app/[workspace]/project/[projectId]/(home)/data/tables/page.tsx b/vibn-frontend/app/[workspace]/project/[projectId]/(home)/data/tables/page.tsx index f6c7f7dc..1bf3a98e 100644 --- a/vibn-frontend/app/[workspace]/project/[projectId]/(home)/data/tables/page.tsx +++ b/vibn-frontend/app/[workspace]/project/[projectId]/(home)/data/tables/page.tsx @@ -8,11 +8,16 @@ import { ChevronDown, ChevronRight, Database, - CircleDot, } from "lucide-react"; import { DatabaseTableTree } from "@/components/project/database-table-tree"; import { TableViewer } from "@/components/project/table-viewer"; import { useAnatomy } from "@/components/project/use-anatomy"; +import { + THEME, + PageHeader, + Card, + StatusDot, +} from "@/components/project/dashboard-ui"; type Selection = { kind: "table"; @@ -46,327 +51,286 @@ export default function DataTablesPage() { const showLoading = loading && !anatomy; return ( -
-
- {/* ── Left rail ── */} -
- {showLoading && ( - - Loading… - - )} - {error && !showLoading && ( - - {error} - - )} +
+
+ - {anatomy && ( - - {activeDatabases.length === 0 && ( - - No databases yet. - - Try: "Add a Postgres database to my project" +
+ {/* ── Left rail ── */} +
+ {showLoading && ( + +
+ Loading… +
+
+ )} + {error && !showLoading && ( + +
+ {error} +
+
+ )} + + {anatomy && ( +
+
+

+ Databases +

+ + {activeDatabases.length} - - )} - {activeDatabases.map((db) => { - return ( -
-
- - +
+
+ {activeDatabases.length === 0 && ( +
+ No databases yet. + + Try: "Add a Postgres database to my project" - -
-
{db.name}
-
{db.type}
-
-
-
- - setSelection({ - kind: "table", - dbUuid: db.uuid, - schema, - name, - }) - } - /> -
- - ); - })} - - )} -
+ )} + {activeDatabases.map((db) => { + return ( +
+
+ + + + +
+
+ {db.name} +
+
+ {db.type} +
+
+ +
+
+ + setSelection({ + kind: "table", + dbUuid: db.uuid, + schema, + name, + }) + } + /> +
+
+ ); + })} +
+
+ )} +
- {/* ── Right pane ── */} - + {/* ── Right pane ── */} + +
); } - -// ────────────────────────────────────────────────── -// Bits -// ────────────────────────────────────────────────── - -function RailGroup({ - title, - count, - children, -}: { - title: string; - count: number; - children: React.ReactNode; -}) { - return ( -
-
- {title} - {count} -
-
{children}
-
- ); -} - -function RailEmpty({ children }: { children: React.ReactNode }) { - return
{children}
; -} - -function Inline({ children }: { children: React.ReactNode }) { - return ( -
- {children} -
- ); -} - -function Empty({ children }: { children: React.ReactNode }) { - return ( -
- {children} -
- ); -} - -function paneHeading(s: Selection): string { - if (!s) return "Preview"; - if (s.kind === "table") - return `Preview · ${s.schema === "public" ? s.name : `${s.schema}.${s.name}`}`; - return "Preview"; -} - -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"; - return "#a09a90"; -} - -// ────────────────────────────────────────────────── -// Tokens -// ────────────────────────────────────────────────── - -const INK = { - ink: "#1a1a1a", - mid: "#5f5e5a", - muted: "#a09a90", - border: "#e8e4dc", - borderSoft: "#efebe1", - cardBg: "#fff", - fontSans: '"Outfit", "Inter", ui-sans-serif, sans-serif', -} as const; - -const pageWrap: React.CSSProperties = { - padding: "28px 48px 48px", - fontFamily: INK.fontSans, - color: INK.ink, -}; -const grid: React.CSSProperties = { - display: "grid", - gridTemplateColumns: "minmax(280px, 360px) minmax(0, 1fr)", - gap: 28, - maxWidth: 1400, - margin: "0 auto", - alignItems: "stretch", -}; -const leftCol: React.CSSProperties = { - minWidth: 0, - display: "flex", - flexDirection: "column", - gap: 18, -}; -const rightCol: React.CSSProperties = { - 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", -}; -const railGroup: React.CSSProperties = { - display: "flex", - flexDirection: "column", -}; -const railGroupHeader: React.CSSProperties = { - 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, -}; -const countPill: React.CSSProperties = { - 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 railEmpty: React.CSSProperties = { - 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", -}; -const codebaseTile: React.CSSProperties = { - 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", - font: "inherit", - color: "inherit", -}; -const tileLabel: React.CSSProperties = { - fontSize: "0.85rem", - fontWeight: 600, - color: INK.ink, - marginBottom: 2, -}; -const tileHint: React.CSSProperties = { - fontSize: "0.74rem", - color: INK.mid, - lineHeight: 1.4, - textTransform: "capitalize", -}; -const tileBody: React.CSSProperties = { - padding: "8px 10px 12px", - borderTop: `1px solid ${INK.borderSoft}`, -}; -const chevronCell: React.CSSProperties = { - 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", -}; diff --git a/vibn-frontend/app/[workspace]/project/[projectId]/(home)/domains/page.tsx b/vibn-frontend/app/[workspace]/project/[projectId]/(home)/domains/page.tsx index f3fd1dd0..b724f996 100644 --- a/vibn-frontend/app/[workspace]/project/[projectId]/(home)/domains/page.tsx +++ b/vibn-frontend/app/[workspace]/project/[projectId]/(home)/domains/page.tsx @@ -1,227 +1,189 @@ "use client"; -import { Copy } from "lucide-react"; +import { useParams } from "next/navigation"; +import { useState } from "react"; +import { Globe, ExternalLink, Copy, Check, Loader2 } from "lucide-react"; +import { useAnatomy, type Anatomy } from "@/components/project/use-anatomy"; +import { + THEME, + PageHeader, + Card, + EmptyState, +} from "@/components/project/dashboard-ui"; + +type LiveApp = Anatomy["hosting"]["live"][number]; + +// All public URLs for an app: its fqdn(s) (Coolify can store a comma-joined +// list) plus any attached custom domains, de-duplicated. +function urlsFor(app: LiveApp): string[] { + const out = new Set(); + (app.fqdn ?? "") + .split(",") + .map((s) => s.trim()) + .filter(Boolean) + .forEach((u) => out.add(u)); + (app.domains ?? []).forEach((d) => + out.add(d.startsWith("http") ? d : `https://${d}`), + ); + return [...out]; +} export default function DomainsPage() { + const params = useParams(); + const projectId = params.projectId as string; + const { anatomy, loading } = useAnatomy(projectId, { pollMs: 8000 }); + const live = anatomy?.hosting.live ?? []; + return (
-
-

- Domains -

-

- Buy, connect and manage your domains.{" "} - - Learn more - -

-
+
+ -
-
-
-

- Built-in URL -

-
- -
-
-
- - steadfast-camp-core-flow - - .vibn.app -
- -
-
- -

- Custom domains -

-
-

- Want to use your domain? -

-

- Custom domains are available on our Builder plan and above. Upgrade to - continue working to this app. -

- -
- -
-
-
+ {loading && !anatomy ? ( +
-

- Email domain -

- - Builder+ - -
-
- no-reply@notifications.vibn.app -
-
- Sender Name: App + Loading…
+
+ ) : live.length === 0 ? ( + } + title="No deployed apps yet" + hint="Once you deploy an app, its URL and any custom domains will appear here." + /> + ) : ( +
+ {live.map((app) => ( + + ))}
- -
+ )}
); } + +function DomainCard({ app }: { app: LiveApp }) { + const urls = urlsFor(app); + return ( + +
+ + {app.name} + + + {app.sourceLabel} + +
+ + {urls.length === 0 ? ( +
+ No domain assigned yet — still deploying. +
+ ) : ( + urls.map((url, i) => ( + + )) + )} +
+ ); +} + +function DomainRow({ url, last }: { url: string; last: boolean }) { + const [copied, setCopied] = useState(false); + const copy = () => { + navigator.clipboard?.writeText(url).then(() => { + setCopied(true); + setTimeout(() => setCopied(false), 1500); + }); + }; + return ( + + ); +} diff --git a/vibn-frontend/app/[workspace]/project/[projectId]/(home)/hosting/page.tsx b/vibn-frontend/app/[workspace]/project/[projectId]/(home)/hosting/page.tsx index 919d6313..a4e2c4d7 100644 --- a/vibn-frontend/app/[workspace]/project/[projectId]/(home)/hosting/page.tsx +++ b/vibn-frontend/app/[workspace]/project/[projectId]/(home)/hosting/page.tsx @@ -427,15 +427,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 +483,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)/infrastructure/page.tsx b/vibn-frontend/app/[workspace]/project/[projectId]/(home)/infrastructure/page.tsx index 3479c770..5773eebc 100644 --- a/vibn-frontend/app/[workspace]/project/[projectId]/(home)/infrastructure/page.tsx +++ b/vibn-frontend/app/[workspace]/project/[projectId]/(home)/infrastructure/page.tsx @@ -3,14 +3,26 @@ import { useEffect, useState } from "react"; import { useParams } from "next/navigation"; import { - Loader2, AlertCircle, Database, KeyRound, CircleDot, - ShieldCheck, Mail, CreditCard, Sparkles, HardDrive, - ExternalLink, Pencil, RotateCw, - ChevronDown, ChevronRight, + Loader2, + AlertCircle, + Database, + KeyRound, + CircleDot, + ShieldCheck, + Mail, + CreditCard, + Sparkles, + HardDrive, + ExternalLink, + Pencil, + RotateCw, + ChevronDown, + ChevronRight, } from "lucide-react"; import { useAnatomy, type Anatomy } from "@/components/project/use-anatomy"; import { DatabaseTableTree } from "@/components/project/database-table-tree"; import { TableViewer } from "@/components/project/table-viewer"; +import { KvRow } from "@/components/project/dashboard-ui"; /** * Infrastructure tab — supporting plumbing the product runs on. @@ -30,21 +42,25 @@ import { TableViewer } from "@/components/project/table-viewer"; * one auto-provisioned, so the tile is always there with status. */ -type ProviderCategory = Anatomy["infrastructure"]["providers"][number]["category"]; +type ProviderCategory = + Anatomy["infrastructure"]["providers"][number]["category"]; type CategoryKey = | "databases" - | "auth" | "email" | "payments" | "llm" + | "auth" + | "email" + | "payments" + | "llm" | "storage" | "secrets"; type Selection = | { kind: "category"; category: CategoryKey } | { kind: "database"; uuid: string } - | { kind: "table"; dbUuid: string; schema: string; name: string } + | { kind: "table"; dbUuid: string; schema: string; name: string } | { kind: "provider"; id: string } | { kind: "storage" } - | { kind: "secrets"; resourceUuid: string } + | { kind: "secrets"; resourceUuid: string } | null; interface CategoryDef { @@ -59,57 +75,114 @@ interface CategoryDef { const INFRA_NUDGE = { databases: 'No database yet. Try: "Add a Postgres database to my project"', - auth: 'No auth provider connected. Try: "Add Google OAuth to my app"', - email: 'No email provider. Try: "Set up email sending with Resend"', - payments: 'No payment provider. Try: "Connect Stripe to my project"', - llm: 'No LLM connected. Try: "Add an OpenAI key to this project"', - secrets: 'No secrets stored yet. Try: "Add my Stripe secret key"', + auth: 'No auth provider connected. Try: "Add Google OAuth to my app"', + email: 'No email provider. Try: "Set up email sending with Resend"', + payments: 'No payment provider. Try: "Connect Stripe to my project"', + llm: 'No LLM connected. Try: "Add an OpenAI key to this project"', + secrets: 'No secrets stored yet. Try: "Add my Stripe secret key"', } as const; const CATEGORIES: CategoryDef[] = [ { - key: "databases", label: "Databases", icon: Database, - blurb: "Postgres, Redis, Mongo, ClickHouse — anything your app reads or writes from.", + key: "databases", + label: "Databases", + icon: Database, + blurb: + "Postgres, Redis, Mongo, ClickHouse — anything your app reads or writes from.", examples: ["PostgreSQL", "Redis", "MongoDB", "ClickHouse", "MySQL"], }, { - key: "auth", label: "Auth", icon: ShieldCheck, - blurb: "Identity, sessions, SSO. Detected from env-var keys (CLERK_*, AUTH0_*, NEXTAUTH_*…).", - examples: ["Clerk", "Auth0", "Supabase Auth", "NextAuth", "WorkOS", "SuperTokens"], - dashboards: { Clerk: "https://dashboard.clerk.com", Auth0: "https://manage.auth0.com", "Supabase Auth": "https://supabase.com/dashboard", WorkOS: "https://dashboard.workos.com" }, + key: "auth", + label: "Auth", + icon: ShieldCheck, + blurb: + "Identity, sessions, SSO. Detected from env-var keys (CLERK_*, AUTH0_*, NEXTAUTH_*…).", + examples: [ + "Clerk", + "Auth0", + "Supabase Auth", + "NextAuth", + "WorkOS", + "SuperTokens", + ], + dashboards: { + Clerk: "https://dashboard.clerk.com", + Auth0: "https://manage.auth0.com", + "Supabase Auth": "https://supabase.com/dashboard", + WorkOS: "https://dashboard.workos.com", + }, }, { - key: "email", label: "Email", icon: Mail, + key: "email", + label: "Email", + icon: Mail, blurb: "Transactional + outbound email. SMS is intentionally not here yet.", examples: ["Resend", "Postmark", "SendGrid", "Mailgun", "AWS SES", "Loops"], - dashboards: { Resend: "https://resend.com/emails", Postmark: "https://account.postmarkapp.com", SendGrid: "https://app.sendgrid.com", Mailgun: "https://app.mailgun.com", "AWS SES": "https://console.aws.amazon.com/ses" }, + dashboards: { + Resend: "https://resend.com/emails", + Postmark: "https://account.postmarkapp.com", + SendGrid: "https://app.sendgrid.com", + Mailgun: "https://app.mailgun.com", + "AWS SES": "https://console.aws.amazon.com/ses", + }, }, { - key: "payments", label: "Payments", icon: CreditCard, - blurb: "Stripe / Paddle / LemonSqueezy. Connect Stripe to enable webhook + checkout setup (coming soon).", + key: "payments", + label: "Payments", + icon: CreditCard, + blurb: + "Stripe / Paddle / LemonSqueezy. Connect Stripe to enable webhook + checkout setup (coming soon).", examples: ["Stripe", "LemonSqueezy", "Paddle"], - dashboards: { Stripe: "https://dashboard.stripe.com", LemonSqueezy: "https://app.lemonsqueezy.com", Paddle: "https://vendors.paddle.com" }, + dashboards: { + Stripe: "https://dashboard.stripe.com", + LemonSqueezy: "https://app.lemonsqueezy.com", + Paddle: "https://vendors.paddle.com", + }, }, { - key: "llm", label: "Models", icon: Sparkles, - blurb: "AI models the project calls. BYOK (Bring Your Own Key) lets you use your own provider keys instead of platform defaults.", - examples: ["OpenAI", "Anthropic", "Google AI", "Mistral", "Cohere", "Groq", "OpenRouter"], - dashboards: { OpenAI: "https://platform.openai.com", Anthropic: "https://console.anthropic.com", "Google AI": "https://aistudio.google.com", Mistral: "https://console.mistral.ai", Groq: "https://console.groq.com", OpenRouter: "https://openrouter.ai" }, + key: "llm", + label: "Models", + icon: Sparkles, + blurb: + "AI models the project calls. BYOK (Bring Your Own Key) lets you use your own provider keys instead of platform defaults.", + examples: [ + "OpenAI", + "Anthropic", + "Google AI", + "Mistral", + "Cohere", + "Groq", + "OpenRouter", + ], + dashboards: { + OpenAI: "https://platform.openai.com", + Anthropic: "https://console.anthropic.com", + "Google AI": "https://aistudio.google.com", + Mistral: "https://console.mistral.ai", + Groq: "https://console.groq.com", + OpenRouter: "https://openrouter.ai", + }, }, { - key: "storage", label: "Storage", icon: HardDrive, - blurb: "Vibn provisions an S3-compatible bucket per workspace. Every project in the workspace shares it.", + key: "storage", + label: "Storage", + icon: HardDrive, + blurb: + "Vibn provisions an S3-compatible bucket per workspace. Every project in the workspace shares it.", examples: ["GCS bucket (S3-compatible)"], }, { - key: "secrets", label: "Secrets", icon: KeyRound, - blurb: "Every env var across every app + service. Values are never read here; rotate via the AI chat.", + key: "secrets", + label: "Secrets", + icon: KeyRound, + blurb: + "Every env var across every app + service. Values are never read here; rotate via the AI chat.", examples: [], }, ]; function categoryDef(key: CategoryKey): CategoryDef { - return CATEGORIES.find(c => c.key === key)!; + return CATEGORIES.find((c) => c.key === key)!; } export default function InfrastructureTab() { @@ -122,9 +195,10 @@ export default function InfrastructureTab() { const [expandedDbs, setExpandedDbs] = useState>(new Set()); const toggleDb = (uuid: string) => { - setExpandedDbs(prev => { + setExpandedDbs((prev) => { const next = new Set(prev); - if (next.has(uuid)) next.delete(uuid); else next.add(uuid); + if (next.has(uuid)) next.delete(uuid); + else next.add(uuid); return next; }); }; @@ -132,7 +206,11 @@ export default function InfrastructureTab() { // Auto-expand the first database tile once anatomy lands so the user // sees tables immediately on a project that has one DB. useEffect(() => { - if (anatomy && anatomy.infrastructure.databases[0] && expandedDbs.size === 0) { + if ( + anatomy && + anatomy.infrastructure.databases[0] && + expandedDbs.size === 0 + ) { setExpandedDbs(new Set([anatomy.infrastructure.databases[0].uuid])); } }, [anatomy, expandedDbs.size]); @@ -145,24 +223,29 @@ export default function InfrastructureTab() { {/* ── Left rail ── */}
{showLoading && ( - Loading… + + Loading… + )} {error && !showLoading && ( - {error} + + {error} + )} - {anatomy && CATEGORIES.map(def => ( - - ))} + {anatomy && + CATEGORIES.map((def) => ( + + ))}
{/* ── Right pane ── */} @@ -170,9 +253,15 @@ export default function InfrastructureTab() {

{paneHeading(selection, anatomy)}

{!anatomy && Loading…} - {anatomy && !selection && } + {anatomy && !selection && ( + + )} {anatomy && selection?.kind === "category" && ( - + )} {anatomy && selection?.kind === "database" && ( @@ -192,7 +281,10 @@ export default function InfrastructureTab() { )} {anatomy && selection?.kind === "secrets" && ( - + )}
@@ -206,8 +298,13 @@ export default function InfrastructureTab() { // ────────────────────────────────────────────────── function CategoryRail({ - def, anatomy, selection, onSelect, - projectId, expandedDbs, onToggleDb, + def, + anatomy, + selection, + onSelect, + projectId, + expandedDbs, + onToggleDb, }: { def: CategoryDef; anatomy: Anatomy; @@ -218,7 +315,8 @@ function CategoryRail({ onToggleDb: (uuid: string) => void; }) { const Icon = def.icon; - const headerActive = selection?.kind === "category" && selection.category === def.key; + const headerActive = + selection?.kind === "category" && selection.category === def.key; // Storage is a single-tile category backed by workspace state if (def.key === "storage") { @@ -227,7 +325,12 @@ function CategoryRail({ const active = selection?.kind === "storage"; return (
- onSelect({ kind: "category", category: def.key })} /> + onSelect({ kind: "category", category: def.key })} + /> {present && (
{s.bucketName ?? "provisioning…"}
- +
)} @@ -255,10 +361,15 @@ function CategoryRail({ const dbs = anatomy.infrastructure.databases; return (
- onSelect({ kind: "category", category: def.key })} /> + onSelect({ kind: "category", category: def.key })} + /> {dbs.length > 0 && (
- {dbs.map(db => { + {dbs.map((db) => { const open = expandedDbs.has(db.uuid); const tileSelected = selection?.kind === "database" && selection.uuid === db.uuid; @@ -280,16 +391,21 @@ function CategoryRail({ aria-pressed={tileSelected} > - {open - ? - : } + {open ? ( + + ) : ( + + )}
{db.name}
{db.type}
- + {open && (
@@ -298,7 +414,12 @@ function CategoryRail({ dbUuid={db.uuid} selectedTable={selectedTable} onSelectTable={({ schema, name }) => - onSelect({ kind: "table", dbUuid: db.uuid, schema, name }) + onSelect({ + kind: "table", + dbUuid: db.uuid, + schema, + name, + }) } />
@@ -314,16 +435,21 @@ function CategoryRail({ const items = itemsForCategory(def.key, anatomy); const count = - def.key === "secrets" - ? anatomy.infrastructure.secrets.total - : items.length; + def.key === "secrets" ? anatomy.infrastructure.secrets.total : items.length; return (
- onSelect({ kind: "category", category: def.key })} /> + onSelect({ kind: "category", category: def.key })} + /> {items.length > 0 && (
- {items.map(item => renderRailItem(def.key, item, selection, onSelect))} + {items.map((item) => + renderRailItem(def.key, item, selection, onSelect), + )}
)}
@@ -331,9 +457,15 @@ function CategoryRail({ } function CategoryHeader({ - def, count, active, onClick, + def, + count, + active, + onClick, }: { - def: CategoryDef; count: number; active: boolean; onClick: () => void; + def: CategoryDef; + count: number; + active: boolean; + onClick: () => void; }) { const Icon = def.icon; return ( @@ -348,7 +480,10 @@ function CategoryHeader({ aria-pressed={active} > - + {def.label} {count} @@ -358,16 +493,16 @@ function CategoryHeader({ function itemsForCategory(key: CategoryKey, a: Anatomy): unknown[] { if (key === "databases") return a.infrastructure.databases; - if (key === "secrets") return a.infrastructure.secrets.byResource; - if (key === "storage") return []; - return a.infrastructure.providers.filter(p => p.category === key); + if (key === "secrets") return a.infrastructure.secrets.byResource; + if (key === "storage") return []; + return a.infrastructure.providers.filter((p) => p.category === key); } function renderRailItem( key: CategoryKey, item: unknown, selection: Selection, - onSelect: (s: Selection) => void + onSelect: (s: Selection) => void, ) { if (key === "databases") { const db = item as Anatomy["infrastructure"]["databases"][number]; @@ -385,18 +520,26 @@ function renderRailItem(
{db.name}
{db.type}
- + ); } if (key === "secrets") { - const r = item as Anatomy["infrastructure"]["secrets"]["byResource"][number]; - const active = selection?.kind === "secrets" && selection.resourceUuid === r.resourceUuid; + const r = + item as Anatomy["infrastructure"]["secrets"]["byResource"][number]; + const active = + selection?.kind === "secrets" && + selection.resourceUuid === r.resourceUuid; return ( ); @@ -435,8 +580,12 @@ function renderRailItem( // ────────────────────────────────────────────────── function Overview({ - anatomy, onJump, -}: { anatomy: Anatomy; onJump: (s: Selection) => void }) { + anatomy, + onJump, +}: { + anatomy: Anatomy; + onJump: (s: Selection) => void; +}) { const dbCount = anatomy.infrastructure.databases.length; const providerCount = anatomy.infrastructure.providers.length; const secretsCount = anatomy.infrastructure.secrets.total; @@ -446,12 +595,26 @@ function Overview({
dbCount && onJump({ kind: "database", uuid: anatomy.infrastructure.databases[0].uuid })} + label="Databases" + value={dbCount} + onClick={() => + dbCount && + onJump({ + kind: "database", + uuid: anatomy.infrastructure.databases[0].uuid, + }) + } /> providerCount && onJump({ kind: "provider", id: anatomy.infrastructure.providers[0].id })} + label="Providers" + value={providerCount} + onClick={() => + providerCount && + onJump({ + kind: "provider", + id: anatomy.infrastructure.providers[0].id, + }) + } /> onJump({ kind: "storage" })} /> secretsCount && onJump({ kind: "secrets", resourceUuid: anatomy.infrastructure.secrets.byResource[0].resourceUuid })} + label="Secrets" + value={secretsCount} + onClick={() => + secretsCount && + onJump({ + kind: "secrets", + resourceUuid: + anatomy.infrastructure.secrets.byResource[0].resourceUuid, + }) + } />
@@ -468,13 +639,23 @@ function Overview({ } function CategoryDetail({ - def, anatomy, onJump, -}: { def: CategoryDef; anatomy: Anatomy; onJump: (s: Selection) => void }) { + def, + anatomy, + onJump, +}: { + def: CategoryDef; + anatomy: Anatomy; + onJump: (s: Selection) => void; +}) { const items = itemsForCategory(def.key, anatomy); const count = - def.key === "secrets" ? anatomy.infrastructure.secrets.total : - def.key === "storage" ? (anatomy.infrastructure.bundledStorage.status === "unprovisioned" ? 0 : 1) : - items.length; + def.key === "secrets" + ? anatomy.infrastructure.secrets.total + : def.key === "storage" + ? anatomy.infrastructure.bundledStorage.status === "unprovisioned" + ? 0 + : 1 + : items.length; // Per-category functional CTAs (no explainer prose). let actionRow: React.ReactNode = null; @@ -486,7 +667,11 @@ function CategoryDetail({ ); } else if (def.key === "storage") { actionRow = ( - ); @@ -507,19 +692,23 @@ function CategoryDetail({
) : def.key === "secrets" ? (
- {anatomy.infrastructure.secrets.byResource.map(r => ( + {anatomy.infrastructure.secrets.byResource.map((r) => (
{r.resourceName} - {r.count} keys · {r.resourceKind} + + {r.count} keys · {r.resourceKind} +
))}
) : def.key === "databases" ? (
- {anatomy.infrastructure.databases.map(db => ( + {anatomy.infrastructure.databases.map((db) => (
{db.name} - {db.type} · {db.status} + + {db.type} · {db.status} +
))}
@@ -527,16 +716,19 @@ function CategoryDetail({
Workspace bucket - {anatomy.infrastructure.bundledStorage.bucketName ?? "—"} + + {anatomy.infrastructure.bundledStorage.bucketName ?? "—"} +
) : (
- {(items as Anatomy["infrastructure"]["providers"]).map(p => ( + {(items as Anatomy["infrastructure"]["providers"]).map((p) => (
{p.vendor} - {p.attachments.length} resource{p.attachments.length === 1 ? "" : "s"} + {p.attachments.length} resource + {p.attachments.length === 1 ? "" : "s"}
))} @@ -548,21 +740,30 @@ function CategoryDetail({ } function DatabaseDetail({ uuid, anatomy }: { uuid: string; anatomy: Anatomy }) { - const db = anatomy.infrastructure.databases.find(d => d.uuid === uuid); + const db = anatomy.infrastructure.databases.find((d) => d.uuid === uuid); if (!db) return This database is no longer in the project.; return (
- - - - {db.publicPort != null && } - {db.internalAddress && } + + + + {db.publicPort != null && ( + + )} + {db.internalAddress && ( + + )}
Connection env
- {db.consumerEnvKey}={""} + + {db.consumerEnvKey}={""} +
@@ -570,7 +771,7 @@ function DatabaseDetail({ uuid, anatomy }: { uuid: string; anatomy: Anatomy }) { } function ProviderDetail({ id, anatomy }: { id: string; anatomy: Anatomy }) { - const p = anatomy.infrastructure.providers.find(x => x.id === id); + const p = anatomy.infrastructure.providers.find((x) => x.id === id); if (!p) return This provider is no longer detected.; const def = categoryDef(p.category); const dashboard = def.dashboards?.[p.vendor]; @@ -581,25 +782,38 @@ function ProviderDetail({ id, anatomy }: { id: string; anatomy: Anatomy }) { return (
- + - + {dashboard && ( - + Open {p.vendor} dashboard )}
Detected here - {p.attachments.map(att => ( + {p.attachments.map((att) => (
- {att.resourceName} {att.resourceKind} + {att.resourceName}{" "} + {att.resourceKind}
- {att.keys.map(k => {k})} + {att.keys.map((k) => ( + + {k} + + ))}
))} @@ -620,13 +834,15 @@ function StorageDetail({ anatomy }: { anatomy: Anatomy }) { {s.bucketName && } {s.region && } - {s.hmacAccessId && } + {s.hmacAccessId && ( + + )}
Connection env
-{`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 ( - -
- -
-
-
-
-
Stripe
-
- Sell products or subscriptions and get paid online. -
-
- -
-
- -

- 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. -

- -
-
-
- ); -} 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) => ( + + ))} +
+ + {/* 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 - -
-
-
-
-
- -
- - - -
- -
-
- -
-

- Run an SEO & GEO scan -

-

- Scan your app for SEO basics and GEO details. Get a prioritized - checklist to fix issues in minutes. -

- -
- -
-
-
-
- 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. -

- -
-
- ); -} 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…" : "Redeploy"} +
- {/* ── Status line ── */} -
+
{statusLabel} {item.lastBuild && ( - + · Last build {item.lastBuild.status}{" "} {formatRelative(item.lastBuild.finishedAt)} )}
- {/* ── Live URL ── */} {primaryUrl ? ( -
- - +
+ + {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 && ( -
+
{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)} + icon={} + > + Edit Objective + )} {editing && ( <> - - + {saving ? "Saving…" : "Save Changes"} + + Cancel )}
-
- {editing ? ( - <> -
- - -
- {dirty && ( - +
+ {editing ? ( + <> +
+
- - {editorView === "write" ? ( -