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:
Logan Cusano
2026-10-04 17:56:06 -04:00
co-authored by Claude Opus 5.5
parent d57738a8f6
commit aa5b71d853
28 changed files with 3342 additions and 132 deletions
+283
View File
@@ -0,0 +1,283 @@
"""Live ops wall camera providers: registry, licence gate, admin routes, adapters.
What is held here:
* every provider defaults OFF, and an off provider is never fetched;
* stubs and PROHIBITED sources can never be enabled;
* a gated licence needs ``acknowledge_licence``, which is stored with who/when
and audited -- and an audit failure refuses the enable (fail closed);
* an acknowledgement given for a different licence status no longer counts;
* the admin routes are admin-only;
* each implemented adapter parses its (verified) upstream shape, pins imagery
to the agency's hosts, and treats an all-unparseable payload as an error.
"""
import asyncio
from unittest.mock import AsyncMock, patch
import pytest
from fastapi import HTTPException
from fastapi.testclient import TestClient
from app.internal.auth import require_admin_token, require_service_or_firebase_token
from app.internal.cameras import base, general, regional, registry
from app.internal.cameras.base import Licence
from app.main import app
client = TestClient(app)
ADMIN = {"uid": "admin-1", "email": "admin@example.com", "role": "admin"}
class FakeStore:
def __init__(self):
self.docs = {}
self.audit = []
async def doc_get(self, col, doc):
return self.docs.get((col, doc))
async def doc_set(self, col, doc, data, merge=True):
cur = self.docs.setdefault((col, doc), {})
if col == "audit_log":
self.audit.append(data)
return
for k, v in data.items(): # one-level deep merge is all registry needs
if merge and isinstance(v, dict) and isinstance(cur.get(k), dict):
cur[k] = {**cur[k], **v}
else:
cur[k] = v
@pytest.fixture(autouse=True)
def store():
s = FakeStore()
registry.invalidate_cache()
for cat in registry.CATALOGS.values():
cat.items, cat.fetched_at, cat.error, cat._next_attempt = [], None, None, 0.0
with patch.object(registry.fstore, "doc_get", s.doc_get), patch.object(registry.fstore, "doc_set", s.doc_set), \
patch("app.internal.audit.fstore.doc_set", s.doc_set):
yield s
registry.invalidate_cache()
app.dependency_overrides.clear()
def run(coro):
return asyncio.run(coro)
# ── registry / gate ──────────────────────────────────────────────────────────
def test_every_catalogued_source_is_registered_and_defaults_off():
listed = run(registry.list_providers())
ids = {p["id"] for p in listed}
assert {"ny511", "nyc_dot", "westchester_dpw", "caltrans", "deldot", "tfl", "ontario511", "drivebc",
"calgary", "nsw", "fintraffic", "txdot", "austin", "tallinn", "tarktee", "warendorf"} <= ids
assert all(p["enabled"] is False and p["serving"] is False for p in listed)
assert all(p["licence"]["label"] for p in listed)
def test_off_provider_is_never_fetched():
fetch = AsyncMock(return_value=[])
with patch.object(registry.PROVIDERS["tfl"], "fetch_catalog", fetch):
out = run(registry.cameras_in_bbox((51.4, -0.2, 51.6, 0.1)))
assert out["cameras"] == [] and out["providers"] == [] and fetch.await_count == 0
def test_stub_and_prohibited_cannot_be_enabled():
for pid in ("nyc_dot", "westchester_dpw", "ctroads", "trafficland"):
with pytest.raises(registry.ProviderError) as e:
run(registry.set_enabled(pid, True, ("u", "e"), acknowledge_licence=True))
assert e.value.code == 409
def test_attribution_licence_enables_without_acknowledgement_and_is_audited(store):
out = run(registry.set_enabled("nsw", True, ("u1", "a@x")))
assert out["serving"] is True and out["acknowledgement"] is None
assert store.audit[-1]["action"] == "camera_provider.enable"
assert store.audit[-1]["actor_email"] == "a@x"
def test_gated_licence_requires_acknowledgement(store):
with pytest.raises(registry.ProviderError) as e:
run(registry.set_enabled("ny511", True, ("u1", "a@x")))
assert e.value.code == 409 and e.value.extra["requires_acknowledgement"] is True
assert "Developer Access Agreement" in e.value.extra["warning"]
assert not store.audit and not run(registry.is_serving("ny511"))
out = run(registry.set_enabled("ny511", True, ("u1", "a@x"), acknowledge_licence=True))
assert out["serving"] is True
ack = out["acknowledgement"]
assert ack["by_uid"] == "u1" and ack["by_email"] == "a@x" and ack["at"]
assert ack["licence_status"] == Licence.AGREEMENT_REQUIRED.value
assert store.audit[-1]["action"] == "camera_provider.enable_acknowledged"
assert store.audit[-1]["details"]["provider_id"] == "ny511"
def test_gated_enable_refused_when_audit_cannot_be_written(store):
with patch.object(registry, "_audit", AsyncMock(side_effect=RuntimeError("firestore down"))):
with pytest.raises(registry.ProviderError) as e:
run(registry.set_enabled("deldot", True, ("u", "e"), acknowledge_licence=True))
assert e.value.code == 503
assert not run(registry.is_serving("deldot"))
def test_disable_clears_acknowledgement(store):
run(registry.set_enabled("deldot", True, ("u", "e"), acknowledge_licence=True))
out = run(registry.set_enabled("deldot", False, ("u", "e")))
assert out["serving"] is False and out["acknowledgement"] is None
def test_stale_acknowledgement_stops_serving():
p = registry.PROVIDERS["deldot"]
entry = {"enabled": True, "acknowledgement": {"licence_status": "non_commercial"}}
assert registry.effective(p, entry) == (False, "licence acknowledgement missing or out of date")
entry["acknowledgement"]["licence_status"] = p.meta.licence.status.value
assert registry.effective(p, entry) == (True, None)
def test_unreadable_settings_fail_closed():
with patch.object(registry.fstore, "doc_get", AsyncMock(side_effect=RuntimeError("boom"))):
registry.invalidate_cache()
assert run(registry.get_settings()) == {}
def test_query_returns_attribution_and_skips_providers_outside_bbox(store):
run(registry.set_enabled("nsw", True, ("u", "e")))
run(registry.set_enabled("tfl", True, ("u", "e")))
cam = base.make_camera("nsw", "x", -33.87, 151.2, name="Sydney", image_url="https://webcams.transport.nsw.gov.au/a.jpg")
nsw_fetch, tfl_fetch = AsyncMock(return_value=[cam]), AsyncMock(return_value=[])
with patch.object(registry.PROVIDERS["nsw"], "fetch_catalog", nsw_fetch), \
patch.object(registry.PROVIDERS["tfl"], "fetch_catalog", tfl_fetch):
out = run(registry.cameras_in_bbox((-34.0, 151.0, -33.7, 151.4)))
assert [c["id"] for c in out["cameras"]] == ["nsw:x"]
assert out["providers"][0]["attribution"] == "Transport for NSW, CC BY 4.0"
assert tfl_fetch.await_count == 0 # London never fetched for a Sydney bbox
# ── admin routes ─────────────────────────────────────────────────────────────
def test_admin_routes_require_admin():
def deny():
raise HTTPException(403, "Admin access required")
app.dependency_overrides[require_admin_token] = deny
assert client.get("/admin/camera-providers").status_code == 403
assert client.put("/admin/camera-providers/nsw", json={"enabled": True}).status_code == 403
assert client.get("/admin/camera-providers", headers={}).status_code == 403
def test_admin_route_toggle_and_gate(store):
app.dependency_overrides[require_admin_token] = lambda: ADMIN
r = client.put("/admin/camera-providers/caltrans", json={"enabled": True})
assert r.status_code == 409 and r.json()["requires_acknowledgement"] is True
r = client.put("/admin/camera-providers/caltrans", json={"enabled": True, "acknowledge_licence": True})
assert r.status_code == 200 and r.json()["acknowledgement"]["by_email"] == "admin@example.com"
assert client.put("/admin/camera-providers/nope", json={"enabled": True}).status_code == 404
listed = client.get("/admin/camera-providers").json()["providers"]
assert next(p for p in listed if p["id"] == "caltrans")["serving"] is True
def test_cameras_route_rejects_inverted_bbox():
app.dependency_overrides[require_service_or_firebase_token] = lambda: {"uid": "v"}
assert client.get("/cameras", params={"south": 2, "west": 0, "north": 1, "east": 1}).status_code == 400
r = client.get("/cameras", params={"south": 40.9, "west": -74.0, "north": 41.4, "east": -73.4})
assert r.status_code == 200 and r.json()["cameras"] == []
# ── adapters ─────────────────────────────────────────────────────────────────
def _with_payload(payload):
return patch.object(general, "get_json", AsyncMock(return_value=payload))
def test_heading_from_text():
assert base.heading_from_text("Northbound") == 0
assert base.heading_from_text("WB") == 270
assert base.heading_from_text("SE") == 135
assert base.heading_from_text("Both directions") is None
assert base.heading_from_text("Unknown") is None
def test_https_on_pins_hosts():
assert base.https_on("https://cwwp2.dot.ca.gov/x.jpg", "dot.ca.gov")
assert base.https_on("http://cwwp2.dot.ca.gov/x.jpg", "dot.ca.gov") is None
assert base.https_on("https://dot.ca.gov.evil.com/x.jpg", "dot.ca.gov") is None
def test_caltrans_adapter():
row = {"cctv": {"inService": "true", "index": "1", "location": {
"district": "4", "locationName": "TV102 -- I-580 : West of SR-24", "latitude": "37.82539",
"longitude": "-122.27291", "direction": "West", "route": "I-580"},
"imageData": {"streamingVideoURL": "https://wzmedia.dot.ca.gov/D4/x.stream/playlist.m3u8",
"static": {"currentImageURL": "https://cwwp2.dot.ca.gov/data/d4/cctv/image/a/a.jpg"}}}}
off = {"cctv": {**row["cctv"], "inService": "false"}}
with _with_payload({"data": [row, off]}):
cams = run(general.CaltransProvider().fetch_catalog())
assert len(cams) == len(general.CaltransProvider.DISTRICTS) # same row per district fetch
c = cams[0]
assert c["id"] == "caltrans:d4-TV102" and c["heading_deg"] == 270 and c["video_url"].endswith(".m3u8")
def test_deldot_adapter_requires_https_hls():
good = {"id": "KCAM001", "title": "DE 1", "lat": 38.99, "lon": -75.44, "enabled": True, "status": "Active",
"urls": {"m3u8s": "https://video.deldot.gov:443/live/KCAM001.stream/playlist.m3u8",
"m3u8": "http://video.deldot.gov:1935/live/KCAM001.stream/playlist.m3u8"}}
down = {**good, "id": "KCAM002", "status": "Unavailable"}
with _with_payload({"videoCameras": [good, down]}):
cams = run(general.DelDotProvider().fetch_catalog())
assert [c["id"] for c in cams] == ["deldot:KCAM001"] and cams[0]["image_url"] is None
def test_drivebc_calgary_tfl_nsw_fintraffic_adapters():
with _with_payload([{"id": 1176, "name": "Lougheed", "is_on": True, "should_appear": True, "orientation": "W",
"location": {"coordinates": [-122.5, 49.2]}, "highway_description": "Hwy 7"}]):
assert run(general.DriveBcProvider().fetch_catalog())[0]["image_url"] == "https://www.drivebc.ca/images/1176.jpg"
with _with_payload([{"camera_url": {"url": "http://trafficcam.calgary.ca/loc86.jpg"}, "camera_location": "Stoney",
"point": {"coordinates": [-113.97, 50.9]}, "quadrant": "SE"}]):
c = run(general.CalgaryProvider().fetch_catalog())[0]
assert c["image_url"] == "https://trafficcam.calgary.ca/loc86.jpg" and c["id"] == "calgary:loc86"
with _with_payload([{"id": "JamCams_00002.00865", "commonName": "A406", "lat": 51.6, "lon": -0.016,
"additionalProperties": [{"key": "available", "value": "true"},
{"key": "imageUrl", "value": general.TflProvider.IMAGE_PREFIX + "a.jpg"}]}]):
assert run(general.TflProvider().fetch_catalog())[0]["id"] == "tfl:00002.00865"
with _with_payload({"features": [{"id": "abc", "geometry": {"coordinates": [151.1, -34.0]},
"properties": {"title": "5 Ways", "direction": "W",
"href": "https://webcams.transport.nsw.gov.au/x.jpeg"}}]}):
assert run(general.NswProvider().fetch_catalog())[0]["heading_deg"] == 270
with _with_payload({"features": [{"geometry": {"coordinates": [23.99, 60.05, 0]},
"properties": {"id": "C01503", "name": "kt51", "collectionStatus": "GATHERING",
"presets": [{"id": "C0150301", "inCollection": True},
{"id": "C0150302", "inCollection": False}]}}]}):
cams = run(general.FintrafficProvider().fetch_catalog())
assert [c["image_url"] for c in cams] == ["https://weathercam.digitraffic.fi/C0150301.jpg"]
def test_schema_change_is_an_error_not_an_empty_catalog():
with _with_payload([{"renamed": True}]):
with pytest.raises(ValueError, match="schema"):
run(general.DriveBcProvider().fetch_catalog())
def test_ontario_needs_its_key(monkeypatch):
monkeypatch.setattr(general.settings, "ontario511_api_key", None, raising=False)
with pytest.raises(RuntimeError, match="ONTARIO511_API_KEY"):
run(general.Ontario511Provider().fetch_catalog())
def test_ny511_provider_wraps_legacy_client_and_carries_daa_metadata():
row = {"id": "NYSDOT-1", "lat": 41.03, "lon": -73.76, "name": "I-287", "roadway": "I-287",
"direction": "Northbound", "image_url": "https://x/1", "video_url": None}
with patch.object(regional.ny511, "_fetch", AsyncMock(return_value=[row])):
cams = run(regional.Ny511Provider().fetch_catalog())
assert cams[0]["id"] == "ny511:NYSDOT-1" and cams[0]["heading_deg"] == 0
lic = regional.Ny511Provider.meta.licence
assert lic.attribution == "powered by 511NY" and lic.attribution_url == "https://511ny.org"
assert "with all faults" in lic.disclaimer and lic.gated
def test_failed_refresh_keeps_last_good_catalog():
cat = registry.CATALOGS["nsw"]
cam = base.make_camera("nsw", "x", -33.8, 151.2)
with patch.object(cat.provider, "fetch_catalog", AsyncMock(return_value=[cam])):
run(cat.get())
cat._next_attempt = 0.0
with patch.object(cat.provider, "fetch_catalog", AsyncMock(side_effect=RuntimeError("down"))):
run(cat.get())
assert cat.items == [cam] and "down" in cat.error and cat.fetched_at