From d67b2057e623ab289f4d6c294be8f390230072d4 Mon Sep 17 00:00:00 2001 From: Logan Cusano Date: Mon, 7 Sep 2026 00:07:54 -0400 Subject: [PATCH] frontend: safe fixes from the #109 punch-list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CallSpineEntry.tsx: drop the dead `hasAudio` prop + the early `return null` that sat between hooks in InlinePlayer (React #310 risk). Parent already gates the mount on audio presence. - NodeCard.tsx + nodes/page.tsx: pending-node card no longer double-fires. NodeCard gains `linkToDetail` (default true); the pending branch passes false so the wrapping onClick (open config modal) isn't swallowed by the inner navigation. List view unchanged. - trips/page.tsx: TripCard badge now buckets on end_date >= today, matching the list's own upcoming/past split — an in-progress trip no longer shows a "Past" badge under "Upcoming". - trips/page.tsx, NodeConfigModal.tsx, nodes/[id]/page.tsx: tall modals get `p-4` on the overlay + `max-h-[90vh] overflow-y-auto` on the panel so they don't clip on short viewports (incidents' CreateModal pattern). - lib/types.ts: IncidentRecord.units / vehicles are optional now, matching Firestore (older docs omit them); incidents/[id] gains a `?? []` guard. Untypechecked (no node/npm locally). next build in deploy.yml gates it. Full list of remaining items in server-26 #109. Co-Authored-By: Claude Sonnet 5 --- drb-frontend/app/incidents/[id]/page.tsx | 5 +++-- drb-frontend/app/nodes/[id]/page.tsx | 2 +- drb-frontend/app/nodes/page.tsx | 2 +- drb-frontend/app/trips/page.tsx | 8 +++++--- drb-frontend/components/CallSpineEntry.tsx | 6 ++---- drb-frontend/components/NodeCard.tsx | 14 ++++++++++---- drb-frontend/components/NodeConfigModal.tsx | 4 ++-- drb-frontend/lib/types.ts | 5 +++-- 8 files changed, 27 insertions(+), 19 deletions(-) diff --git a/drb-frontend/app/incidents/[id]/page.tsx b/drb-frontend/app/incidents/[id]/page.tsx index 19638c7..01c961a 100644 --- a/drb-frontend/app/incidents/[id]/page.tsx +++ b/drb-frontend/app/incidents/[id]/page.tsx @@ -100,6 +100,7 @@ export default function IncidentDetailPage() { const displayTags = incident.tags.filter((t) => t !== "auto-generated"); const unitsActive = incident.units_active ?? incident.units ?? []; const unitsCleared = incident.units_cleared ?? []; + const vehicles = incident.vehicles ?? []; const active = incident.status === "active"; const visible = newestFirst.slice(0, earlierShown); @@ -213,11 +214,11 @@ export default function IncidentDetailPage() { - {incident.vehicles?.length > 0 && ( + {vehicles.length > 0 && (

Vehicles

