From 94898d1af1e4f7cb84183dc7eeb62bf8b6e3da56 Mon Sep 17 00:00:00 2001 From: Logan Cusano Date: Sun, 25 Oct 2026 23:12:07 -0400 Subject: [PATCH] fix(frontend): issues from the 10-25 Chrome sweep MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Incident detail "← Incidents" used router.back(), so arriving from Live or a shared link it went to the map / out of the app. Now a Link. - Resolved incidents showed "0m elapsed" (manual resolve sets no resolved_at, updated_at can predate the last call): end is now the latest of resolved_at, updated_at and the newest call. - Admin: the max-width toggle wrapped the tab bar, so it reflowed on every tab switch and left the cursor over e.g. "Close calls". Body only now. - STT Eval sat on "Loading…" forever when a scanned page had zero matches but a next_cursor — the refill effect didn't depend on cursor. - Trip event blocks used dark-only bg-indigo-950/60: unreadable in light mode. Theme tokens now. - Reassign modal said "connected for the first time" on configured nodes. - Dark-mode placeholders dimmed + italic; they read as filled-in values. Frontend unverified locally (no Node, #207); CI build typechecks. Co-Authored-By: Claude Opus 5.5 --- drb-frontend/app/admin/page.tsx | 11 +++++++-- drb-frontend/app/globals.css | 8 +++++++ drb-frontend/app/incidents/[id]/page.tsx | 25 ++++++++++++--------- drb-frontend/app/trips/[id]/page.tsx | 8 ++++--- drb-frontend/components/NodeConfigModal.tsx | 8 +++++-- drb-frontend/lib/types.ts | 2 ++ 6 files changed, 45 insertions(+), 17 deletions(-) diff --git a/drb-frontend/app/admin/page.tsx b/drb-frontend/app/admin/page.tsx index 05a2bfc..61c0387 100644 --- a/drb-frontend/app/admin/page.tsx +++ b/drb-frontend/app/admin/page.tsx @@ -1169,8 +1169,10 @@ function SttEvalTab() { // scanning", not "done", so this fires again on its own. useEffect(() => { if (queue.length === 0 && !exhausted) loadBatch(); + // `cursor` is a dep so an empty page that advanced the cursor scans the + // next one; without it the tab sat on "Loading…" forever. // eslint-disable-next-line react-hooks/exhaustive-deps - }, [queue.length, exhausted]); + }, [queue.length, exhausted, cursor]); useEffect(() => { setDraft(current ? (current.transcript_corrected || current.transcript || "") : ""); @@ -1309,7 +1311,10 @@ export default function AdminPage() { const wide = tab === "users" || tab === "audit" || tab === "replay" || tab === "cameras" || tab === "outages"; return ( -
+ // The width toggle wraps the tab body only. It used to wrap the tab bar + // too, so the bar reflowed on every switch and left the cursor over + // whatever landed under it (e.g. Calls -> "Close calls"). +

Admin

