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).
This commit is contained in:
Moheeb Zara
2026-05-16 18:13:09 -07:00
parent 2559166092
commit ad477252c8
10 changed files with 417 additions and 0 deletions
@@ -66,6 +66,13 @@ export function IncidentForm({ members }: { members: Member[] }) {
or display it elsewhere.
</p>
<pre className="bg-background border border-border rounded p-3 font-mono text-sm break-all">{token}</pre>
<p className="font-sans text-sm text-muted-foreground">
Check the status any time at{' '}
<Link href={`/track?token=${encodeURIComponent(token)}`} className="text-primary underline">
/track
</Link>{' '}
with this code. Bookmark it now.
</p>
<Link
href="/incidents"
className="inline-block bg-primary text-white font-sans text-sm font-medium px-4 py-2 rounded hover:bg-primary/90 transition"
+26
View File
@@ -0,0 +1,26 @@
import { TrackClient } from './track-client'
export const dynamic = 'force-dynamic'
// Public, unauthenticated. An anonymous incident reporter pastes the tracking
// token they were given at filing time to see their report's status and the
// official updates meant for them. /track is in middleware PUBLIC_ROUTES.
export default async function TrackPage({
searchParams,
}: {
searchParams: Promise<{ token?: string }>
}) {
const { token } = await searchParams
return (
<main className="mx-auto max-w-xl p-4 md:p-10">
<h1 className="text-2xl font-semibold">Track a report</h1>
<p className="mt-2 text-sm text-muted-foreground">
Enter the tracking code you were given when you filed an anonymous report. It is the
only way to look the report up — we cannot recover it for you.
</p>
<div className="mt-6">
<TrackClient initialToken={token ?? ''} autoSubmit={Boolean(token)} />
</div>
</main>
)
}
+128
View File
@@ -0,0 +1,128 @@
'use client'
import { useEffect, useRef, useState } from 'react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Badge } from '@/components/ui/badge'
import { trackIncident } from '@/lib/actions'
import type { PublicIncidentView } from '@/lib/incident-logic'
export function TrackClient({
initialToken,
autoSubmit,
}: {
initialToken: string
autoSubmit: boolean
}) {
const [token, setToken] = useState(initialToken)
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const [view, setView] = useState<PublicIncidentView | null>(null)
const didAuto = useRef(false)
async function lookup(t: string) {
setError(null)
setView(null)
setLoading(true)
const res = await trackIncident({ token: t.trim() })
setLoading(false)
if ('error' in res && res.error) {
setError(res.error)
return
}
setView(res.data)
}
useEffect(() => {
if (autoSubmit && initialToken && !didAuto.current) {
didAuto.current = true
lookup(initialToken)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
return (
<div className="space-y-6">
<form
onSubmit={e => {
e.preventDefault()
if (token.trim()) lookup(token)
}}
className="flex gap-2"
>
<Input
value={token}
onChange={e => setToken(e.target.value)}
placeholder="Tracking code"
aria-label="Tracking code"
className="font-mono"
/>
<Button type="submit" disabled={loading || !token.trim()}>
{loading ? 'Looking up…' : 'Track'}
</Button>
</form>
{error && <p className="text-sm text-destructive">{error}</p>}
{view && (
<div className="space-y-5 rounded-lg border p-5">
<div className="flex flex-wrap items-center gap-2">
<h2 className="text-lg font-semibold">{view.title}</h2>
<Badge variant="secondary">{view.statusLabel}</Badge>
<Badge variant="outline">{view.severity}</Badge>
</div>
<p className="whitespace-pre-wrap text-sm text-muted-foreground">{view.body}</p>
<dl className="grid grid-cols-2 gap-x-4 gap-y-1 text-xs text-muted-foreground">
<dt>Filed</dt>
<dd>{new Date(view.createdAt).toLocaleString()}</dd>
{view.acknowledgedAt && (
<>
<dt>Acknowledged</dt>
<dd>{new Date(view.acknowledgedAt).toLocaleString()}</dd>
</>
)}
{view.decidedAt && (
<>
<dt>Decided</dt>
<dd>{new Date(view.decidedAt).toLocaleString()}</dd>
</>
)}
{view.closedAt && (
<>
<dt>Closed</dt>
<dd>{new Date(view.closedAt).toLocaleString()}</dd>
</>
)}
</dl>
{view.disposition && (
<div className="rounded-md border bg-muted/40 p-3 text-sm">
<p className="mb-1 font-medium">Outcome</p>
<p className="whitespace-pre-wrap">{view.disposition}</p>
</div>
)}
<div>
<p className="mb-2 text-sm font-medium">Updates</p>
{view.updates.length === 0 ? (
<p className="text-sm text-muted-foreground">No updates yet.</p>
) : (
<ul className="space-y-3">
{view.updates.map((u, i) => (
<li key={i} className="rounded-md border p-3 text-sm">
<p className="whitespace-pre-wrap">{u.body}</p>
<p className="mt-1 text-xs text-muted-foreground">
{u.author} · {new Date(u.createdAt).toLocaleString()}
</p>
</li>
))}
</ul>
)}
</div>
</div>
)}
</div>
)
}