Vite/React 19 console and enterprise PWA. Officer console screens, offline ledger and sync, data feeds and trip-wire rules screens with freshness chip. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
71 lines
2.2 KiB
TypeScript
71 lines
2.2 KiB
TypeScript
import { useCallback, useEffect, useState } from 'react'
|
|
import type { AlertState, RiskAlert } from './api'
|
|
import { alertsFor } from './demo/ops'
|
|
|
|
/*
|
|
* Officer actions on the alert queue.
|
|
*
|
|
* The queue itself is derived from the portfolio; only the state changes an
|
|
* officer makes (acknowledge / escalate / snooze / close) and their audit trail
|
|
* are persisted, which is exactly the split the deck describes — the model
|
|
* proposes, the officer disposes, and every disposal is auditable.
|
|
*/
|
|
|
|
const KEY = 'ul-alert-states'
|
|
const AUDIT_KEY = 'ul-alert-audit'
|
|
|
|
export interface AuditEntry {
|
|
alertId: string
|
|
from: AlertState
|
|
to: AlertState
|
|
actor: string
|
|
note: string | null
|
|
at: string
|
|
}
|
|
|
|
function readStates(): Record<string, AlertState> {
|
|
try {
|
|
return JSON.parse(localStorage.getItem(KEY) ?? '{}') as Record<string, AlertState>
|
|
} catch {
|
|
return {}
|
|
}
|
|
}
|
|
|
|
export function readAudit(): AuditEntry[] {
|
|
try {
|
|
return JSON.parse(localStorage.getItem(AUDIT_KEY) ?? '[]') as AuditEntry[]
|
|
} catch {
|
|
return []
|
|
}
|
|
}
|
|
|
|
export function setAlertState(alert: RiskAlert, to: AlertState, actor: string, note?: string) {
|
|
const states = readStates()
|
|
const from = states[alert.alertId] ?? alert.state
|
|
states[alert.alertId] = to
|
|
localStorage.setItem(KEY, JSON.stringify(states))
|
|
|
|
const audit: AuditEntry = { alertId: alert.alertId, from, to, actor, note: note ?? null, at: new Date().toISOString() }
|
|
localStorage.setItem(AUDIT_KEY, JSON.stringify([audit, ...readAudit()].slice(0, 200)))
|
|
window.dispatchEvent(new Event('ul-alerts-changed'))
|
|
}
|
|
|
|
/** The alert queue for a block, with any officer actions applied on top. */
|
|
export function useAlerts(block: string) {
|
|
const [version, setVersion] = useState(0)
|
|
|
|
const bump = useCallback(() => setVersion((v) => v + 1), [])
|
|
|
|
useEffect(() => {
|
|
window.addEventListener('ul-alerts-changed', bump)
|
|
return () => window.removeEventListener('ul-alerts-changed', bump)
|
|
}, [bump])
|
|
|
|
const states = readStates()
|
|
// `version` is read so the memo-free derivation re-runs after every action.
|
|
void version
|
|
const alerts = alertsFor(block).map((a) => ({ ...a, state: states[a.alertId] ?? a.state }))
|
|
|
|
return { alerts, audit: readAudit() }
|
|
}
|