diff --git a/apps/web/app/(dashboard)/dashboard/page.tsx b/apps/web/app/(dashboard)/dashboard/page.tsx new file mode 100644 index 0000000..e29dfc6 --- /dev/null +++ b/apps/web/app/(dashboard)/dashboard/page.tsx @@ -0,0 +1,129 @@ +'use client'; + +import Link from 'next/link'; +import { useEffect, useState } from 'react'; +import { apiFetch } from '@/lib/api'; +import { StatusPill } from '@/components/status-pill'; +import { Button } from '@/components/ui/button'; + +interface ServerRow { + id: string; + name: string; + slug: string; + status: string; + publicUrl: string | null; + createdAt: string; +} + +export default function Overview() { + const [servers, setServers] = useState(null); + const [err, setErr] = useState(null); + + useEffect(() => { + apiFetch<{ servers: ServerRow[] }>('/v1/servers') + .then((r) => setServers(r.servers)) + .catch((e) => setErr((e as Error).message)); + }, []); + + if (err?.includes('401')) { + if (typeof window !== 'undefined') { + window.location.href = '/login'; + } + return null; + } + + const total = servers?.length ?? 0; + const live = servers?.filter((s) => s.status === 'live').length ?? 0; + + return ( +
+
+
+

Overview

+

+ Your MCP servers, calls and recent builds. +

+
+ + + New server + +
+ +
+ + + +
+ +
+
+

Recent servers

+ + View all → + +
+
+ {servers === null && ( +
Loading…
+ )} + {servers && servers.length === 0 && ( +
+

No servers yet.

+

+ Describe the tool you want — we host the server. +

+ + + +
+ )} + {servers && servers.length > 0 && ( + + + + + + + + + + + {servers.slice(0, 5).map((s) => ( + + + + + + + ))} + +
NameSlugStatusURL
+ + {s.name} + + {s.slug} + + + {s.publicUrl ?? '—'} +
+ )} +
+
+
+ ); +} + +function Card({ label, value, sub }: { label: string; value: string; sub: string }) { + return ( +
+
{label}
+
{value}
+
{sub}
+
+ ); +} diff --git a/apps/web/app/(dashboard)/layout.tsx b/apps/web/app/(dashboard)/layout.tsx new file mode 100644 index 0000000..72fdc2b --- /dev/null +++ b/apps/web/app/(dashboard)/layout.tsx @@ -0,0 +1,58 @@ +import Link from 'next/link'; +import { Logo } from '@/components/logo'; +import { LayoutGrid, Server, Settings, FileClock } from 'lucide-react'; + +export default function DashboardLayout({ children }: { children: React.ReactNode }) { + return ( +
+
+
+
+ + +
+ + + New server + +
+
+
{children}
+
+ ); +} + +function NavLink({ + href, + children, + icon, +}: { + href: string; + children: React.ReactNode; + icon: React.ReactNode; +}) { + return ( + + {icon} + {children} + + ); +} diff --git a/apps/web/app/(dashboard)/servers/[id]/page.tsx b/apps/web/app/(dashboard)/servers/[id]/page.tsx new file mode 100644 index 0000000..e4f5861 --- /dev/null +++ b/apps/web/app/(dashboard)/servers/[id]/page.tsx @@ -0,0 +1,283 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { useParams } from 'next/navigation'; +import { apiFetch } from '@/lib/api'; +import { StatusPill } from '@/components/status-pill'; +import { CodeBlock } from '@/components/code-block'; +import { InstallSnippets } from '@/components/install-snippets'; +import { StreamingLogs } from '@/components/streaming-logs'; +import { Button } from '@/components/ui/button'; +import { Textarea, Label } from '@/components/input'; +import { cn } from '@/lib/cn'; +import type { ToolSpec } from '@bmm/types'; + +interface ServerDetail { + id: string; + name: string; + slug: string; + status: string; + publicUrl: string | null; + toolsSchema: ToolSpec[] | null; + currentVersion: number; + oauthEnabled: boolean; + createdAt: string; + updatedAt: string; +} + +interface BuildSummary { + id: string; + version: number; + status: string; + startedAt: string | null; + finishedAt: string | null; + errorMessage: string | null; +} + +type Tab = 'overview' | 'tools' | 'logs' | 'metrics' | 'secrets' | 'iterate'; + +export default function ServerDetailPage() { + const params = useParams<{ id: string }>(); + const [server, setServer] = useState(null); + const [builds, setBuilds] = useState([]); + const [tab, setTab] = useState('overview'); + const [iteratePrompt, setIteratePrompt] = useState(''); + const [latestBuildId, setLatestBuildId] = useState(null); + + async function refresh() { + const r = await apiFetch<{ server: ServerDetail; builds: BuildSummary[] }>( + `/v1/servers/${params.id}`, + ); + setServer(r.server); + setBuilds(r.builds); + if (r.builds.length > 0 && !latestBuildId) { + setLatestBuildId(r.builds[0]!.id); + } + } + + useEffect(() => { + refresh(); + const t = setInterval(refresh, 4000); + return () => clearInterval(t); + }, [params.id]); + + async function onIterate() { + if (!server) return; + const res = await apiFetch<{ build: { id: string } }>( + `/v1/servers/${server.id}/iterate`, + { method: 'POST', body: JSON.stringify({ prompt: iteratePrompt, secrets: {} }) }, + ); + setLatestBuildId(res.build.id); + setIteratePrompt(''); + setTab('logs'); + } + + if (!server) { + return ( +
Loading…
+ ); + } + + const tabs: { id: Tab; label: string }[] = [ + { id: 'overview', label: 'Overview' }, + { id: 'tools', label: 'Tools' }, + { id: 'logs', label: 'Logs' }, + { id: 'metrics', label: 'Metrics' }, + { id: 'secrets', label: 'Secrets' }, + { id: 'iterate', label: 'Iterate' }, + ]; + + return ( +
+
+
+
+

{server.name}

+ +
+
+ {server.slug} + · + v{server.currentVersion} + {server.publicUrl && ( + <> + · + + {server.publicUrl}/mcp + + + )} +
+
+
+ +
+ {tabs.map((t) => ( + + ))} +
+ +
+ {tab === 'overview' && ( +
+
+
+ Endpoint +
+
+ {server.publicUrl ? ( + + ) : ( +
Not deployed yet.
+ )} +
+
+ + OAuth 2.1 enforced · Streamable HTTP +
+
+
+
+ Builds +
+
+ {builds.slice(0, 5).map((b) => ( +
+ v{b.version} + + + {b.startedAt ? new Date(b.startedAt).toLocaleString() : '—'} + +
+ ))} +
+
+ {server.publicUrl && ( +
+
+ Install +
+
+ +
+
+ )} +
+ )} + + {tab === 'tools' && ( +
+ {!server.toolsSchema || server.toolsSchema.length === 0 ? ( +
No tools yet.
+ ) : ( +
+ {server.toolsSchema.map((tool) => ( +
+
+ {tool.name} +
+

{tool.description}

+ {Object.keys(tool.inputSchema ?? {}).length > 0 && ( +
+ +
+ )} +
+ ))} +
+ )} +
+ )} + + {tab === 'logs' && ( + latestBuildId ? ( + + ) : ( +
+ No builds to stream yet. +
+ ) + )} + + {tab === 'metrics' && ( +
+
+ + + +
+

+ Live metrics begin streaming after the first tool call. +

+
+ )} + + {tab === 'secrets' && ( +
+ Secrets are AES-256-GCM encrypted at rest. They are injected as environment variables + into your container and are never echoed back. Use the wizard or CLI to rotate them. +
+ )} + + {tab === 'iterate' && ( +
+ +