Live ops wall + pluggable camera providers with licence gating (server-26#198)
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>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
d57738a8f6
commit
aa5b71d853
@@ -139,16 +139,83 @@ export interface VesselTrack {
|
||||
|
||||
// 511NY traffic layer (server-26#183) — GET /traffic/511, cached server-side,
|
||||
// not Firestore. *_local times are New York local, no offset: display only.
|
||||
export interface Ny511Camera {
|
||||
/**
|
||||
* One public camera from any enabled provider (GET /cameras, live ops wall).
|
||||
* `id` is provider-prefixed ("ny511:NYSDOT-1"). `heading_deg` is APPROXIMATE —
|
||||
* most sources publish the road's direction of travel, not the camera heading.
|
||||
*/
|
||||
export interface Camera {
|
||||
id: string;
|
||||
provider: string;
|
||||
lat: number;
|
||||
lon: number;
|
||||
name: string;
|
||||
roadway: string;
|
||||
direction: string;
|
||||
heading_deg: number | null;
|
||||
image_url: string | null;
|
||||
video_url: string | null;
|
||||
}
|
||||
/** Kept for older call sites; every camera is now a provider camera. */
|
||||
export type Ny511Camera = Camera;
|
||||
|
||||
/** What must travel with a provider's imagery: credit, link, pass-through disclaimer. */
|
||||
export interface CameraCredit {
|
||||
attribution: string | null;
|
||||
attribution_url: string | null;
|
||||
disclaimer: string | null;
|
||||
}
|
||||
|
||||
export interface CameraProviderFeed extends CameraCredit {
|
||||
id: string;
|
||||
name: string;
|
||||
fetched_at: number | null; // epoch s of the provider's last successful catalog fetch
|
||||
error: string | null;
|
||||
total_in_bbox: number;
|
||||
truncated: boolean;
|
||||
}
|
||||
|
||||
export interface CamerasResponse {
|
||||
cameras: Camera[];
|
||||
providers: CameraProviderFeed[];
|
||||
serving_count: number; // providers enabled anywhere (0 → "none enabled" empty state)
|
||||
}
|
||||
|
||||
export type LicenceStatus =
|
||||
| "commercial_ok" | "attribution_required" | "agreement_required"
|
||||
| "non_commercial" | "unverified" | "paid" | "prohibited";
|
||||
|
||||
/** Admin → Camera providers (GET /admin/camera-providers). */
|
||||
export interface CameraProviderAdmin {
|
||||
id: string;
|
||||
name: string;
|
||||
region: string;
|
||||
formats: string;
|
||||
implemented: boolean;
|
||||
notes: string;
|
||||
homepage: string | null;
|
||||
licence: {
|
||||
status: LicenceStatus;
|
||||
label: string;
|
||||
terms: string;
|
||||
terms_url: string | null;
|
||||
attribution: string | null;
|
||||
attribution_url: string | null;
|
||||
disclaimer: string | null;
|
||||
issue: string | null;
|
||||
gated: boolean;
|
||||
warning: string | null;
|
||||
};
|
||||
enabled: boolean;
|
||||
serving: boolean;
|
||||
not_serving_reason: string | null;
|
||||
acknowledgement: { by_uid: string; by_email: string; at: string; licence_status: string; warning: string } | null;
|
||||
updated_at: string | null;
|
||||
updated_by: { uid: string; email: string } | null;
|
||||
key_setting: string | null;
|
||||
key_missing: boolean;
|
||||
health: { fetched_at: number | null; last_attempt_at: number | null; error: string | null; count: number } | null;
|
||||
}
|
||||
|
||||
export interface Ny511Event {
|
||||
id: string;
|
||||
@@ -171,13 +238,17 @@ export interface Ny511FeedStatus {
|
||||
error: string | null; // set when the latest refresh failed (data is then stale)
|
||||
total_in_bbox: number;
|
||||
truncated: boolean;
|
||||
/** True when an admin has not enabled the 511NY provider (default OFF, DAA-gated). */
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export interface Ny511Response {
|
||||
cameras?: Ny511Camera[];
|
||||
cameras?: Camera[];
|
||||
cameras_status?: Ny511FeedStatus;
|
||||
events?: Ny511Event[];
|
||||
events_status?: Ny511FeedStatus;
|
||||
/** DAA: source credit + NYSDOT as-is disclaimer, present when the feed is served. */
|
||||
credit?: CameraCredit;
|
||||
}
|
||||
|
||||
export interface VocabularyPendingTerm {
|
||||
|
||||
Reference in New Issue
Block a user