186 lines
6.1 KiB
TypeScript
186 lines
6.1 KiB
TypeScript
|
|
import { useEffect, useState } from 'react';
|
||
|
|
|
||
|
|
import { Button } from '@/components/ui/Button';
|
||
|
|
import { Card } from '@/components/ui/Card';
|
||
|
|
import { FlowNode } from '@/components/ui/FlowNode';
|
||
|
|
import { SectionHeader } from '@/components/ui/SectionHeader';
|
||
|
|
import { Tag } from '@/components/ui/Tag';
|
||
|
|
import { apiGet } from '@/lib/api';
|
||
|
|
|
||
|
|
interface HealthResponse {
|
||
|
|
status: string;
|
||
|
|
version: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
interface DbDiagResponse {
|
||
|
|
reachable: boolean;
|
||
|
|
alembic_revision?: string | null;
|
||
|
|
table_count?: number;
|
||
|
|
error?: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
type HealthState =
|
||
|
|
| { kind: 'loading' }
|
||
|
|
| { kind: 'ok'; data: HealthResponse }
|
||
|
|
| { kind: 'error'; error: string };
|
||
|
|
|
||
|
|
type DbState =
|
||
|
|
| { kind: 'loading' }
|
||
|
|
| { kind: 'ok'; data: DbDiagResponse }
|
||
|
|
| { kind: 'error'; error: string };
|
||
|
|
|
||
|
|
export function HomePage() {
|
||
|
|
const [health, setHealth] = useState<HealthState>({ kind: 'loading' });
|
||
|
|
const [db, setDb] = useState<DbState>({ kind: 'loading' });
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
let cancelled = false;
|
||
|
|
apiGet<HealthResponse>('/health', { anonymous: true })
|
||
|
|
.then((data) => !cancelled && setHealth({ kind: 'ok', data }))
|
||
|
|
.catch((err: unknown) =>
|
||
|
|
!cancelled &&
|
||
|
|
setHealth({ kind: 'error', error: err instanceof Error ? err.message : String(err) }),
|
||
|
|
);
|
||
|
|
apiGet<DbDiagResponse>('/diag/db', { anonymous: true })
|
||
|
|
.then((data) => !cancelled && setDb({ kind: 'ok', data }))
|
||
|
|
.catch((err: unknown) =>
|
||
|
|
!cancelled &&
|
||
|
|
setDb({ kind: 'error', error: err instanceof Error ? err.message : String(err) }),
|
||
|
|
);
|
||
|
|
return () => {
|
||
|
|
cancelled = true;
|
||
|
|
};
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
return (
|
||
|
|
<>
|
||
|
|
<header className="text-center mb-12">
|
||
|
|
<h1 className="font-mono text-[28px] font-bold tracking-tight text-text-bright">
|
||
|
|
<span className="text-red">Meta</span>
|
||
|
|
<span>morph</span>{' '}
|
||
|
|
<span className="text-purple">Purple Team Platform</span>
|
||
|
|
</h1>
|
||
|
|
<p className="font-mono text-sm font-light text-text-dim mt-2">
|
||
|
|
Collaborative red & blue test orchestration — M3 milestone (RBAC)
|
||
|
|
</p>
|
||
|
|
</header>
|
||
|
|
<SectionHeader
|
||
|
|
prefix="System"
|
||
|
|
highlight="Health"
|
||
|
|
accent="cyan"
|
||
|
|
description="Live status pulled from /api/v1/health and /api/v1/diag/db."
|
||
|
|
/>
|
||
|
|
<div className="grid gap-4 [grid-template-columns:repeat(auto-fill,minmax(420px,1fr))]">
|
||
|
|
<Card
|
||
|
|
accent={health.kind === 'ok' ? 'green' : health.kind === 'error' ? 'red' : 'cyan'}
|
||
|
|
title="API"
|
||
|
|
sub={
|
||
|
|
health.kind === 'loading'
|
||
|
|
? 'probing…'
|
||
|
|
: health.kind === 'error'
|
||
|
|
? 'unreachable'
|
||
|
|
: `version ${health.data.version}`
|
||
|
|
}
|
||
|
|
>
|
||
|
|
{health.kind === 'loading' && <p>Waiting for the backend…</p>}
|
||
|
|
{health.kind === 'ok' && (
|
||
|
|
<p>
|
||
|
|
Status:{' '}
|
||
|
|
<code className="accent-fill-green px-2 py-[2px] rounded-sm font-mono text-4xs">
|
||
|
|
{health.data.status}
|
||
|
|
</code>
|
||
|
|
</p>
|
||
|
|
)}
|
||
|
|
{health.kind === 'error' && (
|
||
|
|
<p>
|
||
|
|
Error:{' '}
|
||
|
|
<code className="accent-fill-red px-2 py-[2px] rounded-sm font-mono text-4xs">
|
||
|
|
{health.error}
|
||
|
|
</code>
|
||
|
|
</p>
|
||
|
|
)}
|
||
|
|
</Card>
|
||
|
|
|
||
|
|
<Card
|
||
|
|
accent={db.kind === 'ok' && db.data.reachable ? 'blue' : db.kind === 'error' ? 'red' : 'cyan'}
|
||
|
|
title="Database"
|
||
|
|
sub={
|
||
|
|
db.kind === 'loading'
|
||
|
|
? 'probing…'
|
||
|
|
: db.kind === 'error'
|
||
|
|
? 'unreachable'
|
||
|
|
: db.data.reachable
|
||
|
|
? `revision ${db.data.alembic_revision?.slice(0, 8) ?? 'unknown'}`
|
||
|
|
: 'unreachable'
|
||
|
|
}
|
||
|
|
>
|
||
|
|
{db.kind === 'ok' && db.data.reachable && (
|
||
|
|
<p>
|
||
|
|
Tables:{' '}
|
||
|
|
<code
|
||
|
|
className="accent-fill-blue px-2 py-[2px] rounded-sm font-mono text-4xs"
|
||
|
|
data-testid="db-table-count"
|
||
|
|
>
|
||
|
|
{db.data.table_count}
|
||
|
|
</code>{' '}
|
||
|
|
· Alembic head reached.
|
||
|
|
</p>
|
||
|
|
)}
|
||
|
|
{(db.kind === 'loading' || (db.kind === 'ok' && !db.data.reachable)) && (
|
||
|
|
<p>Querying schema metadata…</p>
|
||
|
|
)}
|
||
|
|
{db.kind === 'error' && (
|
||
|
|
<p>
|
||
|
|
Error:{' '}
|
||
|
|
<code className="accent-fill-red px-2 py-[2px] rounded-sm font-mono text-4xs">
|
||
|
|
{db.error}
|
||
|
|
</code>
|
||
|
|
</p>
|
||
|
|
)}
|
||
|
|
</Card>
|
||
|
|
|
||
|
|
<Card accent="purple" title="Roadmap" sub="14 milestones">
|
||
|
|
<p>
|
||
|
|
M0 + M1 + M2 + M3 done. Next:{' '}
|
||
|
|
<code className="accent-fill-cyan px-2 py-[2px] rounded-sm font-mono text-4xs">
|
||
|
|
M4 — MITRE ATT&CK
|
||
|
|
</code>
|
||
|
|
.
|
||
|
|
</p>
|
||
|
|
</Card>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<SectionHeader
|
||
|
|
prefix="Design"
|
||
|
|
highlight="Tokens"
|
||
|
|
accent="orange"
|
||
|
|
description="Sanity check of the RTOps design system primitives (cf. tasks/design.md)."
|
||
|
|
/>
|
||
|
|
<div className="mb-6">
|
||
|
|
<Tag accent="red">EVASION</Tag>
|
||
|
|
<Tag accent="purple">C2</Tag>
|
||
|
|
<Tag accent="cyan">LATERAL</Tag>
|
||
|
|
<Tag accent="orange">CRED</Tag>
|
||
|
|
<Tag accent="green">PHISH</Tag>
|
||
|
|
<Tag accent="teal">PERSIST</Tag>
|
||
|
|
</div>
|
||
|
|
<div className="flex items-center gap-1 flex-wrap py-3 mb-6">
|
||
|
|
<FlowNode accent="orange">recon</FlowNode>
|
||
|
|
<span className="text-text-dim font-mono text-2xs">→</span>
|
||
|
|
<FlowNode accent="green">phish</FlowNode>
|
||
|
|
<span className="text-text-dim font-mono text-2xs">→</span>
|
||
|
|
<FlowNode accent="purple">c2</FlowNode>
|
||
|
|
<span className="text-text-dim font-mono text-2xs">→</span>
|
||
|
|
<FlowNode accent="cyan">lateral</FlowNode>
|
||
|
|
<span className="text-text-dim font-mono text-2xs">→</span>
|
||
|
|
<FlowNode accent="red">impact</FlowNode>
|
||
|
|
</div>
|
||
|
|
<div className="flex gap-2">
|
||
|
|
<Button accent="cyan">Primary</Button>
|
||
|
|
<Button accent="red">Danger</Button>
|
||
|
|
<Button variant="ghost">Cancel</Button>
|
||
|
|
</div>
|
||
|
|
</>
|
||
|
|
);
|
||
|
|
}
|