Power Outages map layer + wires-down cross-match on the live wall (#184) #270

Merged
logan merged 2 commits from feat/184-outage-map into main 2026-10-05 03:17:55 -04:00
Owner

This is part 2 of 2 for #184. Part 1 was the provider registry, the ODIN adapter, /outages and the admin tab (#255, already merged). The frontend only reads GET /outages. No backend changes, and no frontend path can enable a provider.

Map: "Power Outages" overlay (components/map/OutageLayer.tsx, lib/useOutages.ts, MapView.tsx)

  • Off by default. While it is on, the map polls every 3 min, only when the tab is visible, and refetches on pan/zoom and when the tab comes back.
  • Each county with outages gets a bolt marker at the county's label point, sized by meters out. Counties with only stale rows (the backend's ETR-passed-over-24h flag) are dimmed and dashed and are never shown as live.
  • The popup shows meters/customers out (utility-reported), % served when known (ODIN gives "not known"), since, ETR, and per-utility counts. It also says "county centre, not where the outage is", plus the SourceCredit attribution, disclaimer, utility update time and fetch time.
  • The layer is never silently empty. While it is on, a status chip always says one of:
    • loading
    • N counties
    • none reported in view by ODIN, with ODIN's coverage note
    • no outage source enabled (only admins see the /admin#outages link)
    • no licensed outage source covers this area
    • feed error
  • An upstream schema change reaches this layer as the backend's status: "error" or a provider error. The chip shows it.
  • The legend gets an outage entry while the layer is on.

Cross-match (lib/outageMatch.ts, pure, WallPanels.tsx)

  • County data matches when the incident falls inside the county bbox. The wording is ": county has N meters/customers out (… county-level, not a location)". It becomes "In or near …" when two county bboxes hold the point.
  • Point data, from any future provider, matches within 500 m.
  • Stale rows are never a match. If the incident sits in a county that only has stale rows, the panel says so.
  • Wires-down, transformer, manhole, utility-pole and tree-on-wires incidents get a chip: "⚡ Utility-hazard call in a county with reported outages", "… within 0.5 km …", or "… no reported outage matched".
  • The incident panel's Situational context gets OUTAGE lines with source credit and fetch time. The region Feeds list gets an OUTAGE row per covering provider.
  • I skipped flagging outage clusters with no radio activity (optional in the brief).

Not done here: the overlay is not saved in the #234 URL hash yet. LAYER_NAMES is in lib/liveWall.ts, which #235 owns this batch. That follow-up is #269. The layer works, but it always starts off.

Verification

  • tsc --noEmit: exit 0, run in a fresh copy with the shared node_modules.
  • outageMatch smoke test (transpiled and run under node), all as expected:
    • hazard regex: wires-down, transformer, tree on wires, manhole, utility pole and downed power lines matched; "MVA" did not.
    • county match and its wording, the two-county "In or near" case, the 500 m point cutoff, stale exclusion, and a point outside every county.
  • No pytest run: no backend files changed.
  • Not run against live ODIN in a browser.

Closes #184

🤖 Generated with Claude Code

This is part 2 of 2 for #184. Part 1 was the provider registry, the ODIN adapter, `/outages` and the admin tab (#255, already merged). The frontend only reads `GET /outages`. No backend changes, and no frontend path can enable a provider. **Map: "Power Outages" overlay** (`components/map/OutageLayer.tsx`, `lib/useOutages.ts`, `MapView.tsx`) - Off by default. While it is on, the map polls every 3 min, only when the tab is visible, and refetches on pan/zoom and when the tab comes back. - Each county with outages gets a bolt marker at the county's label point, sized by meters out. Counties with only stale rows (the backend's ETR-passed-over-24h flag) are dimmed and dashed and are never shown as live. - The popup shows meters/customers out (utility-reported), % served when known (ODIN gives "not known"), since, ETR, and per-utility counts. It also says "county centre, not where the outage is", plus the `SourceCredit` attribution, disclaimer, utility update time and fetch time. - The layer is never silently empty. While it is on, a status chip always says one of: - loading - N counties - none reported in view by ODIN, with ODIN's coverage note - no outage source enabled (only admins see the `/admin#outages` link) - no licensed outage source covers this area - feed error - An upstream schema change reaches this layer as the backend's `status: "error"` or a provider `error`. The chip shows it. - The legend gets an outage entry while the layer is on. **Cross-match** (`lib/outageMatch.ts`, pure, `WallPanels.tsx`) - County data matches when the incident falls inside the county bbox. The wording is "<County>: county has N meters/customers out (… county-level, not a location)". It becomes "In or near …" when two county bboxes hold the point. - Point data, from any future provider, matches within 500 m. - Stale rows are never a match. If the incident sits in a county that only has stale rows, the panel says so. - Wires-down, transformer, manhole, utility-pole and tree-on-wires incidents get a chip: "⚡ Utility-hazard call in a county with reported outages", "… within 0.5 km …", or "… no reported outage matched". - The incident panel's Situational context gets OUTAGE lines with source credit and fetch time. The region Feeds list gets an OUTAGE row per covering provider. - I skipped flagging outage clusters with no radio activity (optional in the brief). **Not done here:** the overlay is not saved in the #234 URL hash yet. `LAYER_NAMES` is in `lib/liveWall.ts`, which #235 owns this batch. That follow-up is #269. The layer works, but it always starts off. **Verification** - `tsc --noEmit`: exit 0, run in a fresh copy with the shared node_modules. - `outageMatch` smoke test (transpiled and run under node), all as expected: - hazard regex: wires-down, transformer, tree on wires, manhole, utility pole and downed power lines matched; "MVA" did not. - county match and its wording, the two-county "In or near" case, the 500 m point cutoff, stale exclusion, and a point outside every county. - No pytest run: no backend files changed. - Not run against live ODIN in a browser. Closes #184 🤖 Generated with [Claude Code](https://claude.com/claude-code)
logan added 1 commit 2026-10-05 03:13:18 -04:00
- 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>
logan added 1 commit 2026-10-05 03:17:15 -04:00
- 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>
Author
Owner

Review (batch 3): ship. Pushed e7070a0 with three small fixes: (1) useOutagesIn now drops the previous target's answer when the box changes, since IncidentPanel is not keyed and a newly selected incident briefly read "none reported here"; (2) marker divIcons are memoized per response, so MapView re-renders no longer rebuild every marker; (3) the popup carries the provider coverage note (ODIN: Con Ed/O&R not reporting). tsc exit 0 in a fresh copy; c2-core pytest 919 passed. Merges cleanly onto origin/main f49dc86. Hash persistence is deferred to #269. Not merged.

Review (batch 3): ship. Pushed e7070a0 with three small fixes: (1) useOutagesIn now drops the previous target's answer when the box changes, since IncidentPanel is not keyed and a newly selected incident briefly read "none reported here"; (2) marker divIcons are memoized per response, so MapView re-renders no longer rebuild every marker; (3) the popup carries the provider coverage note (ODIN: Con Ed/O&R not reporting). tsc exit 0 in a fresh copy; c2-core pytest 919 passed. Merges cleanly onto origin/main f49dc86. Hash persistence is deferred to #269. Not merged.
logan merged commit 6e462720a1 into main 2026-10-05 03:17:55 -04:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: logan/server-26#270