Files
hackerspace.sh/lib/incident-logic.ts
T
Moheeb Zara ad477252c8 feat(incidents): anonymous report tracking by token + public /track
Gap: fileIncident minted a reporter_token and the filing UI showed it
("the only way to look it up later") but there was no lookup UI.

- lib/incident-logic.ts (pure, tested): publicIncidentView redacts —
  board_only updates, subjects, decision-maker ids dropped; disposition
  only surfaced once decided/closed; status label mapping
- trackIncident: public service-client action (token is a 192-bit
  bearer credential on a UNIQUE column; anon has no RLS path so the
  service client is used AFTER token validation; generic not-found is
  enumeration-safe; board_only filtered at the query AND in logic)
- public /track page + client (also accepts ?token= deep link);
  /track added to middleware PUBLIC_ROUTES
- filing UI now deep-links /track?token=<code>
- trackIncidentSchema; __tests__/incident-tracking.test.ts (6)
- docs: API_REFERENCE + HANDOFF pass-20 (session arc, web-verified
  integration facts, locked forward roadmap, open items incl. the
  still-open production incident-INSERT RLS bug)

343 tests; build verified (Compiled successfully, exit 0).
2026-05-16 18:13:09 -07:00

84 lines
2.5 KiB
TypeScript

// Pure projection logic for the anonymous incident tracking page. No deps so
// it is unit-testable. The single rule that matters here is REDACTION: a
// holder of a reporter token is not authenticated and must never see
// board-only notes, internal decision-maker ids, named subjects, or anything
// beyond their own report's status and the official updates meant for them.
export const INCIDENT_STATUS_LABELS: Record<string, string> = {
received: 'Received',
under_review: 'Under review',
decided: 'Decided',
appealed: 'Appealed',
closed: 'Closed',
}
// Updates a token holder may see. board_only is intentionally excluded.
const VISIBLE_UPDATE_VISIBILITIES = ['reporter_only', 'all_parties'] as const
export interface RawIncident {
title: string
body: string
category: string
severity: string
status: string
disposition: string | null
created_at: string
acknowledged_at: string | null
decided_at: string | null
closed_at: string | null
}
export interface RawUpdate {
body: string
author_name: string | null
visibility: string
created_at: string
}
export interface PublicIncidentView {
title: string
body: string
category: string
severity: string
status: string
statusLabel: string
createdAt: string
acknowledgedAt: string | null
decidedAt: string | null
closedAt: string | null
// Only surfaced once a decision exists — never leak an in-progress lean.
disposition: string | null
updates: { body: string; author: string; createdAt: string }[]
}
export function visibleUpdate(visibility: string): boolean {
return (VISIBLE_UPDATE_VISIBILITIES as readonly string[]).includes(visibility)
}
export function publicIncidentView(
incident: RawIncident,
updates: RawUpdate[],
): PublicIncidentView {
const decisionReached = incident.status === 'decided' || incident.status === 'closed'
return {
title: incident.title,
body: incident.body,
category: incident.category,
severity: incident.severity,
status: incident.status,
statusLabel: INCIDENT_STATUS_LABELS[incident.status] ?? incident.status,
createdAt: incident.created_at,
acknowledgedAt: incident.acknowledged_at,
decidedAt: incident.decided_at,
closedAt: incident.closed_at,
disposition: decisionReached ? incident.disposition : null,
updates: updates
.filter(u => visibleUpdate(u.visibility))
.map(u => ({
body: u.body,
author: u.author_name ?? 'Space team',
createdAt: u.created_at,
})),
}
}