Commit Graph
443 Commits
Author SHA1 Message Date
Logan CusanoandClaude Opus 5.5 94898d1af1 fix(frontend): issues from the 10-25 Chrome sweep
- Incident detail "← Incidents" used router.back(), so arriving from Live
  or a shared link it went to the map / out of the app. Now a Link.
- Resolved incidents showed "0m elapsed" (manual resolve sets no
  resolved_at, updated_at can predate the last call): end is now the
  latest of resolved_at, updated_at and the newest call.
- Admin: the max-width toggle wrapped the tab bar, so it reflowed on every
  tab switch and left the cursor over e.g. "Close calls". Body only now.
- STT Eval sat on "Loading…" forever when a scanned page had zero matches
  but a next_cursor — the refill effect didn't depend on cursor.
- Trip event blocks used dark-only bg-indigo-950/60: unreadable in light
  mode. Theme tokens now.
- Reassign modal said "connected for the first time" on configured nodes.
- Dark-mode placeholders dimmed + italic; they read as filled-in values.

Frontend unverified locally (no Node, #207); CI build typechecks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 23:12:07 -04:00
logan 33056e2269 Merge pull request 'Frontend: hide admin-only node/system controls from non-admins (#222, #283) + popup contrast' (#291) from fix/222-283-admin-gating into main
Build & Deploy / Build & push images (push) Successful in 4m18s
Build & Deploy / Deploy Firestore rules & indexes (push) Successful in 28s
Build & Deploy / Deploy to VM (push) Successful in 1m27s
Build & Deploy / Report a failed deploy (push) Skipped
2026-10-25 22:17:26 -04:00
Logan CusanoandClaude Opus 5.5 e8084357c3 fix(map): keep muted popup text AA on Leaflet's white popups
Token text-ink-muted is #8C98AE in dark mode, ~2.9:1 on the always-white
popups (node, incident, path-stop, AIS, 511 camera). Scoped override to
#5B6577 (~6:1). Found in tonight's regression review.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 22:09:57 -04:00
Logan CusanoandClaude Opus 5.5 9702b90d40 fix(frontend): hide admin-only node and system controls from non-admins (#222, #283)
UI gate only — no route auth changes. Node page: override Ack/Force
Revert, Configure/Reassign and Restart OP25 are admin-only; non-admins on
an unconfigured node get a "an admin needs to assign it" note. /nodes
"Needs Configuration" cards open the config modal only for admins, and
link to the node page otherwise. /systems: New/Edit/Duplicate/Delete and
the AI-flags, area-context, talkgroup-pending and vocabulary panels are
admin-only; operators get a read-only list. (Delete's missing catch was
fixed in #290.)

Widening any of these routes to org owners/operators is a separate,
security-reviewed backend change.

Frontend unverified: no Node on the authoring box (#207).

Closes #222
Closes #283

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 22:07:22 -04:00
logan 606b0a103c Merge pull request 'Frontend: readable API errors, no silent action failures (#284, #286)' (#290) from fix/284-286-error-feedback into main
Build & Deploy / Build & push images (push) Successful in 4m16s
Build & Deploy / Deploy Firestore rules & indexes (push) Successful in 27s
Build & Deploy / Deploy to VM (push) Successful in 1m24s
Build & Deploy / Report a failed deploy (push) Skipped
2026-10-25 21:39:15 -04:00
Logan CusanoandClaude Opus 5.5 f16f0a9f93 fix(frontend): readable API errors and no more silent action failures (#284, #286)
#286: request() now throws C2ApiError — message is FastAPI's `detail` or a
plain sentence for the status (401/403/404/429/5xx), with toString() so the
many String(e) call sites read cleanly. status/body stay on the error; the
Discord parsers rebuild the legacy "C2 API error N: body" text via
legacyErrorText(), so their invite/code parsing is unchanged.

#284: Resolve, Regenerate summary, alert-rule toggle/delete/load,
Acknowledge, trip deletes, system delete and Restart OP25 now surface
failures (ErrorBanner where the page has one, alert() where the page
already uses it). Rules tab no longer says "No alert rules configured"
when the load failed. Confirms added for Resolve (no reopen path exists),
rule delete and trip delete. Reopen deliberately not built — manual
resolve doesn't set resolved_at, so reopening is a lifecycle change.

Frontend unverified: no Node on the authoring box (#207).

Closes #286

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 21:38:13 -04:00
logan 6d667e81c9 Merge pull request 'Frontend: finish the token migration — raw greys to AA theme tokens' (#282) from fix/readability-tokens into main
Build & Deploy / Build & push images (push) Successful in 4m21s
Build & Deploy / Deploy Firestore rules & indexes (push) Successful in 27s
Build & Deploy / Deploy to VM (push) Successful in 1m36s
Build & Deploy / Report a failed deploy (push) Skipped
2026-10-25 21:34:38 -04:00
Logan CusanoandClaude Opus 5.5 bf206334e3 fix(frontend): finish the token migration — raw grey classes -> theme tokens
UI_REDESIGN chunk 4 was never finished: 39 app/component files still used
raw Tailwind greys, kept readable in light mode only by the !important
override sheet. In dark mode text-gray-600 is ~2.3:1 on surface, and the
light override maps it to #94a3b8 (~2.6:1 on white) — unreadable in both.

Mechanical mapping (886 classes): gray-100/200 -> ink, 300/400 -> ink-2,
500/600/700 -> ink-muted, bg 950/900/800 -> page/surface/raised,
border 800 -> line, 700/600 -> line-strong, plus hover/divide/placeholder.
Alpha suffixes kept (tokens are color-mix, so /60 works). bg-gray-700,
text-white and marketing pages left alone. text-[9px] -> text-[10px].

Frontend unverified: no Node on the authoring box (#207).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 21:29:13 -04:00
logan 3968b49b80 Merge pull request 'Map: readable incident rail — dark basemap, opaque cards, AA colours (#136)' (#281) from fix/136-map-contrast into main
Build & Deploy / Build & push images (push) Successful in 4m54s
Build & Deploy / Deploy Firestore rules & indexes (push) Successful in 29s
Build & Deploy / Deploy to VM (push) Successful in 3m17s
Build & Deploy / Report a failed deploy (push) Skipped
2026-10-25 21:03:22 -04:00
Logan CusanoandClaude Opus 5.5 8099871c68 fix(map): readable incident rail — dark basemap, opaque cards, AA colours (#136)
- "Dark" base layer was bright OSM in prod; invert the raster via
  .drb-tiles-dark so dark-theme overlays sit on a dark ground.
- Rail and mobile incident cards are opaque, not glass over the map.
- Dark --ink-muted #77839A -> #8C98AE, --sev-major #D03B3B -> #F05252
  (both failed AA as small text on surface/raised).
- Card borders used `color + "55"` on CSS var() colours, which is
  invalid CSS — no border rendered. Now color-mix.
- 10px rail labels -> 11px.

Frontend unverified: no Node on the authoring box (#207).

Closes #136

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 21:03:08 -04:00
logan 49166789f4 Merge pull request 'Live map follow-ups: stable pin icons, resolved IncidentPanel, filtered rail, weather vs camera dock (#279 #275 #268 #278)' (#280) from fix/map-followups into main
Build & Deploy / Build & push images (push) Successful in 4m16s
Build & Deploy / Deploy Firestore rules & indexes (push) Successful in 27s
Build & Deploy / Deploy to VM (push) Successful in 1m46s
Build & Deploy / Report a failed deploy (push) Skipped
2026-10-05 04:02:32 -04:00
Logan CusanoandClaude Opus 5.5 9698d463ad Live map follow-ups: stable pin icons, resolved IncidentPanel, filtered rail, weather vs camera dock
- #279: cache incident/fan divIcons by their visual inputs and React.memo
  MapView, so LiveView's 1s clock no longer rebuilds icons / calls setIcon
  and the #238 pulse ring completes its cycle.
- #275: IncidentPanel shows "resolved", "lasted X" frozen at resolved_at,
  and labels live cameras/context as current, not incident-time.
- #268: rail header (desktop + mobile drawer) reads "N of M · filtered"
  via a new filteredFrom prop while the wall filter is on.
- #278: while camera tiles are docked bottom-centre, the weather status
  chip and radar badge stack under the tiles inside the dock instead of
  being covered.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 04:01:20 -04:00
logan 3c5496acb1 Merge pull request 'Live wall attention cues: NEW lane, pin pulse, opt-in tone and notifications (#238)' (#276) from feat/238-attention-cues into main
Build & Deploy / Build & push images (push) Successful in 4m42s
Build & Deploy / Deploy Firestore rules & indexes (push) Successful in 30s
Build & Deploy / Deploy to VM (push) Successful in 2m55s
Build & Deploy / Report a failed deploy (push) Skipped
2026-10-05 03:53:41 -04:00
logan cde86a8ff9 Merge pull request 'Weather map layers: NWS warning polygons, n0q radar with valid time/legend/loop, wind at incident (#237)' (#277) from feat/237-weather-map into main
Build & Deploy / Build & push images (push) Successful in 4m44s
Build & Deploy / Deploy Firestore rules & indexes (push) Successful in 4s
Build & Deploy / Deploy to VM (push) Successful in 10s
Build & Deploy / Report a failed deploy (push) Skipped
2026-10-05 03:53:34 -04:00
Logan CusanoandClaude Opus 5.5 292c4fffd9 Attention cues review fixes (#238)
- NEW lane: Gate A notice pinned below the rows (it scrolled off with >3
  entries, same rule as the rail, #46); rows at text-xs for the wall.
- Mobile chip moved off MapView's centred "Live" chip, which it covered.
- Keyword cues re-resolve to the incident once correlation links the call,
  so the lane row is clickable and the pin pulses.
- Enabling notifications sends one confirmation notification; a browser
  that grants permission but throws on construct (mobile Chrome) now shows
  "unsupported" instead of a switched-on control that never delivers.
- useActiveIncidentsState: includeMetadataChanges so `loaded` flips on a
  metadata-only server sync (quiet org / offline start) instead of waiting
  for the first incident change and swallowing it into the baseline.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 03:52:02 -04:00
Logan CusanoandClaude Opus 5.5 da55114eef Radar loop: tick the frame outside MapView state (#237)
The 1 h loop set MapView state every 600 ms, re-rendering the whole map
component and rebuilding (setIcon) every marker icon on each tick. The
frame now lives in a small external store that only the radar frames and
the valid-time badge subscribe to.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 03:51:07 -04:00
Logan CusanoandClaude Opus 5.5 1a84bf46d7 Weather map layers: NWS warning polygons, n0q radar with valid time/legend/loop, wind at incident (#237)
- Weather Warnings overlay (layer key `wx`, saved in the view hash): NWS alert
  polygons coloured by event (NWS hazard-map colours), outline dash by kind,
  popup with event/headline/expiry/office and National Weather Service credit.
  Polls GET /weather/alerts every 2 min while on and visible, refetches on pan.
  A status chip always says what the layer shows; feed errors use the 511
  FeedProblem wording; zone-only alerts are listed, never dropped.
- Radar: IEM nexrad-n0r -> n0q. Valid-time badge from IEM's n0q_0.json,
  STALE past 15 min or when the time can't be read; dBZ legend; optional 1 h
  loop (nexrad-n0q-900913-m05m..m55m), off by default. Still refreshed in
  place, never remounted.
- liveWall: sampleRadarDbz reads the n0q tile with IEM's published 255-colour
  ramp (verified against live tiles); pointInPolygon/alertsAt, wxStyle,
  wantsWind helpers.
- Wall incident panel: "inside <warning>" chips, wind arrow + speed/gust +
  station/obs time on fire/hazmat incidents; region Feeds gains an NWS row.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 03:46:05 -04:00
Logan CusanoandClaude Opus 5.5 eb0faf2aea Live wall attention cues: NEW lane, pin pulse, opt-in tone and notifications (#238)
- lib/attention.ts: pure snapshot diff (Major opened, severity escalated,
  pursuit path gained a stop, priority keyword in a new transcript) with a
  silent baseline on page open; calls baseline separately on their first
  non-empty snapshot. Diffed over all active incidents so widening a filter
  is never "new"; emitted, laned and pulsed only for the FILTERED view.
- components/live/NewLane.tsx: "NEW · last 15 min" overlay above the rail
  (desktop) / compact chip (mobile), Gate A notice included.
- components/live/AttentionControls.tsx: opt-in Web Audio tone (enabling
  click unlocks audio) and Web Notifications while the tab is hidden, body
  carries the machine-output notice; prefs + keywords in localStorage.
- LiveView wires it and passes pulseIncidentIds to MapView (2 min pulse).
- useIncidents: useActiveIncidentsState exposes a loaded flag (first
  server snapshot) so the baseline never sees an empty pre-load list.
- MachineOutputNotice exports MACHINE_OUTPUT_NOTICE_TEXT (same wording).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 03:45:05 -04:00
logan 1d6e79ba25 Merge pull request 'Live wall: selected incident loads its own calls, resolved deep links, scrubber marks unloaded time (#239)' (#272) from fix/239-call-window into main
Build & Deploy / Build & push images (push) Successful in 4m18s
Build & Deploy / Deploy Firestore rules & indexes (push) Successful in 26s
Build & Deploy / Deploy to VM (push) Successful in 1m28s
Build & Deploy / Report a failed deploy (push) Skipped
2026-10-05 03:33:27 -04:00
logan 0d613019e0 Merge pull request 'Honest pins and paths: geocode precision, type-gated paths, impossible-speed stops (#236)' (#274) from feat/236-honest-pins into main
Build & Deploy / Build & push images (push) Failing after 15s
Build & Deploy / Deploy to VM (push) Skipped
Build & Deploy / Deploy Firestore rules & indexes (push) Skipped
Build & Deploy / Report a failed deploy (push) Successful in 2s
2026-10-05 03:33:20 -04:00
Logan CusanoandClaude Opus 5.5 e1bd3feac5 Incident page spine: number stops from pathStops, not its own index (#236)
The /incidents/[id] page kept its own stop numbering (every geocoded call,
1..N) and path length, while its MapView now numbers only movement
incidents' non-ghosted stops. A fire with 3 geocoded calls showed stops
1-3 on the spine and unnumbered dots on the map. Use the shared
pathStops()/stopNumbers(), show 'km path' only for movement incidents,
and surface the 'may be merged' flag in the header.

Legend: a plain pin is also what an older (pre-#236) pin with no recorded
precision looks like; say so instead of claiming 'Street address'.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 03:32:36 -04:00
Logan CusanoandClaude Opus 5.5 7a0e86decf Live wall review fixes: no not-found flash or map whiplash when the selected incident resolves (#239)
useIncident reports loading until its answer belongs to the current id
(the render before the effect read the previous id's loading=false), and
clears a stale doc on a denied read. LiveView holds the last selected doc
for the same id while the by-id lookup is pending, so a watched incident
resolving no longer deselects (fly to region) and reselects (fly back).

Follow-up for the panel's 'active'/elapsed copy on resolved incidents: #275.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 03:29:58 -04:00
logan 3ef8d586ea Merge pull request 'Discord: main DRB bot optional end to end + owner-flow integration tests (#225 S8)' (#271) from feat/225-s8-main-bot-optional into main
Build & Deploy / Build & push images (push) Successful in 4m26s
Build & Deploy / Deploy Firestore rules & indexes (push) Successful in 3s
Build & Deploy / Deploy to VM (push) Successful in 11s
Build & Deploy / Report a failed deploy (push) Skipped
2026-10-05 03:29:55 -04:00
Logan CusanoandClaude Opus 5.5 6bf072973c Honest pins and paths: geocode precision, type-gated paths, impossible-speed stops (#236)
Backend: _geocode_location now returns {lat, lng, location_type}, so the
precision rides onto the call doc and, unchanged via _resolve_location_pair,
onto the incident pin. Display-only: every correlator read indexes lat/lng.

Frontend: pins styled by precision (rooftop / interpolated ring /
GEOMETRIC_CENTER dashed uncertainty circle labelled with the spoken place /
unverified pre-#23 pins ghosted). liveWall.pathStops() is shared by the map
and the call spine: only police/accident incidents with a pursuit tag draw a
numbered path; others get unnumbered call dots. Segments over >10 min gaps or
talkgroup changes are dashed, stops implying >8 km/min are ghosted off the
path, paths >20 km get a "may be merged" badge. New optional MapView prop
pulseIncidentIds (wired by #238).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 03:28:00 -04:00
Logan CusanoandClaude Opus 5.5 a267fdf982 Live wall: selected incident loads its own calls; resolved deep links; scrubber marks unloaded time (#239)
- LiveView: the selected incident merges useCallsByIncident (org_id +
  incident_ids CONTAINS, existing index, no orderBy/limit) into the calls fed
  to IncidentPanel and MapView, so a long incident keeps its full path and
  spine past the 500-call window. Stale rows from the previous selection are
  re-checked against the current id before merging.
- #inc= not in the active set is fetched with useIncident: a resolved one is
  selected, drawn on the map and shown under a "Resolved HH:MM" banner; a
  missing or other-org id shows "Incident not found". Auto-tour stays on
  active incidents.
- TimeScrubber: new loadedSince prop (oldest call when the window came back
  full). The axis before it is hatched and labelled "not loaded" instead of
  reading as a quiet stretch.
- useCalls returns hasMore (full page, counted before the duplicate_of
  filter); useIncident resets loading on a new id so a lookup doesn't flash
  "not found".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 03:25:13 -04:00
Logan CusanoandClaude Opus 5.5 9c124d4c8b S8: main DRB bot optional end to end + owner-flow integration tests (server-26#225)
- Copy: picker, admin Servers/Streams/How-it-works, org Discord card, bot
  /help and the server-limit refusal no longer imply the DRB server bot is
  required; a radio bot alone is enough.
- Admin overview: per-server main_bot_present, radio_bots, linked_via,
  directory_source; Servers tab gets a Bots column.
- discord_web docstring: states the reversed "never calls Discord" rule
  (plan finding 5) and the optional server bot.
- Server bot directory cog: re-push (which drops C2's cached permission
  answers) on overwrite changes, role permission changes and role deletes.
- tests/test_discord_owner_flows.py: one-bot org (BYO bot, owner-set
  preferred, OAuth link, picker, start, reconciler) and full setup (server
  bot + 4 radio bots: placement, shared channel, every refusal explained),
  against a modelled Discord over httpx.MockTransport.
- storage.download_audio removed: no caller on main since #233.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 03:24:44 -04:00
logan 6e462720a1 Merge pull request 'Power Outages map layer + wires-down cross-match on the live wall (#184)' (#270) from feat/184-outage-map into main
Build & Deploy / Build & push images (push) Successful in 4m33s
Build & Deploy / Deploy Firestore rules & indexes (push) Successful in 28s
Build & Deploy / Deploy to VM (push) Successful in 1m26s
Build & Deploy / Report a failed deploy (push) Skipped
2026-10-05 03:17:54 -04:00
logan fdcedf175c Merge pull request 'Live wall: filter bar, discipline presets and text search (#235)' (#267) from feat/235-filter-bar into main
Build & Deploy / Build & push images (push) Successful in 4m31s
Build & Deploy / Deploy Firestore rules & indexes (push) Successful in 6s
Build & Deploy / Deploy to VM (push) Successful in 7s
Build & Deploy / Report a failed deploy (push) Skipped
2026-10-05 03:17:48 -04:00
Logan CusanoandClaude Opus 5.5 e7070a0afa Outages review fixes: reset on new target, memoize marker icons, coverage note in popup (#184)
- useOutagesIn kept the previous incident's answer while fetching the next
  one (IncidentPanel isn't keyed), so a newly selected incident briefly read
  'none reported here'. Drop the old answer when the target changes.
- OutageLayer built a fresh divIcon per marker every render; MapView
  re-renders on live updates, so Leaflet rebuilt every marker's DOM. Memoize.
- Popup now carries the provider's coverage note (ODIN: non-reporting
  utilities, e.g. Con Ed/O&R, show nothing even during an outage).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 03:16:58 -04:00
Logan CusanoandClaude Opus 5.5 2aaf474828 Live wall filter: preset tags match word starts; keep typed trailing space (#235)
- 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>
2026-10-05 03:16:01 -04:00
Logan CusanoandClaude Opus 5.5 d10c2f732a Power Outages map layer + wires-down cross-match on the live wall (#184)
- lib/useOutages.ts: GET /outages for the map view (pan/zoom debounced) or a
  fixed bbox; polls every 3 min while visible, refetches when a hidden tab
  returns.
- components/map/OutageLayer.tsx: county markers at the label point sized by
  meters out, stale-only counties dimmed/dashed; popup with count, % served
  when known, since/ETR, per-utility counts, SourceCredit + fetch time, and
  "county centre, not the outage location". An always-on status chip says
  what the layer shows: loading, N counties, none reported in view (with the
  source's coverage note), no source enabled (admin-only link to
  /admin#outages), no licensed source covers this area, or feed error.
- MapView: "Power Outages" overlay, off by default, plus a legend entry while
  it is on. Not hash-persisted yet: LAYER_NAMES lives in lib/liveWall.ts,
  which #235 owns this batch.
- lib/outageMatch.ts (pure): county data matches by county bbox and says
  "county has N out" ("in or near" when two bboxes hold the point); point
  data matches within 500 m; stale rows never count as live; flags
  wires-down/transformer/manhole/pole/tree-on-wires incidents.
- WallPanels: OUTAGE lines under the incident's situational context (hazard
  chip, source credit, fetch time) and an OUTAGE row in the region Feeds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 03:12:32 -04:00
logan f49dc86111 Merge pull request 'Discord: Connect a Discord server — OAuth UI (#225 S7) — merge after S6' (#259) from feat/225-s7-oauth-ui into main
Build & Deploy / Build & push images (push) Successful in 4m11s
Build & Deploy / Deploy Firestore rules & indexes (push) Successful in 30s
Build & Deploy / Deploy to VM (push) Successful in 1m34s
Build & Deploy / Report a failed deploy (push) Skipped
2026-10-05 03:11:23 -04:00
Logan CusanoandClaude Opus 5.5 f479578942 Live wall: filter bar, discipline presets and text search (#235)
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>
2026-10-05 03:11:02 -04:00
Logan CusanoandClaude Opus 5.5 32d223e556 S7: OAuth start/options accept system_id; Discord picker QA fixes (server-26#225, #256 #257 #258)
- discord_oauth: POST /discord/oauth/start and GET /options take an optional
  system_id. The system must be the org's own or shared (another org's or
  unknown = 404). Its preferred bot is strict: one that can't connect is a
  404, never a quiet swap to Automatic. No preference = the usual order.
  The picker's "bot for a system" now always sends system_id.
- #257: _channel_state gives a bot whose token Discord rejected its own
  "invalid" state; the picker shows "Radio bot needs replacing" with no
  invite links, instead of "No radio bot".
- #256: Start no longer stays disabled on "Needs fix". It re-checks with
  Discord first (refresh=1) and starts only if the fresh answer allows it.
  "Not a voice channel" still disables Start.
- #258: own tooltip for "Not a voice channel"; "platform admin" -> "DRB
  support"; PreferredBotsCard says plainly that adding your own bot isn't
  self-serve (no support address exists yet, see #47), and a load failure
  gets a plain message + Try again instead of the raw C2 error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 03:09:28 -04:00
logan f3b21cd432 Merge pull request 'Weather backend: NWS active alerts + nearest-METAR wind, cached (Refs #237)' (#263) from feat/237-weather-api into main
Build & Deploy / Build & push images (push) Successful in 5m21s
Build & Deploy / Deploy to VM (push) Canceled after 0s
Build & Deploy / Deploy Firestore rules & indexes (push) Canceled after 0s
Build & Deploy / Report a failed deploy (push) Skipped
2026-10-05 03:03:32 -04:00
Logan Cusano 13a3525e4c Merge remote-tracking branch 'origin/main' into feat/225-s7-oauth-ui 2026-10-05 03:03:26 -04:00
logan 6d0d3f79ce Merge pull request 'Live wall: saved views in the URL hash, per-viewer memory, kiosk mode (#234)' (#265) from feat/234-saved-views into main
Build & Deploy / Build & push images (push) Successful in 5m27s
Build & Deploy / Deploy to VM (push) Successful in 3s
Build & Deploy / Deploy Firestore rules & indexes (push) Successful in 3s
Build & Deploy / Report a failed deploy (push) Skipped
2026-10-05 03:03:26 -04:00
Logan CusanoandClaude Opus 5.5 b6055019fc Live wall: report the starting view once so a copied link carries the viewport (#234)
Without it the URL had no z/lat/lon until the first pan, so a link copied
from an untouched wall opened at the other browser's remembered view.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 03:02:44 -04:00
logan 2c254c1f57 Merge pull request 'Discord: per-bot OAuth "Connect a Discord server" backend (#225 S6)' (#260) from feat/225-s6-oauth-link into main
Build & Deploy / Build & push images (push) Successful in 5m35s
Build & Deploy / Deploy Firestore rules & indexes (push) Successful in 5s
Build & Deploy / Deploy to VM (push) Successful in 3s
Build & Deploy / Report a failed deploy (push) Skipped
2026-10-05 03:01:14 -04:00
logan c30b2ca33e Merge pull request 'Discord: channel list and presence without the main bot (#225 S5, fixes #251)' (#262) from feat/225-s5-channels into main
Build & Deploy / Build & push images (push) Successful in 5m37s
Build & Deploy / Deploy Firestore rules & indexes (push) Successful in 5s
Build & Deploy / Deploy to VM (push) Successful in 12s
Build & Deploy / Report a failed deploy (push) Skipped
2026-10-05 03:01:07 -04:00
Logan CusanoandClaude Opus 5.5 bdbde359a1 Weather review fixes: zone-fetch time budget, prod NWS_USER_AGENT (Refs #237)
- Cap one refresh's NWS zone fetches at 15 s wall clock. They run under the
  feed lock, so a hung zone endpoint could otherwise stall every
  /weather/alerts request for minutes; unfinished zones retry next refresh.
- Prod c2-core.env.j2 never set NWS_USER_AGENT, so the VM would have sent the
  contact-less placeholder. Default it to the site URL (overridable via
  vault_nws_user_agent); warn on the .env.example example.com placeholder too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 02:58:32 -04:00
Logan CusanoandClaude Opus 5.5 afd2bc463d Live wall: saved views in the URL hash, per-viewer memory, kiosk mode (#234)
- 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>
2026-10-05 02:58:22 -04:00
Logan CusanoandClaude Opus 5.5 eaa0be07c5 S7 review: align OAuth UI with S6's actual contract (server-26#225)
- complete: S6 returns streams_stopped, not stopped — the "N radio
  channels were stopped" note never showed.
- start: bot name is nested as bot.name.
- "The bot for a system" with an org-owned preferred bot now sends
  token_id (S6 accepts it today); system_id only for a platform-pinned
  pool bot, which still needs S6 to accept system_id.
- A 404 for one chosen bot no longer hides Connect and the picker
  (the message told the user to try Automatic, but the button was gone).
- Server count uses GET /tokens' per-bot server_cap when present.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 02:56:05 -04:00
Logan CusanoandClaude Opus 5.5 fb1b6956c2 Weather backend: NWS active alerts + nearest-METAR wind, cached (Refs #237)
GET /weather/alerts — api.weather.gov/alerts/active for settings.nws_area,
sent with the NWS-required User-Agent, cached weather_alerts_ttl_s (90 s)
with single-flight refresh. Zone-based alerts (geometry null) get their
polygons from affectedZones (24 h zone cache, api.weather.gov/zones/ only);
any still unresolved are returned in zone_only, never dropped. Failed
refresh keeps last-good data + error + fetched_at; a payload where no
feature parses is a schema-change error. Optional bbox filter.

GET /weather/wind — nearest METAR (aviationweather.gov /api/data/metar,
bbox query) within 75 km, cached 5 min per 0.5-degree cell; VRB wind and
stale reports flagged; available:false with a reason otherwise.

Both behind require_service_or_firebase_token like /traffic.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 02:54:33 -04:00
Logan CusanoandClaude Opus 5.5 a976464e72 Discord: channel list and presence without the main bot (server-26#225 S5)
A server linked to an org now shows in the node picker while a radio bot the
org may use is in it, even when the DRB server bot is absent or left. Its
voice-channel list is read from that radio bot's cached REST snapshot
(discord_rest.guild_directory, the same snapshot the permission check reads,
so no extra Discord request): inline when no list is stored, in the
background when the list is older than discord_directory_refresh_seconds.
The server bot's own fresh list still wins (back-compat) and its directory
push now drops the server's cached snapshots.

Guards: a server acting only through the unlinked-server fallback org still
needs the server bot; REST never creates a discord_guilds doc, never writes
org_id or bot_present, and never writes onto a server unlinked meanwhile.

Also fixes server-26#251: an interactive start (/join, admin page, web
picker, node discord_join) re-reads a cached "denied" snapshot older than
10 s once before refusing, so "fix it in Discord, then try again" works on
the next try. The reconciler never re-reads (no REST loop).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 02:53:57 -04:00
Logan CusanoandClaude Opus 5.5 db4e326332 Discord: per-bot OAuth "Connect a Discord server" backend (server-26#225 S6)
An org owner links a server by inviting one radio bot through that bot's
own OAuth2 invite (owner decision Q1). The state is a single-use, 10-minute
nonce bound to org + user (stored only as its sha256); the server linked is
the one Discord's code exchange names, never the redirect query.

- routers/discord_oauth.py: GET /discord/oauth/options, POST /start,
  POST /complete. 404 while discord_oauth_redirect_uri is empty or no
  usable bot has a secret. Bot order: explicit token_id, the node system's
  preferred bot, the org's own bot, least-full pool bot under the cap.
- internal/discord_oauth.py: state mint/consume transaction (login-CSRF:
  state.uid must be the caller; any presentation burns it), code exchange,
  revoke, client-credentials secret check. Through discord_rest's shared
  client and breaker; secrets/codes/tokens never logged or in errors.
- discord_guild_link: one link transaction body (_apply_link) and one
  settle step that stops a server's streams when the org it acted for
  changes (plan finding 3). /drb link redeem, the admin PUT link and OAuth
  all use it; the previous org is now read inside the link transaction.
- tokens: GET /tokens is an allowlist (+ oauth_enabled, server_count,
  server_cap); PUT/DELETE /tokens/{id}/oauth (admin) checks the secret with
  Discord before storing it in bot_oauth/{id}, never echoes it.
- firestore.rules: explicit deny for bot_oauth and discord_oauth_states.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 02:52:57 -04:00
Logan CusanoandClaude Opus 5.5 dd3e5c9655 Discord: "Connect a Discord server" OAuth UI (server-26#225 S7)
- /discord/connected: Discord's OAuth redirect target. Stashes code/state
  in sessionStorage, strips them from the URL (replaceState), no-referrer
  (metadata + response header), sends a signed-out user to
  /login?next=/discord/connected and resumes, access_denied = "Nothing
  changed", result card "Linked X: N ready, M need a fix".
- middleware.ts: the callback path joins NO_SESSION_COOKIE_GATE (plan
  finding 2). Also needed for signed-in users: drb_session is
  SameSite=Strict, so it is not sent on the cross-site redirect back from
  discord.com.
- /login honours ?next= for an allowlist of one path (no open redirect).
- DiscordServersCard: Connect is the primary action with a radio-bot
  picker (automatic / the bot for a system / the org's own bots); the
  /drb link code moves to a collapsed secondary option and opens itself
  when /discord/oauth/start answers 404.
- RadioBotsPanel: write-only client-secret field per bot (PUT/DELETE
  /tokens/{id}/oauth), "Connect on/off", "N of 100 servers" with the
  pool cap (90) warning.
- lib/discordOAuth.ts: the S6 client, kept out of c2api.ts; only ever
  navigates to a discord.com /oauth2/authorize URL.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 02:51:39 -04:00
logan 7bee87b147 Merge pull request 'Outage providers: licence-gated registry, ODIN adapter, /outages API, admin tab (Refs #184)' (#255) from feat/184-outage-providers into main
Build & Deploy / Build & push images (push) Successful in 4m13s
Build & Deploy / Deploy Firestore rules & indexes (push) Successful in 28s
Build & Deploy / Deploy to VM (push) Successful in 1m39s
Build & Deploy / Report a failed deploy (push) Skipped
2026-10-05 02:47:15 -04:00
logan 18c2aeeff2 Merge pull request 'Click-to-listen on the live wall + HTTP Range on /media (#233)' (#253) from feat/233-wall-audio into main
Build & Deploy / Build & push images (push) Failing after 10s
Build & Deploy / Deploy to VM (push) Skipped
Build & Deploy / Deploy Firestore rules & indexes (push) Skipped
Build & Deploy / Report a failed deploy (push) Successful in 1s
2026-10-05 02:47:08 -04:00
logan b615cce30c Merge pull request 'Live wall: truth strip keyed to the pipeline, not node heartbeats (#232)' (#252) from feat/232-truth-strip into main
Build & Deploy / Build & push images (push) Failing after 15s
Build & Deploy / Deploy to VM (push) Skipped
Build & Deploy / Deploy Firestore rules & indexes (push) Skipped
Build & Deploy / Report a failed deploy (push) Successful in 2s
2026-10-05 02:47:01 -04:00