From 76c0241bd1de8a816081654dcf36938b8f144379 Mon Sep 17 00:00:00 2001 From: mawkone Date: Fri, 12 Jun 2026 16:35:45 -0700 Subject: [PATCH] Save frontend state (layout, sidebar, chat panel, preview refresh fix) before rollback --- .../[projectId]/(home)/agents/page.tsx | 383 ++++++ .../[projectId]/(home)/analytics/page.tsx | 75 ++ .../project/[projectId]/(home)/api/page.tsx | 158 +++ .../[projectId]/(home)/automations/page.tsx | 109 ++ .../project/[projectId]/(home)/data/page.tsx | 10 + .../[projectId]/(home)/data/tables/page.tsx | 372 ++++++ .../[projectId]/(home)/domains/page.tsx | 227 ++++ .../[projectId]/(home)/integrations/page.tsx | 223 ++++ .../project/[projectId]/(home)/logs/page.tsx | 98 ++ .../[projectId]/(home)/marketing/page.tsx | 6 + .../[projectId]/(home)/marketing/seo/page.tsx | 366 ++++++ .../(home)/marketing/social/page.tsx | 89 ++ .../[projectId]/(home)/preview/page.tsx | 8 +- .../[projectId]/(home)/services/page.tsx | 696 ++++++++++ .../[projectId]/(home)/settings/app/page.tsx | 101 ++ .../[projectId]/(home)/settings/auth/page.tsx | 121 ++ .../[projectId]/(home)/storage/page.tsx | 89 ++ .../project/[projectId]/(home)/users/page.tsx | 247 ++++ .../api/projects/[projectId]/anatomy/route.ts | 76 +- .../components/vibn-chat/chat-panel.tsx | 166 ++- vibn-frontend/package-lock.json | 1137 ++++++++++++++++- vibn-frontend/package.json | 4 +- 22 files changed, 4552 insertions(+), 209 deletions(-) create mode 100644 vibn-frontend/app/[workspace]/project/[projectId]/(home)/agents/page.tsx create mode 100644 vibn-frontend/app/[workspace]/project/[projectId]/(home)/analytics/page.tsx create mode 100644 vibn-frontend/app/[workspace]/project/[projectId]/(home)/api/page.tsx create mode 100644 vibn-frontend/app/[workspace]/project/[projectId]/(home)/automations/page.tsx create mode 100644 vibn-frontend/app/[workspace]/project/[projectId]/(home)/data/page.tsx create mode 100644 vibn-frontend/app/[workspace]/project/[projectId]/(home)/data/tables/page.tsx create mode 100644 vibn-frontend/app/[workspace]/project/[projectId]/(home)/domains/page.tsx create mode 100644 vibn-frontend/app/[workspace]/project/[projectId]/(home)/integrations/page.tsx create mode 100644 vibn-frontend/app/[workspace]/project/[projectId]/(home)/logs/page.tsx create mode 100644 vibn-frontend/app/[workspace]/project/[projectId]/(home)/marketing/page.tsx create mode 100644 vibn-frontend/app/[workspace]/project/[projectId]/(home)/marketing/seo/page.tsx create mode 100644 vibn-frontend/app/[workspace]/project/[projectId]/(home)/marketing/social/page.tsx create mode 100644 vibn-frontend/app/[workspace]/project/[projectId]/(home)/services/page.tsx create mode 100644 vibn-frontend/app/[workspace]/project/[projectId]/(home)/settings/app/page.tsx create mode 100644 vibn-frontend/app/[workspace]/project/[projectId]/(home)/settings/auth/page.tsx create mode 100644 vibn-frontend/app/[workspace]/project/[projectId]/(home)/storage/page.tsx create mode 100644 vibn-frontend/app/[workspace]/project/[projectId]/(home)/users/page.tsx diff --git a/vibn-frontend/app/[workspace]/project/[projectId]/(home)/agents/page.tsx b/vibn-frontend/app/[workspace]/project/[projectId]/(home)/agents/page.tsx new file mode 100644 index 00000000..58997ea7 --- /dev/null +++ b/vibn-frontend/app/[workspace]/project/[projectId]/(home)/agents/page.tsx @@ -0,0 +1,383 @@ +"use client"; + +import { Bot, Plus, FileText, Wrench, Plug } from "lucide-react"; + +export default function AgentsPage() { + return ( +
+
+

+ Agents +

+

+ Manage agents and users +

+
+ +
+
+ +
+

+ Create your first agent +

+

+ Agents talk to your users, work with your data, and run on schedules – + all guided by clear instructions you define in your own words. +

+ +
+
+ + Suggested for your app + + +
+ +
+ {/* Blurred out cards */} +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + {/* Create from scratch */} +
+
+ + Create from scratch + +
+ +
+
+

+ Create your own agent, defining how it works, responds, and + integrates with your data. +

+
+
+
+ +
+

+ How you can use agents +

+
+
+
+ + + Guidelines + +
+

+ Define the agent's behavior, knowledge, and AI model. +

+
+
+
+ + + Tools + +
+

+ Configure what tools and data the agent can access. +

+
+
+
+ + + Connectors + +
+

+ Connect the agent to Gmail, Calendar & more. +

+
+
+
+
+
+ ); +} + +function RefreshIcon() { + return ( + + + + + + + ); +} diff --git a/vibn-frontend/app/[workspace]/project/[projectId]/(home)/analytics/page.tsx b/vibn-frontend/app/[workspace]/project/[projectId]/(home)/analytics/page.tsx new file mode 100644 index 00000000..5cd3740e --- /dev/null +++ b/vibn-frontend/app/[workspace]/project/[projectId]/(home)/analytics/page.tsx @@ -0,0 +1,75 @@ +"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 new file mode 100644 index 00000000..e346e9f0 --- /dev/null +++ b/vibn-frontend/app/[workspace]/project/[projectId]/(home)/api/page.tsx @@ -0,0 +1,158 @@ +"use client"; + +import { Copy, Key } from "lucide-react"; + +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_******************* +
+
+
+
+ ); +} diff --git a/vibn-frontend/app/[workspace]/project/[projectId]/(home)/automations/page.tsx b/vibn-frontend/app/[workspace]/project/[projectId]/(home)/automations/page.tsx new file mode 100644 index 00000000..df38a456 --- /dev/null +++ b/vibn-frontend/app/[workspace]/project/[projectId]/(home)/automations/page.tsx @@ -0,0 +1,109 @@ +"use client"; + +export default function AutomationsPage() { + return ( +
+
+

+ Automations +

+

+ Build and manage automations in your app. +

+
+ +
+
+
+ + Automations + + + Builder+ + +
+
+ +
+

+ Unlock automations +

+

+ To run automations in your app, you need backend functions enabled. + Upgrade to enable backend functions and start using automations. +

+ +
+
+
+ ); +} diff --git a/vibn-frontend/app/[workspace]/project/[projectId]/(home)/data/page.tsx b/vibn-frontend/app/[workspace]/project/[projectId]/(home)/data/page.tsx new file mode 100644 index 00000000..d03e2def --- /dev/null +++ b/vibn-frontend/app/[workspace]/project/[projectId]/(home)/data/page.tsx @@ -0,0 +1,10 @@ +import { redirect } from "next/navigation"; + +export default async function DataPage({ + params, +}: { + params: Promise<{ workspace: string; projectId: string }>; +}) { + const { workspace, projectId } = await params; + redirect(`/${workspace}/project/${projectId}/data/tables`); +} 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 new file mode 100644 index 00000000..f6c7f7dc --- /dev/null +++ b/vibn-frontend/app/[workspace]/project/[projectId]/(home)/data/tables/page.tsx @@ -0,0 +1,372 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useParams, useSearchParams } from "next/navigation"; +import { + Loader2, + AlertCircle, + 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"; + +type Selection = { + kind: "table"; + dbUuid: string; + schema: string; + name: string; +} | null; + +export default function DataTablesPage() { + const params = useParams(); + const searchParams = useSearchParams(); + const projectId = params.projectId as string; + const targetDbId = searchParams.get("db"); + + const { anatomy, loading, error } = useAnatomy(projectId); + + const databases = anatomy?.infrastructure?.databases ?? []; + + // If targetDbId is in the URL, only show that database. + // Otherwise, default to the first database in the list if available. + const activeDbId = + targetDbId || (databases.length > 0 ? databases[0].uuid : null); + const activeDatabases = databases.filter((db) => db.uuid === activeDbId); + + const [selection, setSelection] = useState(null); + + useEffect(() => { + setSelection(null); + }, [projectId, targetDbId]); + + 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" + + + )} + {activeDatabases.map((db) => { + return ( +
+
+ + + + +
+
{db.name}
+
{db.type}
+
+ +
+
+ + setSelection({ + kind: "table", + dbUuid: db.uuid, + schema, + name, + }) + } + /> +
+
+ ); + })} +
+ )} +
+ + {/* ── 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 new file mode 100644 index 00000000..f3fd1dd0 --- /dev/null +++ b/vibn-frontend/app/[workspace]/project/[projectId]/(home)/domains/page.tsx @@ -0,0 +1,227 @@ +"use client"; + +import { Copy } from "lucide-react"; + +export default function DomainsPage() { + 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. +

+ +
+ +
+
+
+
+

+ Email domain +

+ + Builder+ + +
+
+ no-reply@notifications.vibn.app +
+
+ Sender Name: App +
+
+ +
+
+
+ ); +} diff --git a/vibn-frontend/app/[workspace]/project/[projectId]/(home)/integrations/page.tsx b/vibn-frontend/app/[workspace]/project/[projectId]/(home)/integrations/page.tsx new file mode 100644 index 00000000..80f0be1c --- /dev/null +++ b/vibn-frontend/app/[workspace]/project/[projectId]/(home)/integrations/page.tsx @@ -0,0 +1,223 @@ +"use client"; + +import { Diamond } from "lucide-react"; + +export default function IntegrationsPage() { + return ( +
+
+

+ Integrations +

+
+ +
+
+ + +
+ +
+
+
+
+
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)/logs/page.tsx b/vibn-frontend/app/[workspace]/project/[projectId]/(home)/logs/page.tsx new file mode 100644 index 00000000..6b2de0e6 --- /dev/null +++ b/vibn-frontend/app/[workspace]/project/[projectId]/(home)/logs/page.tsx @@ -0,0 +1,98 @@ +"use client"; + +import { Search } from "lucide-react"; + +export default function LogsPage() { + return ( +
+
+

+ Logs +

+

+ View application and server logs. +

+
+ +
+
+
+ + +
+
+
+
+ 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 new file mode 100644 index 00000000..ec0ee46f --- /dev/null +++ b/vibn-frontend/app/[workspace]/project/[projectId]/(home)/marketing/page.tsx @@ -0,0 +1,6 @@ +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 new file mode 100644 index 00000000..28dc940d --- /dev/null +++ b/vibn-frontend/app/[workspace]/project/[projectId]/(home)/marketing/seo/page.tsx @@ -0,0 +1,366 @@ +"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 new file mode 100644 index 00000000..bce3688d --- /dev/null +++ b/vibn-frontend/app/[workspace]/project/[projectId]/(home)/marketing/social/page.tsx @@ -0,0 +1,89 @@ +"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)/preview/page.tsx b/vibn-frontend/app/[workspace]/project/[projectId]/(home)/preview/page.tsx index cebec62b..5fabcffa 100644 --- a/vibn-frontend/app/[workspace]/project/[projectId]/(home)/preview/page.tsx +++ b/vibn-frontend/app/[workspace]/project/[projectId]/(home)/preview/page.tsx @@ -181,9 +181,13 @@ export default function PreviewTab() { const path = currentPath.startsWith("/") ? currentPath : `/${currentPath}`; - setIframeSrc(`${base}${path}`); + // Add the refreshKey as a query param so the iframe completely remounts/refetches + // when the user hits the manual refresh button. + const urlObj = new URL(`${base}${path}`); + urlObj.searchParams.set("_refresh", refreshKey.toString()); + setIframeSrc(urlObj.toString()); } - }, [primaryRunning?.url, currentPath]); + }, [primaryRunning?.url, currentPath, refreshKey]); useEffect(() => { if (!bridge || !iframeSrc || !iframeDomRef.current) return; diff --git a/vibn-frontend/app/[workspace]/project/[projectId]/(home)/services/page.tsx b/vibn-frontend/app/[workspace]/project/[projectId]/(home)/services/page.tsx new file mode 100644 index 00000000..ab25c71d --- /dev/null +++ b/vibn-frontend/app/[workspace]/project/[projectId]/(home)/services/page.tsx @@ -0,0 +1,696 @@ +"use client"; + +import { useState } from "react"; +import { useParams } from "next/navigation"; +import { + Loader2, + AlertCircle, + ExternalLink, + Globe, + RefreshCw, + CircleDot, + ChevronDown, + ChevronRight, + Copy, + Check, + Terminal, + Server, +} from "lucide-react"; +import { useAnatomy, type Anatomy } from "@/components/project/use-anatomy"; + +/** + * Hosting tab — user-facing: "Is my thing live? How do I reach it?" + * + * One endpoint = one card. Each card shows: + * - Live URL (open in new tab) + * - Status dot + plain-language status + * - Redeploy button + * - Domain(s) list + * - Last build (time + status) + * - Expandable recent logs + * + * No master-detail split — with 1-3 services the overhead isn't worth it. + * Previews (dev server URLs) shown below in a secondary section. + */ + +// ────────────────────────────────────────────────── +// Types +// ────────────────────────────────────────────────── + +type LiveItem = Anatomy["hosting"]["live"][number]; +type Preview = Anatomy["hosting"]["previews"][number]; + +// ────────────────────────────────────────────────── +// Main component +// ────────────────────────────────────────────────── + +export default function ServicesPage() { + const params = useParams(); + const projectId = params.projectId as string; + const { anatomy, loading, error } = useAnatomy(projectId, { pollMs: 8000 }); + 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) => ( + + ))} +
+ )} +
+ + {/* ── Previews ── */} + {anatomy.hosting.previews.length > 0 && ( +
+ +
+ {anatomy.hosting.previews.map((p) => ( + + ))} +
+
+ )} + + )} +
+ ); +} + +// ────────────────────────────────────────────────── +// Live card +// ────────────────────────────────────────────────── + +function LiveCard({ item, projectId }: { item: LiveItem; projectId: string }) { + const [deploying, setDeploying] = useState(false); + const [logsOpen, setLogsOpen] = useState(false); + const [logs, setLogs] = useState(null); + const [logsLoading, setLogsLoading] = useState(false); + const [copied, setCopied] = useState(false); + + const primaryUrl = item.fqdn ? `https://${item.fqdn}` : null; + const phase = classifyPhase(item.status); + const { color: statusColor, label: statusLabel } = phaseDisplay(phase, item); + + const redeploy = async () => { + if (deploying) return; + setDeploying(true); + try { + await fetch(`/api/mcp`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + action: "apps.deploy", + params: { uuid: item.uuid, projectId }, + }), + }); + } finally { + setTimeout(() => setDeploying(false), 3000); + } + }; + + const openLogs = async () => { + if (!logsOpen) { + setLogsOpen(true); + setLogsLoading(true); + try { + const r = await fetch(`/api/mcp`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + action: "apps.logs", + params: { uuid: item.uuid, lines: 60 }, + }), + }); + 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."); + } finally { + setLogsLoading(false); + } + } else { + setLogsOpen(false); + } + }; + + const copyUrl = () => { + if (!primaryUrl) return; + navigator.clipboard.writeText(primaryUrl); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + + return ( +
+ {/* ── Card header ── */} +
+
+ + {item.name} + + {item.source === "repo" ? "built" : "image"} + +
+
+ +
+
+ + {/* ── Status line ── */} +
+ + {statusLabel} + + {item.lastBuild && ( + + · Last build {item.lastBuild.status}{" "} + {formatRelative(item.lastBuild.finishedAt)} + + )} +
+ + {/* ── Live URL ── */} + {primaryUrl ? ( +
+ + + {primaryUrl} + + + +
+ ) : ( +
+ + + No domain attached — ask the AI to add one. + +
+ )} + + {/* ── Extra domains ── */} + {item.domains.length > 1 && ( +
+ {item.domains.slice(1).map((d) => ( + + {d}{" "} + + + ))} +
+ )} + + {/* ── Logs toggle ── */} +
+ + + {logsOpen && ( +
+ {logsLoading ? ( + + Loading… + + ) : ( +
{logs || "(no logs)"}
+ )} +
+ )} +
+
+ ); +} + +// ────────────────────────────────────────────────── +// Preview row +// ────────────────────────────────────────────────── + +function PreviewRow({ preview }: { preview: Preview }) { + const running = preview.state === "running"; + return ( +
+
+ + + {preview.name} + + + port {preview.port} + + + {preview.state} + + {preview.url && running && ( + + )} +
+
+ ); +} + +// ────────────────────────────────────────────────── +// Helpers +// ────────────────────────────────────────────────── + +type Phase = "up" | "deploying" | "down" | "unknown"; + +function classifyPhase(status: string | undefined): Phase { + const s = (status ?? "").toLowerCase(); + if (!s || s === "unknown") return "unknown"; + if (/^(running|healthy)/.test(s)) return "up"; + if ( + /^(starting|restarting|created|deploying|building|in_progress|queued|paused)/.test( + s, + ) + ) + return "deploying"; + if (/^(exited|dead|failed|stopped|unhealthy|error)/.test(s)) return "down"; + return "unknown"; +} + +function phaseDisplay( + phase: Phase, + item: LiveItem, +): { color: string; label: string } { + if (item.inFlightBuild) + return { + color: AMBER, + label: `Deploying (${item.inFlightBuild.status ?? "in progress"})`, + }; + switch (phase) { + case "up": + return { color: GREEN, label: "Live" }; + case "deploying": + return { color: AMBER, label: "Starting…" }; + case "down": + return { color: DANGER, label: "Down" }; + default: + return { color: INK.muted, label: "Unknown" }; + } +} + +function formatRelative(iso: string | undefined) { + if (!iso) return ""; + const ms = Date.now() - new Date(iso).getTime(); + if (Number.isNaN(ms)) return ""; + const min = Math.floor(ms / 60_000); + if (min < 1) return "just now"; + if (min < 60) return `${min}m ago`; + const hr = Math.floor(min / 60); + if (hr < 24) return `${hr}h ago`; + return `${Math.floor(hr / 24)}d ago`; +} + +// ────────────────────────────────────────────────── +// Sub-components +// ────────────────────────────────────────────────── + +function SectionHeader({ title, count }: { title: string; count: number }) { + return ( +
+ {title} + {count} +
+ ); +} + +function EmptySection({ + icon, + title, + hint, + promptSuggestion, +}: { + icon: React.ReactNode; + title: string; + hint: string; + promptSuggestion?: string; +}) { + return ( +
+
{icon}
+
+ {title} +
+
+ {hint} +
+ {promptSuggestion && ( +
+ + Try asking: + + + "{promptSuggestion}" + +
+ )} +
+ ); +} + +// ────────────────────────────────────────────────── +// 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 GREEN = "#10b981"; +const AMBER = "#f59e0b"; +const DANGER = "#ef4444"; + +// ────────────────────────────────────────────────── +// Styles +// ────────────────────────────────────────────────── + +const pageWrap: React.CSSProperties = { + padding: "28px 48px 64px", + fontFamily: INK.fontSans, + color: INK.ink, + maxWidth: 860, +}; +const centeredMsg: React.CSSProperties = { + display: "flex", + alignItems: "center", + gap: 10, + padding: "24px 0", +}; +const sectionHeader: React.CSSProperties = { + display: "flex", + alignItems: "center", + gap: 8, + marginBottom: 14, +}; +const sectionTitle: React.CSSProperties = { + fontSize: "0.68rem", + fontWeight: 700, + 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 card: React.CSSProperties = { + background: INK.cardBg, + border: `1px solid ${INK.border}`, + borderRadius: 10, + padding: "18px 20px", +}; +const cardHeader: React.CSSProperties = { + display: "flex", + alignItems: "center", + justifyContent: "space-between", + gap: 12, + marginBottom: 6, +}; +const cardTitle: React.CSSProperties = { + fontSize: "0.95rem", + fontWeight: 700, + color: INK.ink, +}; +const statusLine: React.CSSProperties = { + fontSize: "0.8rem", + color: INK.mid, + marginBottom: 12, + display: "flex", + alignItems: "center", + gap: 6, + flexWrap: "wrap", +}; +const urlRow: React.CSSProperties = { + display: "flex", + alignItems: "center", + gap: 8, + background: "#f8f5f0", + borderRadius: 6, + padding: "8px 12px", + marginBottom: 2, +}; +const urlLink: React.CSSProperties = { + fontSize: "0.85rem", + color: INK.ink, + textDecoration: "none", + flex: 1, + minWidth: 0, + overflow: "hidden", + textOverflow: "ellipsis", + whiteSpace: "nowrap", + display: "inline-flex", + alignItems: "center", + gap: 4, +}; +const actionBtn: React.CSSProperties = { + display: "inline-flex", + alignItems: "center", + gap: 6, + padding: "6px 12px", + border: `1px solid ${INK.border}`, + borderRadius: 6, + background: "#fff", + cursor: "pointer", + font: "inherit", + fontSize: "0.78rem", + fontWeight: 600, + color: INK.mid, + transition: "background 0.1s, border-color 0.1s", +}; +const iconBtn: React.CSSProperties = { + display: "inline-flex", + alignItems: "center", + justifyContent: "center", + width: 26, + height: 26, + border: "none", + background: "transparent", + cursor: "pointer", + color: INK.muted, + borderRadius: 4, + flexShrink: 0, +}; +const logsToggleBtn: React.CSSProperties = { + display: "inline-flex", + alignItems: "center", + gap: 6, + fontSize: "0.75rem", + fontWeight: 600, + color: INK.mid, + background: "none", + border: "none", + cursor: "pointer", + font: "inherit", + padding: 0, +}; +const logsBox: React.CSSProperties = { + marginTop: 10, + background: "#1a1a1a", + borderRadius: 6, + padding: "12px 14px", + maxHeight: 320, + overflowY: "auto", +}; +const logsPre: React.CSSProperties = { + margin: 0, + fontFamily: "ui-monospace, monospace", + fontSize: "0.72rem", + color: "#d4d0c8", + lineHeight: 1.6, + whiteSpace: "pre-wrap", + wordBreak: "break-all", +}; + +const emptyBox: React.CSSProperties = { + border: `1px dashed ${INK.border}`, + borderRadius: 10, + padding: "36px 28px", + textAlign: "center", + display: "flex", + flexDirection: "column", + alignItems: "center", +}; +const promptChip: React.CSSProperties = { + display: "inline-flex", + alignItems: "center", + background: "#f3eee4", + borderRadius: 6, + padding: "6px 12px", + fontSize: "0.8rem", +}; + +function sourcePill(source: "repo" | "image"): React.CSSProperties { + const isRepo = source === "repo"; + return { + fontSize: "0.62rem", + fontWeight: 700, + letterSpacing: "0.08em", + textTransform: "uppercase", + color: isRepo ? "#2e6d2e" : "#3b5a78", + background: isRepo ? "#eaf3e8" : "#e9eff5", + padding: "1px 6px", + borderRadius: 4, + flexShrink: 0, + }; +} 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 new file mode 100644 index 00000000..438e8be3 --- /dev/null +++ b/vibn-frontend/app/[workspace]/project/[projectId]/(home)/settings/app/page.tsx @@ -0,0 +1,101 @@ +"use client"; + +export default function AppSettingsPage() { + return ( +
+
+

+ App Settings +

+

+ General configuration for your application. +

+
+ +
+
+ + +
+
+ +