Backend (c2-core):
- app/internal/cameras/: provider interface, licence model, per-provider
catalog cache; one adapter per source in design-canvas/camera-sources.md.
Implemented: 511NY (legacy shape + tolerant XML inventory parser, 511x
endpoints UNVERIFIED), Caltrans, DelDOT, Ontario 511, DriveBC, Calgary,
TfL, NSW, Fintraffic. Stubs: NYC DOT, Westchester DPW, TRANSCOM, CTroads,
511NJ, Austin, TxDOT, MD CHART, WSDOT, Tallinn, Tarktee, Warendorf,
TrafficLand, Windy, EarthCam.
- Every provider defaults OFF (config/camera_providers). Gated licences
(agreement required / unverified / non-commercial / paid) need an explicit
acknowledgement stored with who/when and written to audit_log; the audit
write is a precondition. Stubs and prohibited sources cannot be enabled.
Ack is bound to the licence status it was given for.
- GET /cameras (signed-in), GET/PUT /admin/camera-providers (admin only).
- /traffic/511 follows the ny511 provider switch and returns the DAA credit
+ NYSDOT as-is disclaimer; 511NY event descriptions no longer truncated
("may not alter the substance of a report").
Frontend:
- LiveView becomes the ops wall: Region > Incident > Camera drill-down,
worst-first rail (MapView), KPIs, transcript ticker, scrubber incident
bands, auto-tour, deep links (#inc=&cam=&tour), Esc steps up.
- Incident panel: nearest cameras (path-weighted), fused situational context
(ADS-B, AIS, 511NY events, NEXRAD tile sample), units, call spine.
- MapView keeps every layer; adds Camera Viewsheds (approx.) overlay and a
nearest-camera layer for the selected incident.
- CameraFeed hygiene: hidden-tab/off-screen gating, 2 concurrent HLS cap,
last good frame + stale badge, frozen-frame signature, HLS stall watchdog,
provider attribution rendered on the imagery.
- Camera/event popups show source credit, update time and the disclaimer.
- Admin > Camera providers tab with licence status, health, gated confirm.
- Camera detection is not built (blocked on #199/#200); seam only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
64 lines
3.9 KiB
TypeScript
64 lines
3.9 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useRef } from "react";
|
|
import type { Camera, CameraProviderFeed } from "@/lib/types";
|
|
import { CameraFeed } from "@/components/CameraFeed";
|
|
import { SourceCredit, fetchedLabel } from "@/components/SourceCredit";
|
|
|
|
/**
|
|
* Level 3 of the wall: one camera, large. Esc / Back steps up to the incident.
|
|
*
|
|
* Camera detections (the mockup's boxes + per-class counts) are NOT built:
|
|
* blocked on server-26#199 (NYSDOT's written answer on imagery analytics) and
|
|
* #200 (CISO ruling on retention). The seam is `CameraSignalSink` in c2-core
|
|
* (app/internal/cameras/base.py); this panel only states that plainly.
|
|
*/
|
|
export function CameraDrill({
|
|
camera, feed, distanceKm, incidentTitle, pinned, onTogglePin, onClose,
|
|
}: {
|
|
camera: Camera;
|
|
feed?: CameraProviderFeed;
|
|
distanceKm?: number | null;
|
|
incidentTitle?: string | null;
|
|
pinned: boolean;
|
|
onTogglePin: () => void;
|
|
onClose: () => void;
|
|
}) {
|
|
const closeRef = useRef<HTMLButtonElement>(null);
|
|
useEffect(() => { closeRef.current?.focus(); }, [camera.id]);
|
|
|
|
return (
|
|
<div className="fixed inset-0 z-[2000] bg-black/70 flex items-center justify-center p-4" role="dialog" aria-modal="true"
|
|
aria-labelledby="drb-drill-title" onClick={onClose}>
|
|
<div className="bg-surface border border-line-strong rounded-xl shadow-2xl w-full max-w-5xl max-h-full overflow-y-auto grid md:grid-cols-[minmax(0,1fr)_300px]"
|
|
onClick={(e) => e.stopPropagation()}>
|
|
<CameraFeed camera={camera} priority="high" credit={feed} className="w-full aspect-video bg-black md:rounded-l-xl" />
|
|
<div className="p-4 space-y-3 text-sm">
|
|
<div className="flex items-start gap-2">
|
|
<h2 id="drb-drill-title" className="flex-1 font-semibold text-ink leading-snug">{camera.name || camera.id}</h2>
|
|
<button ref={closeRef} type="button" onClick={onClose} className="text-xs border border-line rounded px-2 py-1 hover:bg-raised" aria-label="Close camera">Esc</button>
|
|
</div>
|
|
<dl className="grid grid-cols-[auto_1fr] gap-x-3 gap-y-1 text-xs">
|
|
<dt className="text-ink-muted">Source</dt><dd className="text-ink-2">{feed?.name ?? camera.provider}</dd>
|
|
{camera.roadway && (<><dt className="text-ink-muted">Road</dt><dd className="text-ink-2">{camera.roadway}</dd></>)}
|
|
{camera.direction && (<><dt className="text-ink-muted">Direction</dt><dd className="text-ink-2">{camera.direction}{camera.heading_deg != null ? " (approx. view)" : ""}</dd></>)}
|
|
{distanceKm != null && (<><dt className="text-ink-muted">Distance</dt><dd className="text-ink-2">{distanceKm.toFixed(1)} km from {incidentTitle ?? "the incident"}</dd></>)}
|
|
<dt className="text-ink-muted">Feed</dt><dd className="text-ink-2">{camera.video_url ? "Live stream (falls back to still)" : "Still image"}</dd>
|
|
</dl>
|
|
<SourceCredit credit={feed} fallbackName={feed?.name} updated={feed ? `camera list ${fetchedLabel(feed.fetched_at) ?? "—"}` : null} />
|
|
<div className="border border-dashed border-line rounded-md p-2.5 text-xs text-ink-muted space-y-1">
|
|
<p className="font-medium text-ink-2">Camera signals · future</p>
|
|
<p>Object-class counts from camera stills (vehicle, truck, person, smoke) are not built. Blocked on
|
|
server-26#199 (NYSDOT written answer on imagery analytics) and #200 (CISO ruling on retention).
|
|
No plate or face recognition will ever be part of it.</p>
|
|
</div>
|
|
<div className="flex gap-2 flex-wrap">
|
|
<button type="button" onClick={onTogglePin} className="text-xs rounded px-3 py-1.5 bg-accent text-white">{pinned ? "Unpin from wall" : "Pin to wall"}</button>
|
|
<button type="button" onClick={onClose} className="text-xs border border-line rounded px-3 py-1.5 hover:bg-raised">Back</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|