import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useState } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { Alert } from '@/components/ui/Alert'; import { Button } from '@/components/ui/Button'; import { Card } from '@/components/ui/Card'; import { SectionHeader } from '@/components/ui/SectionHeader'; import { Tag } from '@/components/ui/Tag'; import { ApiError, apiDelete, apiGet, apiPost } from '@/lib/api'; import { MISSION_STATUS_ACCENT, MISSION_STATUS_LABEL, missionKeys, type Mission, type MissionStatus, type TransitionPayload, } from '@/lib/missions'; const TABS = ['tests', 'members', 'synthesis', 'export'] as const; type Tab = (typeof TABS)[number]; const ALLOWED_TRANSITIONS: Record = { draft: ['in_progress', 'archived'], in_progress: ['completed', 'archived'], completed: ['archived'], archived: [], }; function useMission(id: string) { return useQuery({ queryKey: missionKeys.detail(id), queryFn: () => apiGet(`/missions/${id}`), enabled: !!id, }); } function formatDateRange(start: string | null, end: string | null): string { if (!start && !end) return 'No dates set'; if (start && end) return `${start} → ${end}`; return start ?? end ?? ''; } export function MissionDetailPage() { const params = useParams(); const missionId = params.id ?? ''; const navigate = useNavigate(); const qc = useQueryClient(); const [tab, setTab] = useState('tests'); const detail = useMission(missionId); const transition = useMutation({ mutationFn: (body: TransitionPayload) => apiPost(`/missions/${missionId}/transition`, body), onSuccess: () => { qc.invalidateQueries({ queryKey: missionKeys.detail(missionId) }); qc.invalidateQueries({ queryKey: missionKeys.list() }); }, }); const remove = useMutation({ mutationFn: () => apiDelete<{ ok: true }>(`/missions/${missionId}`), onSuccess: () => { qc.invalidateQueries({ queryKey: missionKeys.list() }); navigate('/missions'); }, }); const apiErr = detail.error instanceof ApiError ? detail.error : null; const m = detail.data; if (apiErr) { return (
{apiErr.message}
); } if (!m) { return

Loading mission…

; } const accent = MISSION_STATUS_ACCENT[m.status]; const allowedNext = ALLOWED_TRANSITIONS[m.status]; return (
{MISSION_STATUS_LABEL[m.status]} {allowedNext.map((target) => ( ))}
Client
{m.client_target ?? '—'}
Dates
{formatDateRange(m.date_start, m.date_end)}
Scenarios
{m.scenarios_count}
Tests
{m.tests_count}
{m.description_md && (
{m.description_md}
)}
{tab === 'tests' && ( {m.scenarios.length === 0 ? (

No scenarios snapshotted yet.

) : (
{m.scenarios.map((sc) => (
#{sc.position + 1}

{sc.snapshot_name}

{sc.snapshot_description && (

{sc.snapshot_description}

)} {sc.tests.map((t) => ( ))}
# Test MITRE OPSEC State
{t.position + 1} {t.snapshot_name}
{t.mitre_tags.map((tag) => ( {tag.external_id} ))}
{t.snapshot_opsec_level} {t.state}
))}
)}
)} {tab === 'members' && ( {m.members.length === 0 ? (

No members assigned. An admin can add them via the API.

) : (
    {m.members.map((mb) => (
  • {mb.user_display_name ?? mb.user_email}

    {mb.user_display_name && (

    {mb.user_email}

    )}
    {mb.role_hint}
  • ))}
)}
)} {tab === 'synthesis' && (

Reveal.js slide synthesis lands in M10.

)} {tab === 'export' && (

JSON / CSV exports land in M11.

)}
); }