From 54adfee69014247bffdde6c2d531b0277c0c35e0 Mon Sep 17 00:00:00 2001 From: Knacky Date: Tue, 12 May 2026 19:19:19 +0200 Subject: [PATCH] =?UTF-8?q?fix(m4):=20typed=20MitreSyncResult=20interface?= =?UTF-8?q?=20=E2=80=94=20drop=20the=20`as`=20cast?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirrors the backend Pydantic `SyncResultOut` in TS so the mutation result is properly typed end-to-end. `(res as { duration_ms: number })` cast removed from MitrePage.tsx; `apiPost` carries the contract. Also annotated the unused query-key factories in mitre.ts so the next reader knows they're parked for M5 template-form consumption (not dead). Co-Authored-By: Claude Opus 4.7 (1M context) --- frontend/src/lib/mitre.ts | 18 ++++++++++++++++++ frontend/src/pages/MitrePage.tsx | 11 ++++++++--- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/frontend/src/lib/mitre.ts b/frontend/src/lib/mitre.ts index 67fa10e..a9d0a1c 100644 --- a/frontend/src/lib/mitre.ts +++ b/frontend/src/lib/mitre.ts @@ -51,6 +51,10 @@ export interface MitreTag { name: string; } +// Query keys. `status` + `matrix` drive the M4 picker; the per-list factories +// (`tactics`/`techniques`/`subtechniques`) are unused today but the M5 +// template forms will consume them for the standalone REST endpoints when +// users edit a single test's tags inline. export const mitreKeys = { status: ['mitre', 'status'] as const, matrix: ['mitre', 'matrix'] as const, @@ -85,3 +89,17 @@ export interface MatrixTactic { export interface MitreMatrix { tactics: MatrixTactic[]; } + +/** Mirror of backend `SyncResultOut` (`api/mitre.py`). */ +export interface MitreSyncResult { + tactics_upserted: number; + techniques_upserted: number; + subtechniques_upserted: number; + subtechniques_skipped_orphan: number; + technique_tactic_links: number; + version: string | null; + source: string; + started_at: string; + finished_at: string; + duration_ms: number; +} diff --git a/frontend/src/pages/MitrePage.tsx b/frontend/src/pages/MitrePage.tsx index a674aad..b37dc55 100644 --- a/frontend/src/pages/MitrePage.tsx +++ b/frontend/src/pages/MitrePage.tsx @@ -9,7 +9,12 @@ 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'; +import { + mitreKeys, + type MitreStatus, + type MitreSyncResult, + type MitreTag, +} from '@/lib/mitre'; export function MitrePage() { const { state } = useAuth(); @@ -24,14 +29,14 @@ export function MitrePage() { }); const sync = useMutation({ - mutationFn: () => apiPost>('/mitre/sync'), + 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}.`); + setSyncResult(`Sync completed in ${(res.duration_ms / 1000).toFixed(1)}s — ${counts}.`); await qc.invalidateQueries({ queryKey: ['mitre'] }); }, onError: (e) => {