* feat(energy-atlas): EnergyRiskOverviewPanel — executive overview tile (PR 2, plan U5-U6)
Lands the consolidated "first fold" surface that peer reference energy-intel
sites use as their executive overview. Six tiles in a single panel:
1. Strait of Hormuz status (closed/disrupted/restricted/open)
2. EU gas storage fill % (red <30, amber 30-49, green ≥50)
3. Brent crude price + 1-day change (importer-leaning: up=red, down=green)
4. Active disruption count (filtered to endAt === null)
5. Data freshness ("X min ago" from youngest fetchedAt)
6. Hormuz crisis day counter (default 2026-02-23 start, env-overridable)
Per docs/plans/2026-04-25-003-feat-energy-parity-pushup-plan.md PR 2.
U5 — Component (src/components/EnergyRiskOverviewPanel.ts):
- Composes 5 existing services via Promise.allSettled — never .all. One slow
or failing source CANNOT freeze the panel; failed tiles render '—' and
carry data-degraded="true" for QA inspection. Single most important
behavior — guards against the recurrence of the #3386 panel-stuck bug.
- Uses the actual Hormuz status enum 'closed'|'disrupted'|'restricted'|'open'
(NOT 'normal'/'reduced'/'critical' — that triplet was a misread in earlier
drafts). Test suite explicitly rejects the wrong triplet via the gray
sentinel fallback.
- Brent color inverted from a default market panel: oil price UP = red
(bad for energy importers, the dominant Atlas reader); DOWN = green.
- Crisis-day counter sourced from VITE_HORMUZ_CRISIS_START_DATE env
(default 2026-02-23). NaN/future-dated values handled with explicit
'—' / 'pending' sentinels so the tile never renders 'Day NaN'.
- 60s setInterval re-renders the freshness tile only — no new RPCs fire
on tick. setInterval cleared in destroy() so panel teardown is clean.
- Tests: 24 in tests/energy-risk-overview-panel.test.mts cover Hormuz
color enum (including the wrong-triplet rejection), EU gas thresholds,
Brent inversion, active-disruption color bands, freshness label
formatting, crisis-day counter (today/5-days/NaN/future), and the
degraded-mode contract (all-fail still renders 6 tiles with 4 marked
data-degraded).
U6 — Wiring (5 sites per skill panel-stuck-loading-means-missing-primetask):
- src/components/index.ts: barrel export
- src/app/panel-layout.ts: import + createPanel('energy-risk-overview', ...)
- src/config/panels.ts: priority-1 entry in ENERGY_PANELS (top-of-grid),
priority-2 entry in FULL_PANELS (CMD+K-discoverable, default disabled),
panelKey added to PANEL_CATEGORY_MAP marketsFinance category
- src/App.ts: import type + primeTask kickoff (between energy-disruptions
and climate-news in the existing ordering convention)
- src/config/commands.ts: panel:energy-risk-overview command with keywords
for 'risk overview', 'executive overview', 'hormuz status', 'crisis day'
No new RPCs (preserves agent-native parity — every metric the panel shows
is already exposed via existing Connect-RPC handlers and bootstrap-cache
keys; agents can answer the same questions through the same surface).
Tests: typecheck clean; 24 unit tests pass on the panel's pure helpers.
Manual visual QA pending PR merge + deploy.
Plan section §M effort estimate: ~1.5d. Codex-approved through 8 review
rounds against origin/main @ 050073354.
* fix(energy-atlas): extract Risk Overview state-builder + real component test (PR2 review)
P2 — tests duplicated helper logic instead of testing the real panel
(energy-risk-overview-panel.test.mts:10):
- The original tests pinned color/threshold helpers but didn't import
the panel's actual state-building logic, so the panel could ship
with a broken Promise.allSettled wiring while the tests stayed green.
Refactor:
- Extract the state-building logic into a NEW Vite-free module:
src/components/_energy-risk-overview-state.ts. Exports
buildOverviewState(hormuz, euGas, brent, disruptions, now) and a
countDegradedTiles() helper for tests.
- The panel now imports and calls buildOverviewState() directly inside
fetchData(); no logic duplication. The Hormuz tile renderer narrows
status with an explicit cast at use site.
- Why a new module: the panel transitively imports `import.meta.glob`
via the i18n service, which doesn't resolve under node:test even
with tsx loader. Extracting the testable logic into a
Vite-dependency-free module is the cleanest way to exercise the
production code from tests, per skill panel-stuck-loading-means-
missing-primetask's emphasis on "test the actual production logic,
not a copy-paste of it".
Tests added (11 real-component cases via the new module):
- All four sources fulfilled → 0 degraded.
- All four sources rejected → 4 degraded, no throw, no cascade.
- Mixed (1 fulfilled, 3 rejected) → only one tile populated.
- euGas with `unavailable: true` sentinel → degraded.
- euGas with fillPct=0 → degraded (treats as no-data, not "0% red").
- brent empty data array → degraded.
- brent first-quote price=null → degraded.
- disruptions upstreamUnavailable=true → degraded.
- disruptions ongoing filter: counts only endAt-falsy events.
- Malformed hormuz response (missing status field) → degraded sentinel.
- One rejected source MUST NOT cascade to fulfilled siblings (the
core degraded-mode contract — pinned explicitly).
Total: 35 tests in this file (was 24; +11 real-component cases).
typecheck clean.
* fix(energy-atlas): server-side disruptions filter + once-only style + panel name parity (PR2 review)
Three Greptile P2 findings on PR #3398:
- listEnergyDisruptions called with ongoingOnly:true so the server filters
the historical 52-event payload server-side. The state builder still
re-filters as defense-in-depth.
- RISK_OVERVIEW_CSS injected once into <head> via injectRiskOverviewStylesOnce
instead of being emitted into setContent on every render. The 60s freshness
setInterval was tearing out and re-inserting the style tag every minute.
- FULL_PANELS entry renamed from "Energy Risk Overview" to
"Global Energy Risk Overview" to match ENERGY_PANELS and the CMD+K command.