@@ -1326,6 +1331,7 @@ export default function AdminPage() { ))}
+
{tab === "features" && } {tab === "cameras" && } {tab === "outages" && } @@ -1335,6 +1341,7 @@ export default function AdminPage() { {tab === "eval" && } {tab === "users" && } {tab === "audit" && } +
); } diff --git a/drb-frontend/app/globals.css b/drb-frontend/app/globals.css index 372887b..fda5449 100644 --- a/drb-frontend/app/globals.css +++ b/drb-frontend/app/globals.css @@ -219,6 +219,14 @@ html:not(.dark) .border-indigo-800 { border-color: #a5b4fc !important; } } /* ── Form inputs ─────────────────────────────────────────────────────────── */ +/* Placeholders must read as hints, not values: Tailwind's default #9ca3af on + * the dark surface looked like filled-in text (e.g. /systems Local Area showed + * "Ossining / Westchester" next to "no area set"). Dimmer + italic in dark. */ +.dark input::placeholder, +.dark textarea::placeholder { + color: #5F6B80; + font-style: italic; +} html:not(.dark) input:not([type="submit"]):not([type="button"]):not([type="reset"]), html:not(.dark) select, html:not(.dark) textarea { diff --git a/drb-frontend/app/incidents/[id]/page.tsx b/drb-frontend/app/incidents/[id]/page.tsx index db627d4..47aafba 100644 --- a/drb-frontend/app/incidents/[id]/page.tsx +++ b/drb-frontend/app/incidents/[id]/page.tsx @@ -2,7 +2,8 @@ import dynamic from "next/dynamic"; import { useMemo, useState } from "react"; -import { useParams, useRouter } from "next/navigation"; +import Link from "next/link"; +import { useParams } from "next/navigation"; import { useIncident } from "@/lib/useIncidents"; import { useCallsByIncident } from "@/lib/useCalls"; import { useAuth } from "@/components/AuthProvider"; @@ -21,9 +22,9 @@ const MapView = dynamic(() => import("@/components/MapView"), { ssr: false }); const EARLIER_PAGE_SIZE = 8; -function elapsedLabel(startedAt: string, active: boolean, updatedAt: string): string { +function elapsedLabel(startedAt: string, active: boolean, endedAt: string): string { const start = new Date(startedAt).getTime(); - const end = active ? Date.now() : new Date(updatedAt).getTime(); + const end = active ? Date.now() : new Date(endedAt).getTime(); const mins = Math.max(0, Math.round((end - start) / 60000)); if (mins < 60) return `${mins}m`; const hrs = Math.floor(mins / 60); @@ -33,7 +34,6 @@ function elapsedLabel(startedAt: string, active: boolean, updatedAt: string): st export default function IncidentDetailPage() { const params = useParams(); const id = params.id as string; - const router = useRouter(); const { incident, loading } = useIncident(id); const { calls, loading: callsLoading } = useCallsByIncident(id); @@ -82,18 +82,23 @@ export default function IncidentDetailPage() { const unitsCleared = incident.units_cleared ?? []; const vehicles = incident.vehicles ?? []; const active = incident.status === "active"; + // A manual resolve doesn't set resolved_at and updated_at can predate the + // last call, which showed "0m elapsed" on an 11-call incident — so a + // resolved incident runs to the latest of those and its newest call. + const resolvedEnd = [incident.resolved_at, incident.updated_at, newestFirst[0]?.ended_at, newestFirst[0]?.started_at] + .filter((t): t is string => !!t) + .reduce((a, b) => (b > a ? b : a), incident.started_at); const visible = newestFirst.slice(0, earlierShown); const remaining = newestFirst.length - visible.length; return (
- + {/* Header — glyph, severity chip, status, title at 27px, elapsed/path/count */}
@@ -109,7 +114,7 @@ export default function IncidentDetailPage() { {incident.title ?? "Incident"}

- {elapsedLabel(incident.started_at, active, incident.updated_at)} elapsed + {elapsedLabel(incident.started_at, active, resolvedEnd)} elapsed {path?.movement && path.lengthKm > 0 && <> · {path.lengthKm.toFixed(1)} km path} {path?.mayBeMerged && ( diff --git a/drb-frontend/app/trips/[id]/page.tsx b/drb-frontend/app/trips/[id]/page.tsx index 128bdad..db71d85 100644 --- a/drb-frontend/app/trips/[id]/page.tsx +++ b/drb-frontend/app/trips/[id]/page.tsx @@ -484,15 +484,17 @@ function DayTimeline({ style={{ top, height, left: 60, right: 0 }} className={`absolute rounded-lg px-3 py-2 overflow-hidden group transition-colors z-20 ${ isConflict - ? "bg-red-950/70 border border-red-700/70" - : "bg-indigo-950/60 border border-indigo-800/50 hover:border-indigo-600/70" + // Theme tokens: the old bg-indigo-950/60 had no light-mode + // override, so light mode got dark text on a dark block. + ? "bg-sev-major/15 border border-sev-major/50" + : "bg-accent/15 border border-accent/40 hover:border-accent/70" }`} >

{e.title}

{height >= 44 && ( -

+

{fmtTime(e.start_time)} {e.end_time ? ` – ${fmtTime(e.end_time)}` : ""} {isConflict ? " ⚠ conflict" : ""} diff --git a/drb-frontend/components/NodeConfigModal.tsx b/drb-frontend/components/NodeConfigModal.tsx index cd7d063..9281ac6 100644 --- a/drb-frontend/components/NodeConfigModal.tsx +++ b/drb-frontend/components/NodeConfigModal.tsx @@ -54,8 +54,12 @@ export function NodeConfigModal({ node, systems, onClose }: Props) {

Configure Node

- {node.node_id} connected for the first time. - Assign it a radio system to begin monitoring. + {node.configured ? ( + <>{node.node_id}: choose the radio system it should monitor. + ) : ( + <>{node.node_id} connected for the first time. + Assign it a radio system to begin monitoring. + )}

diff --git a/drb-frontend/lib/types.ts b/drb-frontend/lib/types.ts index ba83071..d3c454c 100644 --- a/drb-frontend/lib/types.ts +++ b/drb-frontend/lib/types.ts @@ -487,6 +487,8 @@ export interface IncidentRecord { title: string | null; type: string | null; status: "active" | "resolved"; + /** Set by the idle sweep; a manual resolve (PUT /incidents) doesn't set it. */ + resolved_at?: string | null; location: string | null; location_coords: GeoPoint | null; /** -- 2.54.0