- Storm/Traffic preset tag terms now match at the start of a word, so
"tree" no longer pulls "street-racing"/"street-fight" into Storm mode.
- FilterBar search compared the trimmed hash q against the untrimmed
typed text, so pausing after "main " reset the box to "main" and the
next word ran on ("mainst"). Compare trimmed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
lib/liveFilter.ts (pure): WallFilter {types, sevFloor, systems, talkgroups,
ageH, tags, q, preset}, applyFilter over loaded incidents + calls, parse /
serialise to the f.* hash keys #234 reserved. Presets: Fire (fire + EMS
assists heard on a fire channel; Radar on), Storm (weather/wires/tree/flood
tags; Radar on), Traffic (accident / MVA tags; Traffic Events on).
components/live/FilterBar.tsx: one wrapping row under the wall bar; less-used
controls fold behind a toggle below md; read-only summary in kiosk.
LiveView applies the filter once and feeds the filtered lists to MapView
(pins, paths, rail), RegionPanel, TranscriptTicker, TimeScrubber and the
auto-tour; KPIs count the filtered set and say "filtered". Presets turn
layers on through the saved-view layer state, not a MapView edit. Pipeline
health stays unfiltered. liveWall.ts keeps commas literal in f.* values.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- liveWall.ts: WallLink carries layers (short stable keys), z/lat/lon,
base, tourMs, kiosk and an f.<name> filter pass-through for #235;
parse/serialise stay pure and round-trip; old inc/cam/tour links parse.
- MapView: overlays/basemap/viewport come from initialLayers/initialBase/
initialView (defaults unchanged); onViewStateChange reports layer, base
and region-view changes (debounced). Initially-checked overlays now start
"shown" (react-leaflet adds a checked overlay before the control can fire
overlayadd). Closing an incident flies back to the pre-drill view (#137).
- wallPrefs.ts: per-browser last layers/base/view in localStorage, every
access wrapped; precedence hash > remembered > defaults.
- LiveView: one effect mirrors wall + map state into the hash; hash edits
remount the map with the new view; Copy view link; configurable tour.
- Kiosk (?kiosk=1 or #kiosk): no nav, fullscreen button (auto on first
touch), Screen Wake Lock re-acquired on visibility, reconnect banner
(offline / useCalls error) with backoff reload, 18-20 px type via a
data-kiosk attribute and Tailwind arbitrary variants.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Backend: /media/calls/{id}/audio now honours a single HTTP Range
(bytes=a-b, a-, -n) with 206 + Content-Range, 416 for unsatisfiable,
and a 200 full body (Accept-Ranges: bytes) otherwise. The body is
streamed from GCS in 512 KiB span reads (storage.audio_object_size +
download_audio_range) instead of downloading the whole object first,
so a 600 s FLAC starts after one span and seeks fetch only what they
need. HMAC check and Firestore lookup unchanged and still first.
Frontend: InlinePlayer moved to components/InlinePlayer.tsx and now
drives lib/audioQueue.ts, a useSyncExternalStore store over ONE audio
element (one call plays at a time). Wall spine rows get play buttons
and a "Play incident" queue (oldest first) with the playing row
highlighted; map path-stop popups get a play button and the playing
stop gets an accent ring; rail cards get "play latest". Playback is
gated by canPlayAudio() (BUSINESS_MODEL section 3 / Gate A hook).
Nothing plays until clicked. Spine truncation is now stated
("showing latest 30 of M", show-all toggle, count outside the loaded
window) for server-26#239.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The '● Live · Ns ago' pill was reset by any node/activeCalls/incident change,
so heartbeats kept it green while STT/correlation were dead (#196).
- lib/usePipelineHealth.ts: polls /health/ai (auth-free) every 60 s and
derives stage staleness from data too, since /health/ai resets on restart.
Radio = newest call; STT = calls heard after the newest transcript, dry when
>=3 are waiting and the oldest is > N min; AI = newest incident write /
linked call + any degraded /health/ai tier. N = NEXT_PUBLIC_WALL_STALE_MIN
(default 10); radio silence and AI absence get 3N (weaker evidence).
- components/live/TruthStrip.tsx: per-stage chips; full-width amber
DEGRADED bar naming the dry stage and any degraded tier.
- LiveView: heartbeat lastUpdated removed and no longer passed to MapView
(hides the old pill without touching MapView); 'Listening' only when every
stage is healthy; map + rail greyed when degraded; nodes KPI is now
'nodes hearing' (a call in the last N min).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- LiveView: localStorage pins carry image URLs, so they rendered (without
credit) after an admin disabled the provider. Pins now show only while
their provider is serving (server-26#198 licence gating).
- MapView: pinned/docked map players drop out once their provider leaves
the serving list; credits are kept from the last answer so docked players
keep attribution while the overlay is hidden.
- WallPanels: the "no camera provider enabled" link to /admin was a dead CTA
for non-admins (bounced to /). Link shown to admins only, deep-linked to
/admin#cameras; the admin page now honours a tab hash.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>