Files
Metamorph/frontend/src/pages/HomePage.tsx

186 lines
6.1 KiB
TypeScript
Raw Normal View History

2026-05-11 06:16:00 +02:00
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 &amp; 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&amp;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>
</>
);
}