buildmymcpserver/apps/web/app/(dashboard)/audit/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

121 lines
4.3 KiB
TypeScript

'use client';
import { useEffect, useState } from 'react';
import { apiFetch } from '@/lib/api';
import { Input } from '@/components/input';
interface AuditEntry {
id: string;
action: string;
resourceType: string | null;
resourceId: string | null;
metadata: Record<string, unknown> | null;
ipAddress: string | null;
userId: string | null;
createdAt: string;
}
const ACTION_FILTERS = [
{ value: '', label: 'All actions' },
{ value: 'auth.login', label: 'Logins' },
{ value: 'auth.logout', label: 'Logouts' },
{ value: 'server.create', label: 'Server created' },
{ value: 'server.iterate', label: 'Server iterated' },
{ value: 'server.delete', label: 'Server deleted' },
];
export default function AuditPage() {
const [entries, setEntries] = useState<AuditEntry[] | null>(null);
const [action, setAction] = useState('');
const [search, setSearch] = useState('');
useEffect(() => {
const q = action ? `?action=${encodeURIComponent(action)}` : '';
apiFetch<{ entries: AuditEntry[] }>(`/v1/audit${q}`).then((r) => setEntries(r.entries));
}, [action]);
const visible = entries?.filter((e) =>
search
? e.action.includes(search) ||
e.resourceId?.includes(search) ||
e.ipAddress?.includes(search)
: true,
);
return (
<div className="mx-auto max-w-7xl px-6 py-8">
<div>
<h1 className="text-[22px] font-semibold tracking-tight">Audit log</h1>
<p className="mt-1 text-[13px] text-[--color-fg-muted]">
Every privileged action in your workspace, with IP and metadata.
</p>
</div>
<div className="mt-6 flex flex-wrap gap-2">
<select
value={action}
onChange={(e) => setAction(e.target.value)}
className="h-8 rounded-md border border-[--color-border] bg-[--color-bg-subtle] px-2 text-[13px] focus:border-[--color-accent] focus:outline-none"
>
{ACTION_FILTERS.map((f) => (
<option key={f.value} value={f.value}>
{f.label}
</option>
))}
</select>
<Input
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Filter by resource id or ip…"
className="w-72"
/>
</div>
<div className="panel mt-4">
{!visible && (
<div className="px-4 py-4 text-[12.5px] text-[--color-fg-muted]">Loading</div>
)}
{visible && visible.length === 0 && (
<div className="px-4 py-12 text-center text-[13px] text-[--color-fg-muted]">
No matching entries.
</div>
)}
{visible && visible.length > 0 && (
<table className="w-full text-[12px]">
<thead className="border-b border-[--color-border] text-[--color-fg-subtle]">
<tr>
<th className="px-4 py-2 text-left font-medium">When</th>
<th className="px-4 py-2 text-left font-medium">Action</th>
<th className="px-4 py-2 text-left font-medium">Resource</th>
<th className="px-4 py-2 text-left font-medium">IP</th>
<th className="px-4 py-2 text-left font-medium">Metadata</th>
</tr>
</thead>
<tbody>
{visible.map((e) => (
<tr key={e.id} className="border-b border-[--color-border] last:border-0">
<td className="px-4 py-2 mono text-[--color-fg-muted]">
{new Date(e.createdAt).toLocaleString()}
</td>
<td className="px-4 py-2">
<span className="mono rounded-full border border-[--color-border] bg-[--color-bg-subtle] px-2 py-0.5 text-[11px]">
{e.action}
</span>
</td>
<td className="px-4 py-2 mono text-[--color-fg-muted]">
{e.resourceType ? `${e.resourceType}/${e.resourceId?.slice(0, 8) ?? '—'}` : '—'}
</td>
<td className="px-4 py-2 mono text-[--color-fg-muted]">{e.ipAddress ?? '—'}</td>
<td className="px-4 py-2 mono text-[10.5px] text-[--color-fg-subtle]">
{e.metadata ? JSON.stringify(e.metadata) : '—'}
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
</div>
);
}