- {incident.vehicles.map((v) => ( + {vehicles.map((v) => ( {v} ))}
diff --git a/drb-frontend/app/nodes/[id]/page.tsx b/drb-frontend/app/nodes/[id]/page.tsx index 0298663..9d0796a 100644 --- a/drb-frontend/app/nodes/[id]/page.tsx +++ b/drb-frontend/app/nodes/[id]/page.tsx @@ -60,7 +60,7 @@ function DiscordJoinModal({

Join Discord Voice

diff --git a/drb-frontend/app/nodes/page.tsx b/drb-frontend/app/nodes/page.tsx index f125cbe..7215834 100644 --- a/drb-frontend/app/nodes/page.tsx +++ b/drb-frontend/app/nodes/page.tsx @@ -42,7 +42,7 @@ export default function NodesPage() {
{pending.map((n) => (
setConfigNode(n)} className="cursor-pointer"> - +
))}
diff --git a/drb-frontend/app/trips/page.tsx b/drb-frontend/app/trips/page.tsx index 8ada768..f04a99f 100644 --- a/drb-frontend/app/trips/page.tsx +++ b/drb-frontend/app/trips/page.tsx @@ -22,7 +22,9 @@ function TripCard({ trip, isAdmin, onDelete }: { }) { const router = useRouter(); const today = new Date().toISOString().slice(0, 10); - const upcoming = trip.start_date >= today; + // Bucket and badge must agree: the list groups on end_date (page.tsx ~L176), + // so a trip isn't "Past" until it's over, not when it starts. + const upcoming = trip.end_date >= today; const attendeeCount = Object.keys(trip.attendees ?? {}).length; return ( @@ -97,10 +99,10 @@ function CreateModal({ onClose, onCreate }: { } return ( -
+

New Trip

diff --git a/drb-frontend/components/CallSpineEntry.tsx b/drb-frontend/components/CallSpineEntry.tsx index 27ad0c6..d939c1b 100644 --- a/drb-frontend/components/CallSpineEntry.tsx +++ b/drb-frontend/components/CallSpineEntry.tsx @@ -23,7 +23,7 @@ function fmtClock(s: number): string { return `${m}:${r.toString().padStart(2, "0")}`; } -function InlinePlayer({ callId, hasAudio }: { callId: string; hasAudio: boolean }) { +function InlinePlayer({ callId }: { callId: string }) { const [url, setUrl] = useState(null); const [error, setError] = useState(false); const [loading, setLoading] = useState(false); @@ -32,8 +32,6 @@ function InlinePlayer({ callId, hasAudio }: { callId: string; hasAudio: boolean const [duration, setDuration] = useState(0); const audioRef = useRef(null); - if (!hasAudio) return null; - async function ensureUrl() { if (url || loading) return; setLoading(true); @@ -169,7 +167,7 @@ export function CallSpineEntry({ {hasAudio && (
- +
)} diff --git a/drb-frontend/components/NodeCard.tsx b/drb-frontend/components/NodeCard.tsx index 3d2335f..4b4a2b9 100644 --- a/drb-frontend/components/NodeCard.tsx +++ b/drb-frontend/components/NodeCard.tsx @@ -5,15 +5,20 @@ import type { NodeRecord, SystemRecord } from "@/lib/types"; interface Props { node: NodeRecord; system?: SystemRecord; + /** + * When false, the card renders without its `/nodes/[id]` Link wrapper so a + * parent click handler can take the interaction (pending nodes open the + * config modal instead of navigating). Defaults to true. + */ + linkToDetail?: boolean; } -export function NodeCard({ node, system }: Props) { +export function NodeCard({ node, system, linkToDetail = true }: Props) { const lastSeen = node.last_seen ? new Date(node.last_seen).toLocaleTimeString() : "never"; - return ( - + const body = (
@@ -58,6 +63,7 @@ export function NodeCard({ node, system }: Props) {
)}
- ); + + return linkToDetail ? {body} : body; } diff --git a/drb-frontend/components/NodeConfigModal.tsx b/drb-frontend/components/NodeConfigModal.tsx index f6c4bcc..8a58bcd 100644 --- a/drb-frontend/components/NodeConfigModal.tsx +++ b/drb-frontend/components/NodeConfigModal.tsx @@ -50,8 +50,8 @@ export function NodeConfigModal({ node, systems, onClose }: Props) { const selectedPreset = PRESETS.find((p) => p.value === preset); return ( -
-
+
+

Configure Node

{node.node_id} connected for the first time. diff --git a/drb-frontend/lib/types.ts b/drb-frontend/lib/types.ts index bc07a7a..679b1b5 100644 --- a/drb-frontend/lib/types.ts +++ b/drb-frontend/lib/types.ts @@ -143,8 +143,9 @@ export interface IncidentRecord { call_ids: string[]; system_ids: string[]; talkgroup_ids: string[]; - units: string[]; - vehicles: string[]; + /** Omitted on incident docs written before these fields existed. */ + 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. */