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 (
-
router.back()}
- className="text-xs text-ink-muted hover:text-ink-2 transition-colors"
- >
+ {/* A real link, not router.back(): arriving from Live or a shared URL,
+ "back" went to the map or out of the app. */}
+
← Incidents
-
+
{/* 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.>
+ )}