From 8fdedee25b382267f7d1a49a88acf49b296d42f4 Mon Sep 17 00:00:00 2001 From: Logan Cusano Date: Wed, 19 Aug 2026 22:57:21 -0400 Subject: [PATCH] Frontend redesign chunk 3: type layer honesty and the duplicate fix Declare the fields the backend already writes and the UI was discarding: CallRecord gains location_coords, units, vehicles, cleared_units, duplicate_of, srcaddr (intelligence.py ~315-327); IncidentRecord gains units_active, units_cleared, location_mentions, last_thin_at (incident_correlator.py _attach, ~1270-1300). Filter duplicate_of client-side in useCalls.ts's three hooks (useCalls, useCallsByIncident, useActiveCalls) so a call flagged as a second node's recording of the same transmission no longer renders twice. Client-side rather than a where() clause to avoid a new composite index. Removes the two now-resolved DEFERRED.md entries (dedup/useCalls, lib/types.ts field gaps). Per UI_REDESIGN.md chunk 3. --- drb-frontend/lib/types.ts | 19 ++++++++++++++++++ drb-frontend/lib/useCalls.ts | 37 ++++++++++++++++++++++++------------ 2 files changed, 44 insertions(+), 12 deletions(-) diff --git a/drb-frontend/lib/types.ts b/drb-frontend/lib/types.ts index 0eb32a6..b352295 100644 --- a/drb-frontend/lib/types.ts +++ b/drb-frontend/lib/types.ts @@ -109,6 +109,17 @@ export interface CallRecord { /** Legacy field — present on calls recorded before the multi-scene migration. */ incident_id?: string | null; location: string | null; + /** Per-call geocode, written by intelligence.py. Powers the incident-path polyline on the map. */ + location_coords?: { lat: number; lng: number } | null; + /** Unit callsigns mentioned in this specific transmission (e.g. "15-9", "E-41"). */ + units?: string[]; + vehicles?: string[]; + /** Units this call reported as clearing/back in service. */ + cleared_units?: string[]; + /** Set when dedup.py identifies this as a second node's recording of the same transmission — the canonical copy has this null. */ + duplicate_of?: string | null; + /** Radio source address, when the system exposes it. */ + srcaddr?: string | null; tags: string[]; status: "active" | "ended"; /** Four-level ladder: routine | minor | moderate | major. Legacy docs may still carry "unknown". */ @@ -134,6 +145,14 @@ export interface IncidentRecord { talkgroup_ids: string[]; units: string[]; vehicles: string[]; + /** Units currently believed on scene — maintained by incident_correlator.py `_attach`. */ + units_active?: string[]; + /** Units that reported clearing/back in service on this incident. */ + units_cleared?: string[]; + /** Free-text location mentions accumulated across the incident's calls, beyond the primary `location`. */ + location_mentions?: string[]; + /** ISO timestamp of the last thin/status-only call attached (doesn't refresh `updated_at`). */ + last_thin_at?: string | null; severity: string | null; started_at: string; updated_at: string; diff --git a/drb-frontend/lib/useCalls.ts b/drb-frontend/lib/useCalls.ts index 6af6aa1..42b355d 100644 --- a/drb-frontend/lib/useCalls.ts +++ b/drb-frontend/lib/useCalls.ts @@ -52,10 +52,17 @@ export function useCalls(limitCount = 50, dateFrom?: Date, dateTo?: Date) { const toISO = (v: any): string | null => v?.toDate?.()?.toISOString?.() ?? (typeof v === "string" ? v : null); unsubFirestore = onSnapshot(q, (snap) => { - setCalls(snap.docs.map((d) => { - const data = d.data(); - return { ...data, started_at: toISO(data.started_at) ?? "", ended_at: toISO(data.ended_at) } as CallRecord; - })); + const docs = snap.docs + .map((d) => { + const data = d.data(); + return { ...data, started_at: toISO(data.started_at) ?? "", ended_at: toISO(data.ended_at) } as CallRecord; + }) + // dedup.py flags a second node's recording of the same transmission + // with duplicate_of set to the canonical call_id — filtered client- + // side (not a where() clause) so this doesn't need a new composite + // index alongside the existing org_id/started_at query. + .filter((c) => !c.duplicate_of); + setCalls(docs); setLoading(false); }, (err: FirestoreError) => { console.error("useCalls:", err); setError(err.message); setLoading(false); }); }); @@ -92,10 +99,12 @@ export function useCallsByIncident(incidentId: string | null) { where("incident_ids", "array-contains", incidentId) ); unsubFirestore = onSnapshot(q, (snap) => { - const docs = snap.docs.map((d) => { - const data = d.data(); - return { ...data, started_at: toISO(data.started_at) ?? "", ended_at: toISO(data.ended_at) } as CallRecord; - }); + const docs = snap.docs + .map((d) => { + const data = d.data(); + return { ...data, started_at: toISO(data.started_at) ?? "", ended_at: toISO(data.ended_at) } as CallRecord; + }) + .filter((c) => !c.duplicate_of); docs.sort((a, b) => a.started_at.localeCompare(b.started_at)); setCalls(docs); setLoading(false); @@ -131,10 +140,14 @@ export function useActiveCalls() { const toISO = (v: any): string | null => v?.toDate?.()?.toISOString?.() ?? (typeof v === "string" ? v : null); unsubFirestore = onSnapshot(q, (snap) => { - setCalls(snap.docs.map((d) => { - const data = d.data(); - return { ...data, started_at: toISO(data.started_at) ?? "", ended_at: toISO(data.ended_at) } as CallRecord; - })); + setCalls( + snap.docs + .map((d) => { + const data = d.data(); + return { ...data, started_at: toISO(data.started_at) ?? "", ended_at: toISO(data.ended_at) } as CallRecord; + }) + .filter((c) => !c.duplicate_of) + ); }, (err: FirestoreError) => { console.error("useActiveCalls:", err); }); });