frontend: safe fixes from the #109 punch-list (#113)
Build & Deploy / Build & push images (push) Successful in 4m5s
Build & Deploy / Deploy to VM (push) Successful in 2m33s
Build & Deploy / Report a failed deploy (push) Skipped

This commit was merged in pull request #113.
This commit is contained in:
2026-09-07 00:13:35 -04:00
8 changed files with 27 additions and 19 deletions
+3 -2
View File
@@ -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() {
</div>
</div>
{incident.vehicles?.length > 0 && (
{vehicles.length > 0 && (
<div>
<p className="text-xs text-ink-muted uppercase tracking-wide mb-2">Vehicles</p>
<div className="flex flex-wrap gap-1">
{incident.vehicles.map((v) => (
{vehicles.map((v) => (
<span key={v} className="text-xs bg-raised text-ink-2 px-2 py-0.5 rounded font-mono">{v}</span>
))}
</div>
+1 -1
View File
@@ -60,7 +60,7 @@ function DiscordJoinModal({
<div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50 p-4">
<form
onSubmit={handleSubmit}
className="bg-gray-900 border border-gray-700 rounded-xl p-6 space-y-4 font-mono w-full max-w-sm"
className="bg-gray-900 border border-gray-700 rounded-xl p-6 space-y-4 font-mono w-full max-w-sm max-h-[90vh] overflow-y-auto"
>
<h3 className="text-white font-semibold">Join Discord Voice</h3>
<div>
+1 -1
View File
@@ -42,7 +42,7 @@ export default function NodesPage() {
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{pending.map((n) => (
<div key={n.node_id} onClick={() => setConfigNode(n)} className="cursor-pointer">
<NodeCard node={n} system={systemMap[n.assigned_system_id ?? ""]} />
<NodeCard node={n} system={systemMap[n.assigned_system_id ?? ""]} linkToDetail={false} />
</div>
))}
</div>
+5 -3
View File
@@ -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 (
<div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50">
<div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50 p-4">
<form
onSubmit={handleSubmit}
className="bg-gray-900 border border-gray-700 rounded-xl p-6 w-full max-w-md space-y-4"
className="bg-gray-900 border border-gray-700 rounded-xl p-6 w-full max-w-md space-y-4 max-h-[90vh] overflow-y-auto"
>
<h2 className="text-white font-bold">New Trip</h2>
+2 -4
View File
@@ -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<string | null>(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<HTMLAudioElement | null>(null);
if (!hasAudio) return null;
async function ensureUrl() {
if (url || loading) return;
setLoading(true);
@@ -169,7 +167,7 @@ export function CallSpineEntry({
{hasAudio && (
<div className="mt-1.5">
<InlinePlayer callId={call.call_id} hasAudio={hasAudio} />
<InlinePlayer callId={call.call_id} />
</div>
)}
+10 -4
View File
@@ -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 (
<Link href={`/nodes/${node.node_id}`}>
const body = (
<div className="bg-gray-900 border border-gray-800 rounded-lg p-4 hover:border-gray-600 transition-colors cursor-pointer">
<div className="flex items-start justify-between mb-3">
<div>
@@ -58,6 +63,7 @@ export function NodeCard({ node, system }: Props) {
</div>
)}
</div>
</Link>
);
return linkToDetail ? <Link href={`/nodes/${node.node_id}`}>{body}</Link> : body;
}
+2 -2
View File
@@ -50,8 +50,8 @@ export function NodeConfigModal({ node, systems, onClose }: Props) {
const selectedPreset = PRESETS.find((p) => p.value === preset);
return (
<div className="fixed inset-0 bg-black/70 flex items-center justify-center z-50">
<div className="bg-gray-900 border border-gray-700 rounded-xl p-6 w-full max-w-md font-mono">
<div className="fixed inset-0 bg-black/70 flex items-center justify-center z-50 p-4">
<div className="bg-gray-900 border border-gray-700 rounded-xl p-6 w-full max-w-md font-mono max-h-[90vh] overflow-y-auto">
<h2 className="text-white font-semibold mb-1">Configure Node</h2>
<p className="text-gray-400 text-sm mb-5">
<span className="text-indigo-400">{node.node_id}</span> connected for the first time.
+3 -2
View File
@@ -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. */