feat(web): real 3-step wizard, settings, audit, docs, marketing pages
Sprint 3.5: close every dead link and replace the single-step wizard with the spec-mandated 3-step flow. Wizard: - Step 1 collects prompt + name + slug, calls /v1/servers/preview. - Step 2 renders parsed tools (name, description, input schema as copyable JSON) + a credential field per requiredSecret Claude actually identified. Self-contained servers see 'No credentials needed' instead of generic Notion placeholders. - Step 3 streams the live build over WebSocket and shows install snippets. New dashboard pages: - /settings — org, plan/usage, members table, API keys + billing stubs (Sprint 4), encryption status. Reads /v1/me/org. - /audit — filterable table over /v1/audit with action pills, resource refs, IP, metadata JSON. Docs site (/docs + 6 sub-pages): - Sticky 240px sidebar, max-w-prose article column, shared DocsTitle/H2/Code primitives. - Quickstart, MCP concepts, OAuth 2.1 flow (full walkthrough with curl), Authoring tools, Self-hosting, API reference, FAQ. Marketing pages: - /changelog with tagged release timeline. - /security with 8 pillars + disclosure. - /privacy with GDPR-aware sections. - /terms (10 clauses). - /pricing full page (nav now points here instead of /#pricing anchor). - /status with live 10s probes against /api/health and /login. Footer 'system status' badge now links to /status. All 20 routes 200 OK in smoke crawl. Typecheck clean across packages.
This commit is contained in:
139
apps/web/app/(marketing)/status/page.tsx
Normal file
139
apps/web/app/(marketing)/status/page.tsx
Normal file
@@ -0,0 +1,139 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { cn } from '@/lib/cn';
|
||||
|
||||
interface Probe {
|
||||
name: string;
|
||||
url: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const PROBES: Probe[] = [
|
||||
{
|
||||
name: 'Dashboard',
|
||||
url: '/login',
|
||||
description: 'Web frontend (Next.js)',
|
||||
},
|
||||
{
|
||||
name: 'Control plane API',
|
||||
url: '/api/health',
|
||||
description: 'Fastify control plane via /api proxy',
|
||||
},
|
||||
];
|
||||
|
||||
type State = 'pending' | 'ok' | 'down';
|
||||
|
||||
interface Result {
|
||||
state: State;
|
||||
latencyMs: number | null;
|
||||
detail: string | null;
|
||||
}
|
||||
|
||||
export default function Status() {
|
||||
const [results, setResults] = useState<Record<string, Result>>(() =>
|
||||
Object.fromEntries(PROBES.map((p) => [p.name, { state: 'pending', latencyMs: null, detail: null }])),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
async function probe() {
|
||||
const next: Record<string, Result> = {};
|
||||
for (const p of PROBES) {
|
||||
const start = performance.now();
|
||||
try {
|
||||
const res = await fetch(p.url, { cache: 'no-store' });
|
||||
const latency = Math.round(performance.now() - start);
|
||||
next[p.name] = {
|
||||
state: res.ok ? 'ok' : 'down',
|
||||
latencyMs: latency,
|
||||
detail: res.ok ? `HTTP ${res.status}` : `HTTP ${res.status}`,
|
||||
};
|
||||
} catch (e) {
|
||||
next[p.name] = {
|
||||
state: 'down',
|
||||
latencyMs: null,
|
||||
detail: (e as Error).message,
|
||||
};
|
||||
}
|
||||
}
|
||||
if (!cancelled) setResults(next);
|
||||
}
|
||||
probe();
|
||||
const interval = setInterval(probe, 10_000);
|
||||
return () => {
|
||||
cancelled = true;
|
||||
clearInterval(interval);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const anyDown = Object.values(results).some((r) => r.state === 'down');
|
||||
const allPending = Object.values(results).every((r) => r.state === 'pending');
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-3xl px-6 py-16">
|
||||
<header className="mb-10">
|
||||
<div className="text-[11px] uppercase tracking-[0.16em] text-[--color-fg-subtle]">
|
||||
System status
|
||||
</div>
|
||||
<h1 className="mt-2 text-[32px] font-semibold tracking-tight">
|
||||
{allPending ? 'Checking…' : anyDown ? 'Partial outage' : 'All systems operational'}
|
||||
</h1>
|
||||
<p className="mt-3 text-[14px] leading-relaxed text-[--color-fg-muted]">
|
||||
Live health probes against each service. Refreshes every 10 seconds.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div className="panel divide-y divide-[--color-border]">
|
||||
{PROBES.map((p) => {
|
||||
const r = results[p.name]!;
|
||||
return (
|
||||
<div key={p.name} className="flex items-center justify-between px-4 py-3">
|
||||
<div>
|
||||
<div className="text-[13.5px] font-medium">{p.name}</div>
|
||||
<div className="text-[12px] text-[--color-fg-subtle]">{p.description}</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 text-[12px]">
|
||||
{r.latencyMs !== null && (
|
||||
<span className="mono text-[--color-fg-subtle]">{r.latencyMs}ms</span>
|
||||
)}
|
||||
<Dot state={r.state} />
|
||||
<span className={cn(stateClass(r.state), 'font-medium tabular-nums')}>
|
||||
{stateLabel(r.state)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<p className="mt-8 text-[12px] text-[--color-fg-subtle]">
|
||||
Production status board: BetterStack-hosted at status.buildmymcpserver.com (set up
|
||||
post-launch).
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Dot({ state }: { state: State }) {
|
||||
const color =
|
||||
state === 'ok' ? 'bg-emerald-400' : state === 'down' ? 'bg-red-400' : 'bg-zinc-400';
|
||||
return (
|
||||
<span
|
||||
className={cn('size-2 rounded-full', color)}
|
||||
style={state === 'ok' ? { animation: 'pulse-dot 1.6s ease-in-out infinite' } : undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function stateLabel(state: State): string {
|
||||
if (state === 'ok') return 'Operational';
|
||||
if (state === 'down') return 'Down';
|
||||
return 'Checking';
|
||||
}
|
||||
|
||||
function stateClass(state: State): string {
|
||||
if (state === 'ok') return 'text-emerald-300';
|
||||
if (state === 'down') return 'text-red-300';
|
||||
return 'text-[--color-fg-subtle]';
|
||||
}
|
||||
Reference in New Issue
Block a user