feat(m0): bootstrap repo, design system, compose stack
- Repo scaffolding: .gitignore, .env.example, Makefile, docker-compose.yml, README.md, CHANGELOG.md, pre-commit config. - Three-service stack: api (Flask 3), db (postgres:16-alpine), front (nginx serving the Vite bundle). Named volumes metamorph_db + metamorph_evidence. - Backend skeleton: Flask app factory, JSON structured logging on stdout, GET /api/v1/health, multi-stage Dockerfile, pyproject.toml driven by uv, Pydantic Settings with secret guard rails (refuses to boot in non-dev with placeholders), APP_ENV gating. - Frontend skeleton: Vite + React 18 + TypeScript strict + TailwindCSS, RTOps design tokens from tasks/design.md, self-hosted JetBrains Mono / IBM Plex Sans via @fontsource, base UI primitives (Card/Tag/SectionHeader/FlowNode/ Button), home page wired to /api/v1/health. - Engine-agnostic Makefile: auto-detects docker or podman, picks the matching compose driver. Targets: up/down/build/rebuild/dev/lint/fmt/test/migrate/ seed-mitre/print-install-token/e2e/inspect-health. - Playwright suite: e2e/tests/m0-smoke.spec.ts (8 tests) + HTML + JUnit reports + traces on retry. - Docs: tasks/spec.md (finalized after Q&A), tasks/design.md, tasks/todo.md (14 milestones), tasks/testing-m0.md, tasks/lessons.md. DoD: make up + make health + make e2e all pass on podman 5.x (Fedora) and docker. TLS terminated by external reverse proxy (spec §6 NF-network). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
185
frontend/src/pages/HomePage.tsx
Normal file
185
frontend/src/pages/HomePage.tsx
Normal file
@@ -0,0 +1,185 @@
|
||||
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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user