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>
52 lines
2.0 KiB
Python
52 lines
2.0 KiB
Python
"""Public-camera endpoints for the live ops wall.
|
|
|
|
``GET /cameras`` -- any signed-in user (auth applied at include time in main.py):
|
|
cameras inside a bbox from every provider an admin has enabled, each with the
|
|
provider's required attribution, disclaimer and catalog freshness.
|
|
|
|
``/admin/camera-providers`` -- platform admin only: list every catalogued
|
|
provider with its licence status and health, and toggle it. Gated licences
|
|
need ``acknowledge_licence: true``; that acknowledgement is stored with who and
|
|
when and written to audit_log (see app/internal/cameras/registry.py).
|
|
"""
|
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
from fastapi.responses import JSONResponse
|
|
from pydantic import BaseModel
|
|
|
|
from app.internal.auth import describe_actor, require_admin_token
|
|
from app.internal.cameras import registry
|
|
|
|
router = APIRouter(prefix="/cameras", tags=["cameras"])
|
|
admin_router = APIRouter(prefix="/admin/camera-providers", tags=["admin"])
|
|
|
|
|
|
@router.get("")
|
|
async def get_cameras(
|
|
south: float = Query(..., ge=-90, le=90),
|
|
west: float = Query(..., ge=-180, le=180),
|
|
north: float = Query(..., ge=-90, le=90),
|
|
east: float = Query(..., ge=-180, le=180),
|
|
):
|
|
if south > north or west > east:
|
|
raise HTTPException(400, "bbox must satisfy south<=north and west<=east")
|
|
return await registry.cameras_in_bbox((south, west, north, east))
|
|
|
|
|
|
class ProviderToggle(BaseModel):
|
|
enabled: bool
|
|
acknowledge_licence: bool = False
|
|
|
|
|
|
@admin_router.get("")
|
|
async def list_camera_providers(_=Depends(require_admin_token)):
|
|
return {"providers": await registry.list_providers()}
|
|
|
|
|
|
@admin_router.put("/{provider_id}")
|
|
async def set_camera_provider(provider_id: str, body: ProviderToggle, principal: dict = Depends(require_admin_token)):
|
|
try:
|
|
return await registry.set_enabled(provider_id, body.enabled, describe_actor(principal),
|
|
acknowledge_licence=body.acknowledge_licence)
|
|
except registry.ProviderError as e:
|
|
return JSONResponse(status_code=e.code, content={"detail": e.message, **e.extra})
|