import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useState } from 'react'; import { MitreTagPicker } from '@/components/MitreTagPicker'; 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, apiGet, apiPost } from '@/lib/api'; import { useAuth } from '@/lib/auth'; import { mitreKeys, type MitreStatus, type MitreTag } from '@/lib/mitre'; export function MitrePage() { const { state } = useAuth(); const qc = useQueryClient(); const [selected, setSelected] = useState([]); const [syncResult, setSyncResult] = useState(null); const [syncError, setSyncError] = useState(null); const status = useQuery({ queryKey: mitreKeys.status, queryFn: () => apiGet('/mitre/status'), }); const sync = useMutation({ mutationFn: () => apiPost>('/mitre/sync'), onMutate: () => { setSyncResult(null); setSyncError(null); }, onSuccess: async (res) => { const counts = `${res.tactics_upserted} tactics, ${res.techniques_upserted} techniques, ${res.subtechniques_upserted} subtechniques`; setSyncResult(`Sync completed in ${(res as { duration_ms: number }).duration_ms / 1000}s — ${counts}.`); await qc.invalidateQueries({ queryKey: ['mitre'] }); }, onError: (e) => { if (e instanceof ApiError) { const p = e.payload as { error?: string; message?: string } | null; setSyncError(p?.message ?? p?.error ?? `HTTP ${e.status}`); } else { setSyncError(e instanceof Error ? e.message : 'Sync failed'); } }, }); return ( <>

URL:{' '} {status.data?.source_url ?? status.data?.default_url ?? '—'}

Last sync:{' '} {status.data?.last_sync ?? 'never'}

{state.user?.is_admin && (

The seed CLI runs the same logic; trigger from here only if the URL is reachable from the api container.

{syncResult && {syncResult}} {syncError && {syncError}}
)}
{selected.length > 0 && (
            {JSON.stringify(selected, null, 2)}
          
)} {selected.length === 0 && (

Pick a tactic on the left, then a technique, then optionally a sub-technique. Selections accumulate.

)}
Tactic Technique Sub-technique
); }