Skip to content

feat(energy-atlas): EnergyDisruptionsPanel standalone timeline (§L #4)#3378

Merged
koala73 merged 4 commits into
mainfrom
feat/energy-disruptions-panel
Apr 24, 2026
Merged

feat(energy-atlas): EnergyDisruptionsPanel standalone timeline (§L #4)#3378
koala73 merged 4 commits into
mainfrom
feat/energy-disruptions-panel

Conversation

@koala73

@koala73 koala73 commented Apr 24, 2026

Copy link
Copy Markdown
Owner

Summary

Closes gap #4 in `docs/internal/energy-atlas-registry-expansion.md` §L — the 52 disruption events in `energy:disruptions:v1` were only reachable by drilling into a specific pipeline or storage facility. This PR makes the full log a first-class surface.

Shape

  • Reverse-chronological table of every event (newest first).
  • Filter chips: event type (sabotage, sanction, maintenance, mechanical, weather, war, commercial, other) + "ongoing only" toggle.
  • Row click dispatches the existing `energy:open-pipeline-detail` / `energy:open-storage-facility-detail` CustomEvent with `{assetId, highlightEventId}` — no new open-panel protocol. Mirrors CountryDeepDivePanel's disruption row contract from feat(energy-atlas): seed-side countries[] denorm on disruptions + CountryDeepDive row (§R #5 = B) #3377.
  • Uses the shared `src/shared/disruption-timeline.ts` formatters (`formatEventWindow`, `formatCapacityOffline`, `statusForEvent`) that PipelineStatusPanel + StorageFacilityMapPanel already use — consistent UI across the 3 disruption surfaces.

Wiring

File Change
`src/components/EnergyDisruptionsPanel.ts` New (~230 lines)
`src/components/index.ts` Export
`src/app/panel-layout.ts` `createPanel('energy-disruptions', () => new EnergyDisruptionsPanel())` next to the other 3 atlas panels
`src/config/panels.ts` Add to `FULL_PANELS` (p=2) + `ENERGY_PANELS` (p=1) + `PANEL_CATEGORY_MAP.marketsFinance`
`src/config/commands.ts` CMD+K entry `panel:energy-disruptions` with keywords (sabotage, sanctions events, force majeure, drone strike, nord stream sabotage, …)

Not in this PR (explicitly scoped out)

  • No new map pin layer — per plan §Q (Codex approved), disruptions stay a tabular/timeline surface; map assets (pipelines + storage) already show disruption markers on click via PipelineStatusPanel/StorageFacilityMapPanel drawers.
  • No proto changes — uses existing `listEnergyDisruptions` RPC with empty filters.

Test plan

  • `npm run typecheck` — clean
  • `npm run test:data` — 6694/6694 pass
  • Manual: CMD+K "disruption log" → panel opens with 52 events, newest first
  • Manual: click "Sabotage" chip → narrows to sabotage-only events
  • Manual: click a Nord Stream row → PipelineStatusPanel opens with that event highlighted
  • Manual: toggle "Ongoing only" → hides resolved events

Related

Stack note: this PR works standalone and does not depend on #3377's proto change — uses the existing wire shape. When #3377 merges, no rebase needed here.

Closes gap #4 from docs/internal/energy-atlas-registry-expansion.md §L.
Before this PR, the 52 disruption events in `energy:disruptions:v1`
were only reachable by drilling into a specific pipeline or storage
facility — PipelineStatusPanel and StorageFacilityMapPanel each render
an asset-scoped slice of the log inside their drawers, but no surface
listed the global event log. This panel makes the full log
first-class.

Shape:
- Reverse-chronological table (newest first) of every event.
- Filter chips: event type (sabotage, sanction, maintenance, mechanical,
  weather, war, commercial, other) + "ongoing only" toggle.
- Row click dispatches the existing `energy:open-pipeline-detail` or
  `energy:open-storage-facility-detail` CustomEvent with `{assetId,
  highlightEventId}` — no new open-panel protocol introduced. Mirrors
  the CountryDeepDivePanel disruption row contract from PR #3377.
- Uses `src/shared/disruption-timeline.ts` formatters
  (formatEventWindow, formatCapacityOffline, statusForEvent) that
  PipelineStatus/StorageFacilityMap already use — consistent UI across
  all three disruption surfaces.

Wiring:
- `src/components/EnergyDisruptionsPanel.ts` — new (~230 lines).
- `src/components/index.ts` — export.
- `src/app/panel-layout.ts` — `this.createPanel('energy-disruptions',
  () => new EnergyDisruptionsPanel())` alongside the other three
  atlas panels at :892.
- `src/config/panels.ts` — add to `FULL_PANELS` (priority 2, next to
  fuel-shortages) + `ENERGY_PANELS` (priority 1, top tier) +
  `PANEL_CATEGORY_MAP.marketsFinance` list alongside the other
  atlas panels.
- `src/config/commands.ts` — CMD+K entry `panel:energy-disruptions`
  with keywords matching the user vocabulary (sabotage, sanctions
  events, force majeure, drone strike, nord stream sabotage).

Not done in this PR:
- No new map pin layer — per plan §Q (Codex approved), disruptions
  stay a tabular/timeline surface; map assets (pipelines + storage)
  already show disruption markers on click.
- No direct globe-mode or SVG-fallback rendering needs — panel is
  pure DOM, not a map layer.

Test plan:
- [x] npm run typecheck (clean)
- [x] npm run test:data (6694/6694 pass)
- [ ] Manual: CMD+K "disruption log" → panel opens with 52 events,
      newest first. Click "Sabotage" chip → narrows to sabotage events
      only. Click a Nord Stream row → PipelineStatusPanel opens with
      that event highlighted.
@vercel

vercel Bot commented Apr 24, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
worldmonitor Ignored Ignored Preview Apr 24, 2026 2:39pm

Request Review

@greptile-apps

greptile-apps Bot commented Apr 24, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds EnergyDisruptionsPanel, a standalone reverse-chronological timeline of all 52 energy:disruptions:v1 events, wired into the panel layout, CMD+K commands, and variant configs. The implementation structure (data fetch, HTML generation, shared formatter usage, dispatchOpenAsset contract) is correct and consistent with sibling atlas panels.

  • P1 — All interactive elements non-functional: render() calls this.setContent(html) — which debounces the DOM write by 150 ms — then immediately runs querySelector calls on the still-unmodified DOM. Every [data-filter-type], [data-toggle-ongoing], and tr.ed-row query returns nothing, so the filter chips, ongoing toggle, and row click-through never get event listeners. The fix is to replace direct post-render attachment with event delegation on the persistent this.content element, the same pattern Panel uses for its own retry button.

Confidence Score: 3/5

Not safe to merge — the primary user-facing feature (filter chips, ongoing toggle, row click-through) is completely non-functional due to the setContent debounce / event-listener timing bug.

A single P1 defect causes every interactive element in the new panel to silently fail. Visually the panel renders correctly, but the core advertised behaviors (filter by event type, toggle ongoing, click a row to open the asset panel) are all broken. The wiring changes are clean and correct; the bug is isolated to EnergyDisruptionsPanel.ts and has a straightforward fix via event delegation.

src/components/EnergyDisruptionsPanel.ts — the post-setContent event-listener attachment block (lines 193–214) needs to be replaced with delegation on this.content.

Important Files Changed

Filename Overview
src/components/EnergyDisruptionsPanel.ts New panel component (~256 lines) — correct data fetch and HTML rendering, but all interactive elements (filter chips, ongoing toggle, row click-through) are broken because event listeners are queried and attached synchronously after setContent(), which debounces the DOM write by 150 ms.
src/app/panel-layout.ts Straightforward two-line addition: import + createPanel registration for energy-disruptions, consistent with the other three atlas panels.
src/config/panels.ts Adds energy-disruptions to FULL_PANELS (p=2), ENERGY_PANELS (p=1), and PANEL_CATEGORY_MAP.marketsFinance — mirrors the pattern of the three sibling atlas panels.
src/config/commands.ts Adds CMD+K entry panel:energy-disruptions with relevant keywords; consistent with sibling panel entries.
src/components/index.ts Single named-export addition, no issues.

Sequence Diagram

sequenceDiagram
    participant User
    participant EnergyDisruptionsPanel
    participant PanelBase
    participant SupplyChainServiceClient
    participant PipelineStatusPanel
    participant StorageFacilityMapPanel

    User->>EnergyDisruptionsPanel: CMD+K disruption log
    EnergyDisruptionsPanel->>PanelBase: showLoading()
    EnergyDisruptionsPanel->>SupplyChainServiceClient: listEnergyDisruptions
    SupplyChainServiceClient-->>EnergyDisruptionsPanel: ListEnergyDisruptionsResponse (52 events)
    EnergyDisruptionsPanel->>PanelBase: setContent(html) [150ms debounce]
    Note over EnergyDisruptionsPanel: querySelector() runs NOW on old DOM — no listeners attached
    Note over PanelBase: 150ms later innerHTML updated — chips and rows rendered but unclickable
    User->>EnergyDisruptionsPanel: Click filter chip
    Note over User: No response — listener never attached
    Note over EnergyDisruptionsPanel: With event delegation fix
    User->>EnergyDisruptionsPanel: Click filter chip
    EnergyDisruptionsPanel->>EnergyDisruptionsPanel: setTypeFilter -> render()
    User->>EnergyDisruptionsPanel: Click table row
    EnergyDisruptionsPanel->>PipelineStatusPanel: window.dispatchEvent energy:open-pipeline-detail
    EnergyDisruptionsPanel->>StorageFacilityMapPanel: window.dispatchEvent energy:open-storage-facility-detail
Loading

Reviews (1): Last reviewed commit: "feat(energy-atlas): EnergyDisruptionsPan..." | Re-trigger Greptile

Comment on lines +193 to +214
.ed-row { cursor: pointer; }
.ed-row:hover td { background: rgba(255,255,255,0.03); }
.ed-event { font-weight: 600; color: var(--text, #eee); }
.ed-sub { font-size: 9px; color: var(--text-dim, #888); text-transform: uppercase; letter-spacing: 0.04em; }
.ed-asset-type { display: inline-block; padding: 1px 6px; border-radius: 8px; font-size: 8px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em; background: rgba(255,255,255,0.08); color: var(--text-dim, #aaa); margin-right: 4px; }
.ed-badge { display: inline-block; padding: 2px 8px; border-radius: 10px; font-size: 9px; font-weight: 700; color: #fff; text-transform: uppercase; letter-spacing: 0.04em; }
.ed-empty { text-align: center; color: var(--text-dim, #888); padding: 20px; font-style: italic; }
.ed-offline { font-family: monospace; font-size: 10px; color: var(--text, #eee); }
</style>
`);

const table = this.element?.querySelector('.ed-table') as HTMLTableElement | null;
table?.querySelectorAll<HTMLTableRowElement>('tr.ed-row').forEach(tr => {
const eventId = tr.dataset.eventId;
const assetId = tr.dataset.assetId;
const assetType = tr.dataset.assetType;
if (!eventId || !assetId || !assetType) return;
tr.addEventListener('click', () => this.dispatchOpenAsset(eventId, assetId, assetType));
});

this.element?.querySelectorAll<HTMLButtonElement>('[data-filter-type]').forEach(btn => {
btn.addEventListener('click', () => this.setTypeFilter(btn.dataset.filterType ?? ''));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Event listeners attached before DOM update, all interactions silently broken

setContent() in the Panel base class has a 150 ms debounce — it stores the HTML in pendingContentHtml and only calls setContentImmediate (which actually writes innerHTML) after the timer fires. The querySelector calls that immediately follow setContent() here execute synchronously against the old DOM (the loading-spinner state on first render, or the previous filter result on re-renders), so .ed-table, [data-filter-type], and [data-toggle-ongoing] are never found and no listeners are ever attached. Visually the panel renders correctly after 150 ms, but every user interaction — filter chips, ongoing toggle, row click-through — is permanently non-functional.

The fix is to use event delegation on the persistent this.content element, matching the same pattern Panel uses for its retry button:

// In constructor (or once after first render):
this.content.addEventListener('click', (e) => {
  const filterBtn = (e.target as HTMLElement).closest<HTMLButtonElement>('[data-filter-type]');
  if (filterBtn) { this.setTypeFilter(filterBtn.dataset.filterType ?? ''); return; }

  const ongoingBtn = (e.target as HTMLElement).closest<HTMLButtonElement>('[data-toggle-ongoing]');
  if (ongoingBtn) { this.toggleOngoingOnly(); return; }

  const row = (e.target as HTMLElement).closest<HTMLTableRowElement>('tr.ed-row');
  if (row?.dataset.eventId && row.dataset.assetId && row.dataset.assetType) {
    this.dispatchOpenAsset(row.dataset.eventId, row.dataset.assetId, row.dataset.assetType);
  }
});

This listener sits on a persistent DOM node and works correctly regardless of when setContent flushes.

Comment on lines +137 to +140
const rows = this.filterEvents().map(e => this.renderRow(e)).join('');
const totalCount = this.data.events.length;
const ongoingCount = this.data.events.filter(e => !e.endAt).length;
const filteredCount = this.filterEvents().length;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 filterEvents() called twice per render

filterEvents() is called once to build rows and a second time to compute filteredCount. With 52 events this is negligible, but both calls do the same filtering + sort — capturing the result once avoids the redundancy.

Suggested change
const rows = this.filterEvents().map(e => this.renderRow(e)).join('');
const totalCount = this.data.events.length;
const ongoingCount = this.data.events.filter(e => !e.endAt).length;
const filteredCount = this.filterEvents().length;
const filtered = this.filterEvents();
const rows = filtered.map(e => this.renderRow(e)).join('');
const totalCount = this.data.events.length;
const ongoingCount = this.data.events.filter(e => !e.endAt).length;
const filteredCount = filtered.length;

…te (review P2)

Two Codex P2 findings on this PR:

1. Row click dispatched `highlightEventId` but neither
   PipelineStatusPanel nor StorageFacilityMapPanel consumes it. The
   UI's implicit promise (event-specific highlighting) wasn't
   delivered — clickthrough was asset-generic, and the extra field
   on the wire was a misleading API surface.

   Fix: drop `highlightEventId` from the dispatched detail. Row click
   now opens the asset drawer with just {pipelineId, facilityId}, the
   fields the receivers actually consume. User sees the full
   disruption timeline for that asset and locates the event visually.

   A future PR can add real highlight support by:
     - drawers accept `highlightEventId` in their openDetailHandler
     - loadDetail stores it and renderDisruptionTimeline scrolls +
       emphasises the matching event
     - re-add `highlightEventId` to the dispatch here, symmetrically
       in CountryDeepDivePanel (which has the same wire emission)

   The internal `_eventId` parameter is kept as a plumb-through so
   that future work is a drawer-side change, not a re-plumb.

2. `events.length === 0` was conflated with `upstreamUnavailable` and
   triggered the error UI. The server contract (list-energy-disruptions
   handler) returns `upstreamUnavailable: false` with an empty events
   array when Redis is up but has no entries matching the filter — a
   legitimate empty state, not a fetch failure.

   Fix: gate `showError` on `upstreamUnavailable` alone. Empty results
   fall through to the normal render, where the table's
   `No events match the current filter` row already handles the case.

Typecheck clean, test:data 6694/6694 pass.
koala73 added a commit that referenced this pull request Apr 24, 2026
Codex P2: loadDisruptionsForCountry dispatched `highlightEventId` but
neither PipelineStatusPanel nor StorageFacilityMapPanel consumes it
(the openDetailHandler reads only pipelineId / facilityId). The UI's
implicit promise (event-specific highlighting) wasn't delivered —
clickthrough was asset-generic, and the extra wire field was a
misleading API surface.

Fix: emit only {pipelineId, facilityId} in the dispatched detail.
Row click opens the asset drawer; user sees the full per-asset
disruption timeline and locates the event visually.

Symmetric fix for PR #3378's EnergyDisruptionsPanel — both emitters
now match the drawer contract exactly. Re-add `highlightEventId`
here when the drawer panels ship matching consumer code
(openDetailHandler accepts it, loadDetail stores it,
renderDisruptionTimeline scrolls + emphasises the matching event).

Typecheck clean, test:data 6698/6698 pass.
Codex P1: Panel.setContent() debounces the DOM write by 150ms (see
Panel.ts:1025), so attaching listeners in render() via
`this.element.querySelector(...)` targets the STALE DOM — chips,
rows, and the ongoing-toggle button are silently non-interactive.
Visually the panel renders correctly after the debounce fires, but
every click is permanently dead.

Fix: register a single delegated click handler on `this.content`
(persistent element) in the constructor. The handler uses
`closest('[data-filter-type]')`, `closest('[data-toggle-ongoing]')`,
and `closest('tr.ed-row')` to route by data-attribute. Works
regardless of when setContent flushes or how many times render()
re-rewrites the inner HTML.

Also fixes Codex P2 on the same PR: filterEvents() was called twice
per render (once for row HTML, again for filteredCount). Now computed
once, reused. Trivial for 52 events but eliminates the redundant sort.

Typecheck clean.
Two events referenced pipeline ids that do not exist in
scripts/data/pipelines-oil.json:

- cpc-force-majeure-2022: assetId "cpc-pipeline" → "cpc"
- pdvsa-designation-2019: assetId "ve-petrol-2026-q1"
  → "venezuela-anzoategui-puerto-la-cruz"

Without this, clicking those rows in EnergyDisruptionsPanel
dead-ends at "Pipeline detail unavailable", so the panel
shipped with broken navigation on real data.

Mirrors the same fix on PR #3377 (gap #5a registry); applying
it on this branch as well so PR #3378 is independently
correct regardless of merge order. The two changes will dedupe
cleanly on rebase since the edits are byte-identical.
koala73 added a commit that referenced this pull request Apr 24, 2026
Codex P2: loadDisruptionsForCountry dispatched `highlightEventId` but
neither PipelineStatusPanel nor StorageFacilityMapPanel consumes it
(the openDetailHandler reads only pipelineId / facilityId). The UI's
implicit promise (event-specific highlighting) wasn't delivered —
clickthrough was asset-generic, and the extra wire field was a
misleading API surface.

Fix: emit only {pipelineId, facilityId} in the dispatched detail.
Row click opens the asset drawer; user sees the full per-asset
disruption timeline and locates the event visually.

Symmetric fix for PR #3378's EnergyDisruptionsPanel — both emitters
now match the drawer contract exactly. Re-add `highlightEventId`
here when the drawer panels ship matching consumer code
(openDetailHandler accepts it, loadDetail stores it,
renderDisruptionTimeline scrolls + emphasises the matching event).

Typecheck clean, test:data 6698/6698 pass.
koala73 added a commit that referenced this pull request Apr 24, 2026
…ntryDeepDive row (§R #5 = B) (#3377)

* feat(energy-atlas): seed-side countries[] denorm + CountryDeepDive row (§R #5 = B)

Per plan §R/#5 decision B: denormalise countries[] at seed time on each
disruption event so CountryDeepDivePanel can filter events per country
without an asset-registry round trip. Schema join (pipeline/storage
→ event.assetId) happens once in the weekly cron, not on every panel
render. The alternative (client-side join) was rejected because it
couples UI logic to asset-registry internals and duplicates the join
for every surface that wants a per-country filter.

Changes:
- `proto/.../list_energy_disruptions.proto`: add `repeated string
  countries = 15` to EnergyDisruptionEntry with doc comment tying it
  to the plan decision and the always-non-empty invariant.
- `scripts/_energy-disruption-registry.mjs`:
    • Load pipeline-gas + pipeline-oil + storage-facilities registries
      once per seed cycle; index by id.
    • `deriveCountriesForEvent()` resolves assetId to {fromCountry,
      toCountry, transitCountries} (pipeline) or {country} (storage),
      deduped + alpha-sorted so byte-diff stability holds.
    • `buildPayload()` attaches the computed countries[] to every
      event before writing.
    • `validateRegistry()` now requires non-empty countries[] of
      ISO2 codes. Combined with the seeder's `emptyDataIsFailure:
      true`, this surfaces orphaned assetIds loudly — the next cron
      tick fails validation and seed-meta stays stale, tripping
      health alarms.
- `scripts/data/energy-disruptions.json`: fix two orphaned assetIds
  that the new join caught:
    • `cpc-force-majeure-2022`: `cpc-pipeline` → `cpc` (matches the
      entry in pipelines-oil.json).
    • `pdvsa-designation-2019`: `ve-petrol-2026-q1` (non-existent) →
      `venezuela-anzoategui-puerto-la-cruz`.
- `server/.../list-energy-disruptions.ts`: project countries[] into
  the RPC response via coerceStringArray. Legacy pre-denorm rows
  surface as empty array (always present on wire, length 0 => old).
- `src/components/CountryDeepDivePanel.ts`: add 4th Atlas row —
  "Energy disruptions in {iso2}" — filtered by `iso2 ∈ countries[]`.
  Failure is silent; EnergyDisruptionsPanel (upcoming) is the
  primary disruption surface.
- `tests/energy-disruptions-registry.test.mts`: switch to validating
  the buildPayload output (post-denorm), add §R #5 B invariant
  tests, plus a raw-JSON invariant ensuring curators don't hand-edit
  countries[] (it's derived, not declared).

Proto regen note: `make generate` currently fails with a duplicate
openapi plugin collision in buf.gen.yaml (unrelated bug — 3 plugin
entries emit to the same out dir). Worked around by temporarily
trimming buf.gen.yaml to just the TS plugins for this regen. Added
only the `countries: string[]` wire field to both service_client and
service_server; no other generated-file drift in this PR.

* chore(proto): regenerate openapi specs for countries[] field

Runs `make generate` with the sebuf v0.11.1 plugin now correctly
resolved via the PATH fix (cherry-picked from fix/makefile-generate-path-prefix).
The new `countries` field on EnergyDisruptionEntry propagates into:

- docs/api/SupplyChainService.openapi.yaml (primary per-service spec)
- docs/api/SupplyChainService.openapi.json (machine-readable variant)
- docs/api/worldmonitor.openapi.yaml (consolidated bundle)

No TypeScript drift beyond the already-committed service_client.ts /
service_server.ts updates in 80797e7.

* fix(energy-atlas): drop highlightEventId emission (review P2)

Codex P2: loadDisruptionsForCountry dispatched `highlightEventId` but
neither PipelineStatusPanel nor StorageFacilityMapPanel consumes it
(the openDetailHandler reads only pipelineId / facilityId). The UI's
implicit promise (event-specific highlighting) wasn't delivered —
clickthrough was asset-generic, and the extra wire field was a
misleading API surface.

Fix: emit only {pipelineId, facilityId} in the dispatched detail.
Row click opens the asset drawer; user sees the full per-asset
disruption timeline and locates the event visually.

Symmetric fix for PR #3378's EnergyDisruptionsPanel — both emitters
now match the drawer contract exactly. Re-add `highlightEventId`
here when the drawer panels ship matching consumer code
(openDetailHandler accepts it, loadDetail stores it,
renderDisruptionTimeline scrolls + emphasises the matching event).

Typecheck clean, test:data 6698/6698 pass.

* fix(energy-atlas): collision detection + abort signal + label clamp (review P2)

Three Codex P2 findings on PR #3377:

1. `loadAssetRegistries()` spread-merged gas + oil pipelines, silently
   overwriting entries on id collision. No collision today, but a
   curator adding a pipeline under the same id to both files would
   cause `deriveCountriesForEvent` to return wrong-commodity country
   data with no test flagging it.

   Fix: explicit merge loop that throws on duplicate id. The next
   cron tick fails validation, seed-meta stays stale, health alarms
   fire — same loud-failure pattern the rest of the seeder uses.

2. `loadDisruptionsForCountry` didn't thread `this.signal` through
   the RPC fetch shim. The stale-closure guard (`currentCode !== iso2`)
   discarded stale RESULTS, but the in-flight request couldn't be
   cancelled when the user switched countries or closed the panel.

   Fix: wrap globalThis.fetch with { signal: this.signal } in the
   client factory, matching the signal lifecycle the rest of the
   panel already uses.

3. `shortDescription` values up to 200 chars rendered without
   ellipsis in the compact Atlas row, overflowing the row layout.

   Fix: new `truncateDisruptionLabel` helper clamps to 80 chars with
   ellipsis. Full text still accessible via click-through to the
   asset drawer.

Typecheck clean, test:data 6698/6698 pass.
@koala73
koala73 merged commit 73cd8a9 into main Apr 24, 2026
10 checks passed
@koala73
koala73 deleted the feat/energy-disruptions-panel branch April 24, 2026 15:09
koala73 added a commit that referenced this pull request Apr 24, 2026
…ly fetch (#3386)

Root cause: `App.ts::primeDataForVisiblePanels` is the sole near-viewport
kickoff path for panel `fetchData()` calls. Panel's constructor only
calls `showLoading()`; nothing else in the app triggers fetchData on
attach. Every panel that does real work has a corresponding
`if (shouldPrime('<key>')) primeTask(...)` entry in that table.

All 4 Energy Atlas panels were missing their entries:
- pipeline-status        → PipelineStatusPanel
- storage-facility-map   → StorageFacilityMapPanel
- fuel-shortages         → FuelShortagePanel
- energy-disruptions     → EnergyDisruptionsPanel

User-visible symptom: the 4 panels shipped as part of #3366 / #3378 /
the Energy Atlas PR chain rendered their headers + spinner but never
left "Loading…". Verified all four upstream RPCs return data:
- /api/supply-chain/v1/list-pipelines?commodityType=    → 100 KB
- /api/supply-chain/v1/list-storage-facilities          → 115 KB
- /api/supply-chain/v1/list-fuel-shortages              → 21 KB
- /api/supply-chain/v1/list-energy-disruptions          → 36 KB
and /api/health reports all 5 backing Redis keys as OK with the
expected record counts (75 gas + 75 oil + 200 storage + 29 shortages
+ 52 disruptions).

Fix: 4 primeTask entries mirroring the pattern already used for
energy-crisis, hormuz-tracker, oil-inventories, etc. Each panel
already implements the full fetch path (bootstrap-cache-first,
RPC fallback, setCached... back-propagation, error states); the
entries just give App.ts a reason to call it.

Placement: immediately after oil-inventories, grouping the energy
surface together.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant