buildmymcpserver/apps/web/app/(dashboard)/settings/page.tsx
Marco Sadjadi 09688c1114 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.
2026-05-19 18:20:31 +02:00

179 lines
5.6 KiB
TypeScript

'use client';
import { useEffect, useState } from 'react';
import { apiFetch } from '@/lib/api';
import { Button } from '@/components/ui/button';
interface Org {
id: string;
slug: string;
name: string;
plan: string;
monthlyCallQuota: number;
callsThisPeriod: number;
periodStartsAt: string;
createdAt: string;
}
interface Member {
id: string;
userId: string;
email: string;
name: string | null;
role: string;
createdAt: string;
}
export default function SettingsPage() {
const [org, setOrg] = useState<Org | null>(null);
const [members, setMembers] = useState<Member[]>([]);
const [err, setErr] = useState<string | null>(null);
useEffect(() => {
apiFetch<{ org: Org; members: Member[] }>('/v1/me/org')
.then((r) => {
setOrg(r.org);
setMembers(r.members);
})
.catch((e) => setErr((e as Error).message));
}, []);
if (err?.includes('401')) {
if (typeof window !== 'undefined') window.location.href = '/login';
return null;
}
return (
<div className="mx-auto max-w-5xl px-6 py-8">
<div>
<h1 className="text-[22px] font-semibold tracking-tight">Settings</h1>
<p className="mt-1 text-[13px] text-[--color-fg-muted]">
Organization, plan, members, billing.
</p>
</div>
{!org && <div className="mt-6 text-[12.5px] text-[--color-fg-muted]">Loading</div>}
{org && (
<div className="mt-6 grid gap-4 md:grid-cols-2">
<Card title="Organization">
<Row label="Name" value={org.name} />
<Row label="Slug" value={org.slug} mono />
<Row label="Created" value={new Date(org.createdAt).toLocaleString()} />
<Row label="Organization ID" value={org.id} mono small />
</Card>
<Card title="Plan & usage">
<Row label="Plan" value={org.plan.charAt(0).toUpperCase() + org.plan.slice(1)} />
<Row
label="Calls this period"
value={`${org.callsThisPeriod.toLocaleString()} / ${org.monthlyCallQuota.toLocaleString()}`}
/>
<Row
label="Period started"
value={new Date(org.periodStartsAt).toLocaleString()}
/>
<div className="mt-3">
<Button variant="secondary" size="sm" disabled>
Manage billing (Sprint 4)
</Button>
</div>
</Card>
<Card title="Members" className="md:col-span-2">
<table className="w-full text-[12.5px]">
<thead className="border-b border-[--color-border] text-[--color-fg-subtle]">
<tr>
<th className="py-2 text-left font-medium">Email</th>
<th className="py-2 text-left font-medium">Name</th>
<th className="py-2 text-left font-medium">Role</th>
<th className="py-2 text-left font-medium">Joined</th>
</tr>
</thead>
<tbody>
{members.map((m) => (
<tr key={m.id} className="border-b border-[--color-border] last:border-0">
<td className="py-2.5 mono">{m.email}</td>
<td className="py-2.5 text-[--color-fg-muted]">{m.name ?? '—'}</td>
<td className="py-2.5">
<span className="mono rounded-full border border-[--color-border] bg-[--color-bg-subtle] px-2 py-0.5 text-[11px]">
{m.role}
</span>
</td>
<td className="py-2.5 text-[--color-fg-muted]">
{new Date(m.createdAt).toLocaleDateString()}
</td>
</tr>
))}
</tbody>
</table>
<div className="mt-3">
<Button variant="secondary" size="sm" disabled>
Invite member (Team plan)
</Button>
</div>
</Card>
<Card title="API keys">
<p className="text-[12.5px] text-[--color-fg-muted]">
Programmatic access for the upcoming <span className="mono">bmm</span> CLI.
</p>
<div className="mt-3">
<Button variant="secondary" size="sm" disabled>
Generate key (Sprint 4)
</Button>
</div>
</Card>
<Card title="Encryption">
<Row label="Secret storage" value="AES-256-GCM" mono />
<Row label="Key source" value="env (SECRETS_ENCRYPTION_KEY)" mono />
<p className="mt-3 text-[12px] leading-relaxed text-[--color-fg-subtle]">
Secrets are encrypted before write to Postgres, decrypted only at the moment of
container env injection. Plaintext is never logged.
</p>
</Card>
</div>
)}
</div>
);
}
function Card({
title,
children,
className,
}: {
title: string;
children: React.ReactNode;
className?: string;
}) {
return (
<div className={`panel p-4 ${className ?? ''}`}>
<div className="text-[11px] uppercase tracking-wider text-[--color-fg-subtle]">{title}</div>
<div className="mt-3 space-y-1.5">{children}</div>
</div>
);
}
function Row({
label,
value,
mono,
small,
}: {
label: string;
value: string;
mono?: boolean;
small?: boolean;
}) {
return (
<div className="flex items-baseline justify-between gap-3 text-[12.5px]">
<span className="text-[--color-fg-subtle]">{label}</span>
<span className={`${mono ? 'mono' : ''} ${small ? 'text-[11px]' : ''} text-[--color-fg]`}>
{value}
</span>
</div>
);
}