diff --git a/apps/operator/pages/audit.vue b/apps/operator/pages/audit.vue index b74663e..0d2ebd5 100644 --- a/apps/operator/pages/audit.vue +++ b/apps/operator/pages/audit.vue @@ -71,6 +71,56 @@ function shortIp(ip?: string) { // Strip v4-in-v6 prefix that node sockets sometimes report (::ffff:1.2.3.4 → 1.2.3.4) return ip.replace(/^::ffff:/, '') } + +// ── Tamper-evidence (Phase 3) ────────────────────────────────────────── +interface VerifyReport { + ok: boolean + totalEventsVerified: number + checkpointsChecked: number + latestCheckpointAt: string | null + latestVerifiedSeq: number | null + break?: + | { kind: 'event-hash-mismatch'; seq: number; expected: string; actual: string } + | { kind: 'event-prev-hash-mismatch'; seq: number; expected: string; actual: string } + | { kind: 'checkpoint-signature-mismatch'; headSeq: number } +} +interface CheckpointSummary { + at: string | null + headSeq: number | null + headHash: string | null + reason?: string +} + +const { data: latestCp, refresh: refreshCp } = useLazyFetch( + '/api/audit/checkpoint/latest', + { default: () => ({ at: null, headSeq: null, headHash: null }), server: false }, +) +const verifyReport = ref(null) +const verifying = ref(false) + +async function runVerify() { + verifying.value = true + try { + verifyReport.value = await $fetch('/api/audit/verify') + await refreshCp() + } finally { + verifying.value = false + } +} + +async function forceCheckpoint() { + await $fetch('/api/audit/checkpoint', { method: 'POST' }) + await refreshCp() +} + +function fmtRelative(iso: string | null | undefined): string { + if (!iso) return 'never' + const ms = Date.now() - new Date(iso).getTime() + if (ms < 60_000) return `${Math.floor(ms / 1000)}s ago` + if (ms < 3_600_000) return `${Math.floor(ms / 60_000)}m ago` + if (ms < 86_400_000) return `${Math.floor(ms / 3_600_000)}h ago` + return new Date(iso).toLocaleDateString('da-DK') +}