feat(energy-atlas): EnergyRiskOverviewPanel — executive overview tile (parity PR 2, plan U5-U6)#3398
Conversation
… (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 @ 0500733.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Greptile SummaryThis PR lands the Confidence Score: 4/5Safe to merge; findings are all P2 style/efficiency suggestions with no correctness impact All three findings are P2: an unnecessary full-history RPC payload, CSS re-injection on the freshness tick, and a panel name inconsistency between config entries. None affect runtime correctness or the degraded-mode contract. src/components/EnergyRiskOverviewPanel.ts (ongoingOnly flag and style injection), src/config/panels.ts (name inconsistency) Important Files Changed
Sequence DiagramsequenceDiagram
participant App as App.ts
participant PL as PanelLayoutManager
participant P as EnergyRiskOverviewPanel
participant H as fetchHormuzTracker
participant G as getEuGasStorageData
participant B as fetchCommodityQuotes
participant S as SupplyChainServiceClient
App->>PL: primeTask('energy-risk-overview')
PL->>P: panel.fetchData()
P->>P: Promise.allSettled([...])
par Concurrent fetches
P->>H: fetchHormuzTracker()
P->>G: getEuGasStorageData()
P->>B: fetchCommodityQuotes(BRENT_META)
P->>S: listEnergyDisruptions({ongoingOnly:false})
end
H-->>P: HormuzTrackerData | null
G-->>P: GetEuGasStorageResponse
B-->>P: ListCommodityQuotesResponse
S-->>P: DisruptionsResponse
Note over P: Each settled result → TileState (fulfilled/rejected)
P->>P: render() — 6 tiles (4 data + freshness + crisis-day)
P->>P: setInterval(render, 60s) — freshness tick only
Reviews (1): Last reviewed commit: "feat(energy-atlas): EnergyRiskOverviewPa..." | Re-trigger Greptile |
| fetchHormuzTracker(), | ||
| getEuGasStorageData(), | ||
| fetchCommodityQuotes(BRENT_META), | ||
| supplyChain.listEnergyDisruptions({ assetId: '', assetType: '', ongoingOnly: false }), |
There was a problem hiding this comment.
ongoingOnly: false fetches all historical disruptions client-side
Passing ongoingOnly: false causes the RPC to return every disruption in the database (historical + active), then filter(e => !e.endAt) counts ongoing ones client-side. Since the proto field exists for this purpose, passing ongoingOnly: true would let the server do the filtering and avoid transferring the full historical log on every panel refresh.
| supplyChain.listEnergyDisruptions({ assetId: '', assetType: '', ongoingOnly: false }), | |
| supplyChain.listEnergyDisruptions({ assetId: '', assetType: '', ongoingOnly: true }), |
| private render(): void { | ||
| const html = ` | ||
| <div class="ero-grid"> | ||
| ${this.renderHormuzTile()} | ||
| ${this.renderEuGasTile()} | ||
| ${this.renderBrentTile()} | ||
| ${this.renderActiveDisruptionsTile()} | ||
| ${this.renderFreshnessTile()} | ||
| ${this.renderCrisisDayTile()} | ||
| </div> | ||
| <style>${RISK_OVERVIEW_CSS}</style> | ||
| `; | ||
| this.setContent(html); | ||
| } |
There was a problem hiding this comment.
<style> block re-injected on every 60 s freshness tick
render() is called by setInterval every minute and always includes <style>${RISK_OVERVIEW_CSS}</style> in the setContent payload. If setContent replaces innerHTML, the style tag is torn out and re-inserted each tick, which can cause a brief FOUC as well as unnecessary style recalculation. Moving the CSS injection to the constructor (or to a one-time injectPanelStyle helper like other panels use) would keep the hot render path style-free.
| 'storage-facility-map': { name: 'Strategic Storage Atlas', enabled: true, priority: 2 }, | ||
| 'fuel-shortages': { name: 'Global Fuel Shortage Registry', enabled: true, priority: 2 }, | ||
| 'energy-disruptions': { name: 'Energy Disruptions Log', enabled: true, priority: 2 }, | ||
| 'energy-risk-overview': { name: 'Energy Risk Overview', enabled: false, priority: 2 }, |
There was a problem hiding this comment.
Panel name inconsistency between
FULL_PANELS and ENERGY_PANELS
FULL_PANELS registers the panel as "Energy Risk Overview", while ENERGY_PANELS (line 931) and commands.ts both use "Global Energy Risk Overview". A user browsing the panel picker in the full variant would see the shorter name, while the energy variant and CMD+K show the longer one. Aligning them keeps the UI consistent.
| 'energy-risk-overview': { name: 'Energy Risk Overview', enabled: false, priority: 2 }, | |
| 'energy-risk-overview': { name: 'Global Energy Risk Overview', enabled: false, priority: 2 }, |
…nt 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.
… 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.
Summary
Second of three sequential parity-push PRs. Lands the consolidated executive overview surface — the "first fold" tile peer reference energy-intel sites use to anchor the energy variant. Six tiles in a single panel with degraded-mode rendering; failed sources show '—' and don't block the others.
Plan: `docs/plans/2026-04-25-003-feat-energy-parity-pushup-plan.md` PR 2 (U5–U6).
Tiles
Degraded-mode contract
`Promise.allSettled`, never `Promise.all`. One slow or failing source CANNOT freeze the panel; failed tiles render `—` with `data-degraded="true"` for QA inspection. This is the single most important behavior — guards against the recurrence of the #3386 panel-stuck-on-Loading bug.
Wiring (5 sites per skill panel-stuck-loading-means-missing-primetask)
What's NOT in scope
Test plan
Codex review
Approved through 8 rounds against `origin/main @ 0500733` as part of plan review.
Sequence