feat(energy): days of cover global view (Phase 4 PR B) (#2767)

* feat(energy): days of cover analysis key + EnergyComplexPanel oil stocks section

- seed-iea-oil-stocks.mjs exports buildOilStocksAnalysis and writes
  energy:oil-stocks-analysis:v1 via afterPublish hook after main index
- Rankings sorted by daysOfCover desc (net-exporters last), vsObligation,
  obligationMet, regional summaries (Europe/Asia-Pacific/North America)
- EnergyComplexPanel.setOilStocksAnalysis() renders IEA member table with
  below-obligation badges, rank, days vs 90d obligation, regional summary rows
- Health monitoring: seed-meta:energy:oil-stocks-analysis (42d maxStaleMin)
- Gateway cache tier: static (monthly seed data)
- 13 new tests covering sorting, exclusions, regional rollups, obligation logic

* feat(energy): add proto + regenerate service for oil stocks analysis RPC

- Add get_oil_stocks_analysis.proto with OilStocksAnalysisMember,
  OilStocksRegionalSummary sub-messages, and GetOilStocksAnalysisResponse
- Use proto3 optional fields for nullable int32 (daysOfCover, vsObligation,
  avgDays, minDays) avoiding google.protobuf.wrappers complexity
- Regenerate service_client.ts + service_server.ts via make generate
- Update handler fallback and panel null-safety guards for optional fields
- Regenerated OpenAPI docs include getOilStocksAnalysis endpoint

* fix(energy): preserve oil-stocks-analysis TTL via extraKeys; fix seed-meta TTL to exceed health threshold

- Move ANALYSIS_KEY into ANALYSIS_EXTRA_KEY in extraKeys so runSeed() extends
  its TTL on fetch failure or validation skip (was only written in afterPublish,
  leaving the key unprotected on the sad path)
- afterPublish now writes only the seed-meta for ANALYSIS_KEY with a 50-day TTL
  (Math.max(86400*50, TTL_SECONDS)) — exceeds the health maxStaleMin threshold
- Add optional metaTtlSeconds param to writeExtraKeyWithMeta() (backward-compat,
  defaults to existing 7-day value for all other callers)
- Update health.js oilStocksAnalysis maxStaleMin from 42d to 50d to stay below
  the new seed-meta TTL and avoid false stale/missing reports

* fix(energy): preserve seed-meta:oil-stocks-analysis TTL via extraKeys on seeder failure
This commit is contained in:
Elie Habib
2026-04-06 16:28:04 +04:00
committed by GitHub
parent cdd8e6edda
commit e0dc630ed5
16 changed files with 771 additions and 7 deletions

View File

@@ -728,6 +728,32 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error'
/api/economic/v1/get-oil-stocks-analysis:
get:
tags:
- EconomicService
summary: GetOilStocksAnalysis
description: GetOilStocksAnalysis retrieves the IEA oil stocks days-of-cover ranking and regional summary.
operationId: GetOilStocksAnalysis
responses:
"200":
description: Successful response
content:
application/json:
schema:
$ref: '#/components/schemas/GetOilStocksAnalysisResponse'
"400":
description: Validation error
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
default:
description: Error response
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
components:
schemas:
Error:
@@ -1988,3 +2014,112 @@ components:
sugar:
type: number
format: double
GetOilStocksAnalysisRequest:
type: object
description: GetOilStocksAnalysisRequest is empty — returns the latest global analysis snapshot.
GetOilStocksAnalysisResponse:
type: object
properties:
updatedAt:
type: string
description: UTC ISO-8601 timestamp when this analysis was written.
dataMonth:
type: string
description: 'Data month in YYYY-MM format (source: IEA monthly release).'
ieaMembers:
type: array
items:
$ref: '#/components/schemas/OilStocksAnalysisMember'
belowObligation:
type: array
items:
type: string
description: ISO2 codes of countries currently below the 90-day obligation.
regionalSummary:
$ref: '#/components/schemas/OilStocksRegionalSummary'
unavailable:
type: boolean
description: True when upstream seed data is unavailable (fallback result).
description: GetOilStocksAnalysisResponse contains the IEA oil stocks days-of-cover analysis.
OilStocksAnalysisMember:
type: object
properties:
iso2:
type: string
description: ISO 3166-1 alpha-2 country code.
daysOfCover:
type: integer
format: int32
description: Days of supply cover (absent when country is a net exporter or data anomaly).
netExporter:
type: boolean
description: True when the country is classified as a net exporter by IEA.
belowObligation:
type: boolean
description: True when days_of_cover < 90 (IEA 90-day obligation threshold).
obligationMet:
type: boolean
description: True when the 90-day obligation is met (net exporters always true).
rank:
type: integer
format: int32
description: Rank within the IEA member ranking (1-indexed, net exporters ranked last).
vsObligation:
type: integer
format: int32
description: days_of_cover - 90; absent for net exporters.
description: OilStocksAnalysisMember holds days-of-cover data and obligation status for one IEA member country.
OilStocksRegionalSummary:
type: object
properties:
europe:
$ref: '#/components/schemas/OilStocksRegionalSummaryEurope'
asiaPacific:
$ref: '#/components/schemas/OilStocksRegionalSummaryAsiaPacific'
northAmerica:
$ref: '#/components/schemas/OilStocksRegionalSummaryNorthAmerica'
description: OilStocksRegionalSummary holds regional aggregates for the three IEA regions.
OilStocksRegionalSummaryEurope:
type: object
properties:
avgDays:
type: integer
format: int32
description: Mean days of cover across non-net-exporter European members.
minDays:
type: integer
format: int32
description: Minimum days of cover across non-net-exporter European members.
countBelowObligation:
type: integer
format: int32
description: Count of European members below the 90-day obligation.
description: OilStocksRegionalSummaryEurope aggregates days-of-cover for European IEA members.
OilStocksRegionalSummaryAsiaPacific:
type: object
properties:
avgDays:
type: integer
format: int32
description: Mean days of cover across Asia-Pacific members (AU, JP, KR, NZ).
minDays:
type: integer
format: int32
description: Minimum days of cover across Asia-Pacific members.
countBelowObligation:
type: integer
format: int32
description: Count of Asia-Pacific members below the 90-day obligation.
description: OilStocksRegionalSummaryAsiaPacific aggregates days-of-cover for Asia-Pacific IEA members.
OilStocksRegionalSummaryNorthAmerica:
type: object
properties:
netExporters:
type: integer
format: int32
description: Count of net exporters in North America (CA, MX, US).
avgDays:
type: integer
format: int32
description: Average days of cover for non-exporter North American members (if any).
description: OilStocksRegionalSummaryNorthAmerica aggregates data for North American IEA members.