mirror of
https://github.com/koala73/worldmonitor.git
synced 2026-04-25 17:14:57 +02:00
* fix(supply-chain): split chokepoint transit data + close silent zero-state cache
Production supply-chain panel was rendering 13 empty chokepoints because
the getChokepointStatus RPC silently cached zero-state for 5 minutes:
1. supply_chain:transit-summaries:v1 grew to ~500 KB (180d × 13 × 14 fields
of history per chokepoint).
2. REDIS_OP_TIMEOUT_MS is 1.5 s. Vercel Sydney edge → Upstash for a 500 KB
GET consistently exceeded the budget; getCachedJson caught the AbortError
and returned null.
3. The 500 KB portwatch fallback read hit the same timeout.
4. summaries = {} → every summaries[cp.id] was undefined → 13 chokepoints
got the zero-state default → cached as a non-null success response for
REDIS_CACHE_TTL (5 min) instead of NEG_SENTINEL (120 s).
Fix (one PR, per docs/plans/chokepoint-rpc-payload-split.md):
- ais-relay.cjs: split seedTransitSummaries output.
- supply_chain:transit-summaries:v1 — compact (~30 KB, no history).
- supply_chain:transit-summaries:history:v1:{id} — per chokepoint
(~35 KB each, 13 keys). Both under the 1.5 s Redis read budget.
- New RPC GetChokepointHistory: lazy-loaded on card expand.
- get-chokepoint-status.ts: drop the 500 KB portwatch/corridorrisk/
chokepoint_transits fallback reads. Treat a null transit-summaries
read as upstreamUnavailable=true so cachedFetchJson writes NEG_SENTINEL
(2 min) instead of a 5-min zero-state pin. Omit history from the
response (proto field stays declared; empty array).
- server/_shared/redis.ts: tag AbortError timeouts with [REDIS-TIMEOUT]
key=… timeoutMs=… so log drains / Sentry-Vercel integration pick up
large-payload timeouts instead of them being silently swallowed.
- SupplyChainPanel.ts + MapPopup.ts: lazy-fetch history on card expand
via fetchChokepointHistory; session-scoped cache; graceful "History
unavailable" on empty/error. PRO gating on the map popup unchanged.
- Gateway: cache-tier entry for /get-chokepoint-history (slow).
- Tests: regression guards for upstreamUnavailable gate + per-id key
shape + handler wiring + proto query annotations.
Audit included in plan: no other RPC consumer read stacks >200 KB
besides displacement:summary:v1:2026 (724 KB, same risk, flagged for
follow-up PR). wildfire:fires:v1 at 1.7 MB loads via bootstrap (3 s
timeout, different path) — monitor but out of scope.
Expected impact:
- supply_chain:chokepoints:v4 payload drops from ~508 KB to <100 KB.
- supply_chain:transit-summaries:v1 drops from ~502 KB to <50 KB.
- RPC Redis reads stay well under 1.5 s in the hot path.
- Silent zero-state pinning is now impossible: null reads → 2-min neg
cache → self-heal on next relay tick.
* fix(supply-chain): address PR #3185 review — stop caching empty/error + fix partial coverage
Two P1 regressions caught in review:
1. Client cache poisoning on empty/error (MapPopup.ts, SupplyChainPanel.ts)
Empty-array is truthy in JS, so MapPopup's `!cached && !inflight` branch
never fired once we cached []. Neither `cached && cached.length` fired
either — popup stuck on "Loading transit history..." for the session.
SupplyChainPanel had the explicit `cached && !cached.length` branch but
still never retried, so the same transient became session-sticky there too.
Fix: cache ONLY non-empty successful responses. Empty/error show the
"History unavailable" placeholder but leave the cache untouched, so the
next re-expand retries. The /get-chokepoint-history gateway tier is
"slow" (5-min CF edge cache) → retries stay cheap.
2. Partial portwatch coverage treated as healthy (ais-relay.cjs)
seedTransitSummaries iterated Object.entries(pw), so if seed-portwatch
dropped N of 13 chokepoints (ArcGIS reject/empty), summaries had <13 keys.
get-chokepoint-status upstreamUnavailable fires only on fully-empty
summaries, so the N missing chokepoints fell through to zero-state rows
that got pinned in cache for 5 minutes.
Fix: iterate CANONICAL_IDS (Object.keys(CHOKEPOINT_THREAT_LEVELS)) and
fill zero-state for any ID missing from pw. Shape is consistently 13
keys. Track pwCovered → envelope + seed-meta recordCount reflect real
upstream coverage (not shape size), so health.js can distinguish 13/13
healthy from 10/13 partial. Warn-log on shortfall.
Tests: new regression guards
- panel must NOT cache empty arrays (historyCache.set with []).
- writer must iterate CANONICAL_IDS, not Object.entries(pw).
- seed-meta recordCount binds to pwCovered.
5718/5718 data tests pass. typecheck + typecheck:api clean.
1234 lines
47 KiB
YAML
1234 lines
47 KiB
YAML
openapi: 3.1.0
|
||
info:
|
||
title: SupplyChainService API
|
||
version: 1.0.0
|
||
paths:
|
||
/api/supply-chain/v1/get-shipping-rates:
|
||
get:
|
||
tags:
|
||
- SupplyChainService
|
||
summary: GetShippingRates
|
||
operationId: GetShippingRates
|
||
responses:
|
||
"200":
|
||
description: Successful response
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/GetShippingRatesResponse'
|
||
"400":
|
||
description: Validation error
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/ValidationError'
|
||
default:
|
||
description: Error response
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/Error'
|
||
/api/supply-chain/v1/get-chokepoint-status:
|
||
get:
|
||
tags:
|
||
- SupplyChainService
|
||
summary: GetChokepointStatus
|
||
operationId: GetChokepointStatus
|
||
responses:
|
||
"200":
|
||
description: Successful response
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/GetChokepointStatusResponse'
|
||
"400":
|
||
description: Validation error
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/ValidationError'
|
||
default:
|
||
description: Error response
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/Error'
|
||
/api/supply-chain/v1/get-chokepoint-history:
|
||
get:
|
||
tags:
|
||
- SupplyChainService
|
||
summary: GetChokepointHistory
|
||
description: |-
|
||
GetChokepointHistory returns transit-count history for a single chokepoint,
|
||
loaded lazily on card expand. Keeps the status RPC compact (no 180-day
|
||
history per chokepoint on every call).
|
||
operationId: GetChokepointHistory
|
||
parameters:
|
||
- name: chokepointId
|
||
in: query
|
||
required: false
|
||
schema:
|
||
type: string
|
||
responses:
|
||
"200":
|
||
description: Successful response
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/GetChokepointHistoryResponse'
|
||
"400":
|
||
description: Validation error
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/ValidationError'
|
||
default:
|
||
description: Error response
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/Error'
|
||
/api/supply-chain/v1/get-critical-minerals:
|
||
get:
|
||
tags:
|
||
- SupplyChainService
|
||
summary: GetCriticalMinerals
|
||
operationId: GetCriticalMinerals
|
||
responses:
|
||
"200":
|
||
description: Successful response
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/GetCriticalMineralsResponse'
|
||
"400":
|
||
description: Validation error
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/ValidationError'
|
||
default:
|
||
description: Error response
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/Error'
|
||
/api/supply-chain/v1/get-shipping-stress:
|
||
get:
|
||
tags:
|
||
- SupplyChainService
|
||
summary: GetShippingStress
|
||
description: GetShippingStress returns carrier market data and a composite stress index.
|
||
operationId: GetShippingStress
|
||
responses:
|
||
"200":
|
||
description: Successful response
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/GetShippingStressResponse'
|
||
"400":
|
||
description: Validation error
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/ValidationError'
|
||
default:
|
||
description: Error response
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/Error'
|
||
/api/supply-chain/v1/get-country-chokepoint-index:
|
||
get:
|
||
tags:
|
||
- SupplyChainService
|
||
summary: GetCountryChokepointIndex
|
||
description: GetCountryChokepointIndex returns per-chokepoint exposure scores for a country. PRO-gated.
|
||
operationId: GetCountryChokepointIndex
|
||
parameters:
|
||
- name: iso2
|
||
in: query
|
||
description: ISO 3166-1 alpha-2 country code (uppercase).
|
||
required: false
|
||
schema:
|
||
type: string
|
||
- name: hs2
|
||
in: query
|
||
description: HS2 chapter (2-digit string). Defaults to "27" (energy/mineral fuels) when absent.
|
||
required: false
|
||
schema:
|
||
type: string
|
||
responses:
|
||
"200":
|
||
description: Successful response
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/GetCountryChokepointIndexResponse'
|
||
"400":
|
||
description: Validation error
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/ValidationError'
|
||
default:
|
||
description: Error response
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/Error'
|
||
/api/supply-chain/v1/get-bypass-options:
|
||
get:
|
||
tags:
|
||
- SupplyChainService
|
||
summary: GetBypassOptions
|
||
description: GetBypassOptions returns ranked bypass corridors for a chokepoint. PRO-gated.
|
||
operationId: GetBypassOptions
|
||
parameters:
|
||
- name: chokepointId
|
||
in: query
|
||
required: false
|
||
schema:
|
||
type: string
|
||
- name: cargoType
|
||
in: query
|
||
description: 'container | tanker | bulk | roro (default: "container")'
|
||
required: false
|
||
schema:
|
||
type: string
|
||
- name: closurePct
|
||
in: query
|
||
description: '0-100, percent of capacity blocked (default: 100)'
|
||
required: false
|
||
schema:
|
||
type: integer
|
||
format: int32
|
||
responses:
|
||
"200":
|
||
description: Successful response
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/GetBypassOptionsResponse'
|
||
"400":
|
||
description: Validation error
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/ValidationError'
|
||
default:
|
||
description: Error response
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/Error'
|
||
/api/supply-chain/v1/get-country-cost-shock:
|
||
get:
|
||
tags:
|
||
- SupplyChainService
|
||
summary: GetCountryCostShock
|
||
description: GetCountryCostShock returns cost shock and war risk data for a country+chokepoint. PRO-gated.
|
||
operationId: GetCountryCostShock
|
||
parameters:
|
||
- name: iso2
|
||
in: query
|
||
required: false
|
||
schema:
|
||
type: string
|
||
- name: chokepointId
|
||
in: query
|
||
required: false
|
||
schema:
|
||
type: string
|
||
- name: hs2
|
||
in: query
|
||
description: 'HS2 chapter (default: "27")'
|
||
required: false
|
||
schema:
|
||
type: string
|
||
responses:
|
||
"200":
|
||
description: Successful response
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/GetCountryCostShockResponse'
|
||
"400":
|
||
description: Validation error
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/ValidationError'
|
||
default:
|
||
description: Error response
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/Error'
|
||
/api/supply-chain/v1/get-sector-dependency:
|
||
get:
|
||
tags:
|
||
- SupplyChainService
|
||
summary: GetSectorDependency
|
||
description: GetSectorDependency returns dependency flags and risk profile for a country+HS2 sector. PRO-gated.
|
||
operationId: GetSectorDependency
|
||
parameters:
|
||
- name: iso2
|
||
in: query
|
||
required: false
|
||
schema:
|
||
type: string
|
||
- name: hs2
|
||
in: query
|
||
description: HS2 chapter code, e.g. "27" (mineral fuels), "85" (electronics)
|
||
required: false
|
||
schema:
|
||
type: string
|
||
responses:
|
||
"200":
|
||
description: Successful response
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/GetSectorDependencyResponse'
|
||
"400":
|
||
description: Validation error
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/ValidationError'
|
||
default:
|
||
description: Error response
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/Error'
|
||
/api/supply-chain/v1/get-route-explorer-lane:
|
||
get:
|
||
tags:
|
||
- SupplyChainService
|
||
summary: GetRouteExplorerLane
|
||
description: |-
|
||
GetRouteExplorerLane returns the primary maritime route, chokepoint exposures,
|
||
bypass options with geometry, war risk, and static transit/freight estimates for
|
||
a country pair + HS2 + cargo type. PRO-gated. Wraps the route-intelligence vendor
|
||
endpoint's compute with browser-callable auth and adds fields needed by the
|
||
Route Explorer UI.
|
||
operationId: GetRouteExplorerLane
|
||
parameters:
|
||
- name: fromIso2
|
||
in: query
|
||
required: false
|
||
schema:
|
||
type: string
|
||
- name: toIso2
|
||
in: query
|
||
required: false
|
||
schema:
|
||
type: string
|
||
- name: hs2
|
||
in: query
|
||
description: HS2 chapter code, e.g. "27", "85"
|
||
required: false
|
||
schema:
|
||
type: string
|
||
- name: cargoType
|
||
in: query
|
||
description: 'One of: container, tanker, bulk, roro'
|
||
required: false
|
||
schema:
|
||
type: string
|
||
responses:
|
||
"200":
|
||
description: Successful response
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/GetRouteExplorerLaneResponse'
|
||
"400":
|
||
description: Validation error
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/ValidationError'
|
||
default:
|
||
description: Error response
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/Error'
|
||
/api/supply-chain/v1/get-route-impact:
|
||
get:
|
||
tags:
|
||
- SupplyChainService
|
||
summary: GetRouteImpact
|
||
operationId: GetRouteImpact
|
||
parameters:
|
||
- name: fromIso2
|
||
in: query
|
||
required: false
|
||
schema:
|
||
type: string
|
||
- name: toIso2
|
||
in: query
|
||
required: false
|
||
schema:
|
||
type: string
|
||
- name: hs2
|
||
in: query
|
||
required: false
|
||
schema:
|
||
type: string
|
||
responses:
|
||
"200":
|
||
description: Successful response
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/GetRouteImpactResponse'
|
||
"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:
|
||
type: object
|
||
properties:
|
||
message:
|
||
type: string
|
||
description: Error message (e.g., 'user not found', 'database connection failed')
|
||
description: Error is returned when a handler encounters an error. It contains a simple error message that the developer can customize.
|
||
FieldViolation:
|
||
type: object
|
||
properties:
|
||
field:
|
||
type: string
|
||
description: The field path that failed validation (e.g., 'user.email' for nested fields). For header validation, this will be the header name (e.g., 'X-API-Key')
|
||
description:
|
||
type: string
|
||
description: Human-readable description of the validation violation (e.g., 'must be a valid email address', 'required field missing')
|
||
required:
|
||
- field
|
||
- description
|
||
description: FieldViolation describes a single validation error for a specific field.
|
||
ValidationError:
|
||
type: object
|
||
properties:
|
||
violations:
|
||
type: array
|
||
items:
|
||
$ref: '#/components/schemas/FieldViolation'
|
||
description: List of validation violations
|
||
required:
|
||
- violations
|
||
description: ValidationError is returned when request validation fails. It contains a list of field violations describing what went wrong.
|
||
GetShippingRatesRequest:
|
||
type: object
|
||
GetShippingRatesResponse:
|
||
type: object
|
||
properties:
|
||
indices:
|
||
type: array
|
||
items:
|
||
$ref: '#/components/schemas/ShippingIndex'
|
||
fetchedAt:
|
||
type: string
|
||
upstreamUnavailable:
|
||
type: boolean
|
||
ShippingIndex:
|
||
type: object
|
||
properties:
|
||
indexId:
|
||
type: string
|
||
name:
|
||
type: string
|
||
currentValue:
|
||
type: number
|
||
format: double
|
||
previousValue:
|
||
type: number
|
||
format: double
|
||
changePct:
|
||
type: number
|
||
format: double
|
||
unit:
|
||
type: string
|
||
history:
|
||
type: array
|
||
items:
|
||
$ref: '#/components/schemas/ShippingRatePoint'
|
||
spikeAlert:
|
||
type: boolean
|
||
ShippingRatePoint:
|
||
type: object
|
||
properties:
|
||
date:
|
||
type: string
|
||
value:
|
||
type: number
|
||
format: double
|
||
GetChokepointStatusRequest:
|
||
type: object
|
||
GetChokepointStatusResponse:
|
||
type: object
|
||
properties:
|
||
chokepoints:
|
||
type: array
|
||
items:
|
||
$ref: '#/components/schemas/ChokepointInfo'
|
||
fetchedAt:
|
||
type: string
|
||
upstreamUnavailable:
|
||
type: boolean
|
||
ChokepointInfo:
|
||
type: object
|
||
properties:
|
||
id:
|
||
type: string
|
||
name:
|
||
type: string
|
||
lat:
|
||
type: number
|
||
format: double
|
||
lon:
|
||
type: number
|
||
format: double
|
||
disruptionScore:
|
||
type: integer
|
||
format: int32
|
||
status:
|
||
type: string
|
||
activeWarnings:
|
||
type: integer
|
||
format: int32
|
||
congestionLevel:
|
||
type: string
|
||
affectedRoutes:
|
||
type: array
|
||
items:
|
||
type: string
|
||
description:
|
||
type: string
|
||
aisDisruptions:
|
||
type: integer
|
||
format: int32
|
||
directions:
|
||
type: array
|
||
items:
|
||
type: string
|
||
directionalDwt:
|
||
type: array
|
||
items:
|
||
$ref: '#/components/schemas/DirectionalDwt'
|
||
transitSummary:
|
||
$ref: '#/components/schemas/TransitSummary'
|
||
flowEstimate:
|
||
$ref: '#/components/schemas/FlowEstimate'
|
||
warRiskTier:
|
||
type: string
|
||
enum:
|
||
- WAR_RISK_TIER_UNSPECIFIED
|
||
- WAR_RISK_TIER_NORMAL
|
||
- WAR_RISK_TIER_ELEVATED
|
||
- WAR_RISK_TIER_HIGH
|
||
- WAR_RISK_TIER_CRITICAL
|
||
- WAR_RISK_TIER_WAR_ZONE
|
||
description: |-
|
||
*
|
||
War risk tier derived from Lloyd's JWC Listed Areas + OSINT threat classification.
|
||
This is a FREE field (no PRO gate) — it exposes the existing server-internal
|
||
threatLevel from ChokepointConfig, making it available to clients for badges
|
||
and bypass corridor scoring.
|
||
DirectionalDwt:
|
||
type: object
|
||
properties:
|
||
direction:
|
||
type: string
|
||
dwtThousandTonnes:
|
||
type: number
|
||
format: double
|
||
wowChangePct:
|
||
type: number
|
||
format: double
|
||
TransitSummary:
|
||
type: object
|
||
properties:
|
||
todayTotal:
|
||
type: integer
|
||
format: int32
|
||
todayTanker:
|
||
type: integer
|
||
format: int32
|
||
todayCargo:
|
||
type: integer
|
||
format: int32
|
||
todayOther:
|
||
type: integer
|
||
format: int32
|
||
wowChangePct:
|
||
type: number
|
||
format: double
|
||
history:
|
||
type: array
|
||
items:
|
||
$ref: '#/components/schemas/TransitDayCount'
|
||
riskLevel:
|
||
type: string
|
||
incidentCount7d:
|
||
type: integer
|
||
format: int32
|
||
disruptionPct:
|
||
type: number
|
||
format: double
|
||
riskSummary:
|
||
type: string
|
||
riskReportAction:
|
||
type: string
|
||
TransitDayCount:
|
||
type: object
|
||
properties:
|
||
date:
|
||
type: string
|
||
tanker:
|
||
type: integer
|
||
format: int32
|
||
cargo:
|
||
type: integer
|
||
format: int32
|
||
other:
|
||
type: integer
|
||
format: int32
|
||
total:
|
||
type: integer
|
||
format: int32
|
||
container:
|
||
type: integer
|
||
format: int32
|
||
dryBulk:
|
||
type: integer
|
||
format: int32
|
||
generalCargo:
|
||
type: integer
|
||
format: int32
|
||
roro:
|
||
type: integer
|
||
format: int32
|
||
capContainer:
|
||
type: number
|
||
format: double
|
||
capDryBulk:
|
||
type: number
|
||
format: double
|
||
capGeneralCargo:
|
||
type: number
|
||
format: double
|
||
capRoro:
|
||
type: number
|
||
format: double
|
||
capTanker:
|
||
type: number
|
||
format: double
|
||
FlowEstimate:
|
||
type: object
|
||
properties:
|
||
currentMbd:
|
||
type: number
|
||
format: double
|
||
baselineMbd:
|
||
type: number
|
||
format: double
|
||
flowRatio:
|
||
type: number
|
||
format: double
|
||
disrupted:
|
||
type: boolean
|
||
source:
|
||
type: string
|
||
hazardAlertLevel:
|
||
type: string
|
||
hazardAlertName:
|
||
type: string
|
||
GetChokepointHistoryRequest:
|
||
type: object
|
||
properties:
|
||
chokepointId:
|
||
type: string
|
||
required:
|
||
- chokepointId
|
||
description: |-
|
||
GetChokepointHistory returns the transit-count history for a single
|
||
chokepoint. Loaded lazily on card expand so the main chokepoint-status
|
||
response can stay compact (no 180-day history per chokepoint).
|
||
GetChokepointHistoryResponse:
|
||
type: object
|
||
properties:
|
||
chokepointId:
|
||
type: string
|
||
history:
|
||
type: array
|
||
items:
|
||
$ref: '#/components/schemas/TransitDayCount'
|
||
fetchedAt:
|
||
type: string
|
||
format: int64
|
||
GetCriticalMineralsRequest:
|
||
type: object
|
||
GetCriticalMineralsResponse:
|
||
type: object
|
||
properties:
|
||
minerals:
|
||
type: array
|
||
items:
|
||
$ref: '#/components/schemas/CriticalMineral'
|
||
fetchedAt:
|
||
type: string
|
||
upstreamUnavailable:
|
||
type: boolean
|
||
CriticalMineral:
|
||
type: object
|
||
properties:
|
||
mineral:
|
||
type: string
|
||
topProducers:
|
||
type: array
|
||
items:
|
||
$ref: '#/components/schemas/MineralProducer'
|
||
hhi:
|
||
type: number
|
||
format: double
|
||
riskRating:
|
||
type: string
|
||
globalProduction:
|
||
type: number
|
||
format: double
|
||
unit:
|
||
type: string
|
||
MineralProducer:
|
||
type: object
|
||
properties:
|
||
country:
|
||
type: string
|
||
countryCode:
|
||
type: string
|
||
productionTonnes:
|
||
type: number
|
||
format: double
|
||
sharePct:
|
||
type: number
|
||
format: double
|
||
GetShippingStressRequest:
|
||
type: object
|
||
GetShippingStressResponse:
|
||
type: object
|
||
properties:
|
||
carriers:
|
||
type: array
|
||
items:
|
||
$ref: '#/components/schemas/ShippingStressCarrier'
|
||
stressScore:
|
||
type: number
|
||
format: double
|
||
description: Composite stress score 0–100 (higher = more disruption).
|
||
stressLevel:
|
||
type: string
|
||
description: '"low" | "moderate" | "elevated" | "critical".'
|
||
fetchedAt:
|
||
type: integer
|
||
format: int64
|
||
description: 'Warning: Values > 2^53 may lose precision in JavaScript'
|
||
upstreamUnavailable:
|
||
type: boolean
|
||
description: Set to true when upstream data source is unavailable and cached data is stale.
|
||
ShippingStressCarrier:
|
||
type: object
|
||
properties:
|
||
symbol:
|
||
type: string
|
||
description: Ticker or identifier (e.g., "BDRY", "ZIM").
|
||
name:
|
||
type: string
|
||
description: Human-readable name.
|
||
price:
|
||
type: number
|
||
format: double
|
||
description: Current price.
|
||
changePct:
|
||
type: number
|
||
format: double
|
||
description: Percentage change from previous close.
|
||
carrierType:
|
||
type: string
|
||
description: 'Carrier type: "etf" | "carrier" | "index".'
|
||
sparkline:
|
||
type: array
|
||
items:
|
||
type: number
|
||
format: double
|
||
description: 30-day price sparkline.
|
||
description: ShippingStressCarrier represents market stress data for a carrier or shipping index.
|
||
GetCountryChokepointIndexRequest:
|
||
type: object
|
||
properties:
|
||
iso2:
|
||
type: string
|
||
pattern: ^[A-Z]{2}$
|
||
description: ISO 3166-1 alpha-2 country code (uppercase).
|
||
hs2:
|
||
type: string
|
||
description: HS2 chapter (2-digit string). Defaults to "27" (energy/mineral fuels) when absent.
|
||
required:
|
||
- iso2
|
||
description: GetCountryChokepointIndexRequest specifies the country and optional HS2 chapter.
|
||
GetCountryChokepointIndexResponse:
|
||
type: object
|
||
properties:
|
||
iso2:
|
||
type: string
|
||
description: ISO 3166-1 alpha-2 country code echoed from the request.
|
||
hs2:
|
||
type: string
|
||
description: HS2 chapter used for the computation.
|
||
exposures:
|
||
type: array
|
||
items:
|
||
$ref: '#/components/schemas/ChokepointExposureEntry'
|
||
primaryChokepointId:
|
||
type: string
|
||
description: Canonical ID of the chokepoint with the highest exposure score.
|
||
vulnerabilityIndex:
|
||
type: number
|
||
format: double
|
||
description: Composite vulnerability index 0–100 (weighted sum of top-3 exposures).
|
||
fetchedAt:
|
||
type: string
|
||
description: ISO timestamp of when this data was last seeded.
|
||
description: GetCountryChokepointIndexResponse returns exposure scores for all relevant chokepoints.
|
||
ChokepointExposureEntry:
|
||
type: object
|
||
properties:
|
||
chokepointId:
|
||
type: string
|
||
description: Canonical chokepoint ID from the chokepoint registry.
|
||
chokepointName:
|
||
type: string
|
||
description: Human-readable chokepoint name.
|
||
exposureScore:
|
||
type: number
|
||
format: double
|
||
description: Exposure score 0–100; higher = more dependent on this chokepoint.
|
||
coastSide:
|
||
type: string
|
||
description: Which ocean/basin side the country's ports face (atlantic, pacific, indian, med, multi, landlocked).
|
||
shockSupported:
|
||
type: boolean
|
||
description: Whether the shock model is supported for this chokepoint + hs2 combination.
|
||
description: ChokepointExposureEntry holds per-chokepoint exposure data for a country.
|
||
GetBypassOptionsRequest:
|
||
type: object
|
||
properties:
|
||
chokepointId:
|
||
type: string
|
||
cargoType:
|
||
type: string
|
||
description: 'container | tanker | bulk | roro (default: "container")'
|
||
closurePct:
|
||
type: integer
|
||
format: int32
|
||
description: '0-100, percent of capacity blocked (default: 100)'
|
||
required:
|
||
- chokepointId
|
||
GetBypassOptionsResponse:
|
||
type: object
|
||
properties:
|
||
chokepointId:
|
||
type: string
|
||
cargoType:
|
||
type: string
|
||
closurePct:
|
||
type: integer
|
||
format: int32
|
||
options:
|
||
type: array
|
||
items:
|
||
$ref: '#/components/schemas/BypassOption'
|
||
fetchedAt:
|
||
type: string
|
||
primaryChokepointWarRiskTier:
|
||
type: string
|
||
enum:
|
||
- WAR_RISK_TIER_UNSPECIFIED
|
||
- WAR_RISK_TIER_NORMAL
|
||
- WAR_RISK_TIER_ELEVATED
|
||
- WAR_RISK_TIER_HIGH
|
||
- WAR_RISK_TIER_CRITICAL
|
||
- WAR_RISK_TIER_WAR_ZONE
|
||
description: |-
|
||
*
|
||
War risk tier derived from Lloyd's JWC Listed Areas + OSINT threat classification.
|
||
This is a FREE field (no PRO gate) — it exposes the existing server-internal
|
||
threatLevel from ChokepointConfig, making it available to clients for badges
|
||
and bypass corridor scoring.
|
||
BypassOption:
|
||
type: object
|
||
properties:
|
||
id:
|
||
type: string
|
||
name:
|
||
type: string
|
||
type:
|
||
type: string
|
||
addedTransitDays:
|
||
type: integer
|
||
format: int32
|
||
addedCostMultiplier:
|
||
type: number
|
||
format: double
|
||
capacityConstraintTonnage:
|
||
type: string
|
||
format: int64
|
||
suitableCargoTypes:
|
||
type: array
|
||
items:
|
||
type: string
|
||
activationThreshold:
|
||
type: string
|
||
waypointChokepointIds:
|
||
type: array
|
||
items:
|
||
type: string
|
||
liveScore:
|
||
type: number
|
||
format: double
|
||
bypassWarRiskTier:
|
||
type: string
|
||
enum:
|
||
- WAR_RISK_TIER_UNSPECIFIED
|
||
- WAR_RISK_TIER_NORMAL
|
||
- WAR_RISK_TIER_ELEVATED
|
||
- WAR_RISK_TIER_HIGH
|
||
- WAR_RISK_TIER_CRITICAL
|
||
- WAR_RISK_TIER_WAR_ZONE
|
||
description: |-
|
||
*
|
||
War risk tier derived from Lloyd's JWC Listed Areas + OSINT threat classification.
|
||
This is a FREE field (no PRO gate) — it exposes the existing server-internal
|
||
threatLevel from ChokepointConfig, making it available to clients for badges
|
||
and bypass corridor scoring.
|
||
notes:
|
||
type: string
|
||
GetCountryCostShockRequest:
|
||
type: object
|
||
properties:
|
||
iso2:
|
||
type: string
|
||
pattern: ^[A-Z]{2}$
|
||
chokepointId:
|
||
type: string
|
||
hs2:
|
||
type: string
|
||
description: 'HS2 chapter (default: "27")'
|
||
required:
|
||
- iso2
|
||
- chokepointId
|
||
GetCountryCostShockResponse:
|
||
type: object
|
||
properties:
|
||
iso2:
|
||
type: string
|
||
chokepointId:
|
||
type: string
|
||
hs2:
|
||
type: string
|
||
supplyDeficitPct:
|
||
type: number
|
||
format: double
|
||
description: Average refined-product supply deficit % under full closure (Gasoline/Diesel/Jet fuel/LPG average; HS 27 only)
|
||
coverageDays:
|
||
type: integer
|
||
format: int32
|
||
description: Energy stockpile coverage in days (IEA data, HS 27 only; 0 for non-energy sectors or net exporters)
|
||
warRiskPremiumBps:
|
||
type: integer
|
||
format: int32
|
||
description: War risk insurance premium in basis points for this chokepoint
|
||
warRiskTier:
|
||
type: string
|
||
enum:
|
||
- WAR_RISK_TIER_UNSPECIFIED
|
||
- WAR_RISK_TIER_NORMAL
|
||
- WAR_RISK_TIER_ELEVATED
|
||
- WAR_RISK_TIER_HIGH
|
||
- WAR_RISK_TIER_CRITICAL
|
||
- WAR_RISK_TIER_WAR_ZONE
|
||
description: |-
|
||
*
|
||
War risk tier derived from Lloyd's JWC Listed Areas + OSINT threat classification.
|
||
This is a FREE field (no PRO gate) — it exposes the existing server-internal
|
||
threatLevel from ChokepointConfig, making it available to clients for badges
|
||
and bypass corridor scoring.
|
||
hasEnergyModel:
|
||
type: boolean
|
||
description: Whether supply_deficit_pct and coverage_days are modelled (true) or unavailable (false)
|
||
unavailableReason:
|
||
type: string
|
||
description: Null/unavailable explanation for non-energy sectors
|
||
fetchedAt:
|
||
type: string
|
||
GetSectorDependencyRequest:
|
||
type: object
|
||
properties:
|
||
iso2:
|
||
type: string
|
||
pattern: ^[A-Z]{2}$
|
||
hs2:
|
||
type: string
|
||
description: HS2 chapter code, e.g. "27" (mineral fuels), "85" (electronics)
|
||
required:
|
||
- iso2
|
||
- hs2
|
||
GetSectorDependencyResponse:
|
||
type: object
|
||
properties:
|
||
iso2:
|
||
type: string
|
||
hs2:
|
||
type: string
|
||
hs2Label:
|
||
type: string
|
||
description: Human-readable HS2 chapter name.
|
||
flags:
|
||
type: array
|
||
items:
|
||
type: string
|
||
enum:
|
||
- DEPENDENCY_FLAG_UNSPECIFIED
|
||
- DEPENDENCY_FLAG_SINGLE_SOURCE_CRITICAL
|
||
- DEPENDENCY_FLAG_SINGLE_CORRIDOR_CRITICAL
|
||
- DEPENDENCY_FLAG_COMPOUND_RISK
|
||
- DEPENDENCY_FLAG_DIVERSIFIABLE
|
||
description: DependencyFlag classifies how a country+sector dependency can fail.
|
||
primaryExporterIso2:
|
||
type: string
|
||
description: ISO2 of the country supplying the largest share of this sector's imports.
|
||
primaryExporterShare:
|
||
type: number
|
||
format: double
|
||
description: Share of imports from the primary exporter (0–1). 0 = no Comtrade data available.
|
||
primaryChokepointId:
|
||
type: string
|
||
description: Chokepoint ID with the highest exposure score for this country+sector.
|
||
primaryChokepointExposure:
|
||
type: number
|
||
format: double
|
||
description: Exposure score for the primary chokepoint (0–100).
|
||
hasViableBypass:
|
||
type: boolean
|
||
description: Whether at least one viable bypass corridor exists for the primary chokepoint.
|
||
fetchedAt:
|
||
type: string
|
||
GetRouteExplorerLaneRequest:
|
||
type: object
|
||
properties:
|
||
fromIso2:
|
||
type: string
|
||
pattern: ^[A-Z]{2}$
|
||
toIso2:
|
||
type: string
|
||
pattern: ^[A-Z]{2}$
|
||
hs2:
|
||
type: string
|
||
description: HS2 chapter code, e.g. "27", "85"
|
||
cargoType:
|
||
type: string
|
||
description: 'One of: container, tanker, bulk, roro'
|
||
required:
|
||
- fromIso2
|
||
- toIso2
|
||
- hs2
|
||
- cargoType
|
||
GetRouteExplorerLaneResponse:
|
||
type: object
|
||
properties:
|
||
fromIso2:
|
||
type: string
|
||
toIso2:
|
||
type: string
|
||
hs2:
|
||
type: string
|
||
cargoType:
|
||
type: string
|
||
primaryRouteId:
|
||
type: string
|
||
description: Primary trade route ID from TRADE_ROUTES config. Empty when no modeled lane.
|
||
primaryRouteGeometry:
|
||
type: array
|
||
items:
|
||
$ref: '#/components/schemas/GeoPoint'
|
||
chokepointExposures:
|
||
type: array
|
||
items:
|
||
$ref: '#/components/schemas/ChokepointExposureSummary'
|
||
bypassOptions:
|
||
type: array
|
||
items:
|
||
$ref: '#/components/schemas/BypassCorridorOption'
|
||
warRiskTier:
|
||
type: string
|
||
disruptionScore:
|
||
type: number
|
||
format: double
|
||
estTransitDaysRange:
|
||
$ref: '#/components/schemas/NumberRange'
|
||
estFreightUsdPerTeuRange:
|
||
$ref: '#/components/schemas/NumberRange'
|
||
noModeledLane:
|
||
type: boolean
|
||
description: |-
|
||
True when the wrapper fell back to the origin's first route (no shared route
|
||
between origin and destination clusters). Signals "no modeled lane" to the UI.
|
||
fetchedAt:
|
||
type: string
|
||
GeoPoint:
|
||
type: object
|
||
properties:
|
||
lon:
|
||
type: number
|
||
format: double
|
||
lat:
|
||
type: number
|
||
format: double
|
||
description: GeoPoint is a [longitude, latitude] pair.
|
||
ChokepointExposureSummary:
|
||
type: object
|
||
properties:
|
||
chokepointId:
|
||
type: string
|
||
chokepointName:
|
||
type: string
|
||
exposurePct:
|
||
type: integer
|
||
format: int32
|
||
BypassCorridorOption:
|
||
type: object
|
||
properties:
|
||
id:
|
||
type: string
|
||
name:
|
||
type: string
|
||
type:
|
||
type: string
|
||
addedTransitDays:
|
||
type: integer
|
||
format: int32
|
||
addedCostMultiplier:
|
||
type: number
|
||
format: double
|
||
warRiskTier:
|
||
type: string
|
||
status:
|
||
type: string
|
||
enum:
|
||
- CORRIDOR_STATUS_UNSPECIFIED
|
||
- CORRIDOR_STATUS_ACTIVE
|
||
- CORRIDOR_STATUS_PROPOSED
|
||
- CORRIDOR_STATUS_UNAVAILABLE
|
||
description: |-
|
||
Status of a bypass corridor for UI labeling. "active" means usable today;
|
||
"proposed" means documented but not yet built/operational; "unavailable"
|
||
means blockaded or otherwise blocked from use.
|
||
fromPort:
|
||
$ref: '#/components/schemas/GeoPoint'
|
||
toPort:
|
||
$ref: '#/components/schemas/GeoPoint'
|
||
description: |-
|
||
BypassCorridorOption is a single enriched bypass corridor for the Route Explorer UI.
|
||
Includes coordinate endpoints so the client can call MapContainer.setBypassRoutes
|
||
directly without any client-side geometry lookup.
|
||
NumberRange:
|
||
type: object
|
||
properties:
|
||
min:
|
||
type: integer
|
||
format: int32
|
||
max:
|
||
type: integer
|
||
format: int32
|
||
description: Inclusive integer range for transit days / freight USD estimates.
|
||
GetRouteImpactRequest:
|
||
type: object
|
||
properties:
|
||
fromIso2:
|
||
type: string
|
||
pattern: ^[A-Z]{2}$
|
||
toIso2:
|
||
type: string
|
||
pattern: ^[A-Z]{2}$
|
||
hs2:
|
||
type: string
|
||
required:
|
||
- fromIso2
|
||
- toIso2
|
||
- hs2
|
||
GetRouteImpactResponse:
|
||
type: object
|
||
properties:
|
||
laneValueUsd:
|
||
type: number
|
||
format: double
|
||
primaryExporterIso2:
|
||
type: string
|
||
primaryExporterShare:
|
||
type: number
|
||
format: double
|
||
topStrategicProducts:
|
||
type: array
|
||
items:
|
||
$ref: '#/components/schemas/StrategicProduct'
|
||
resilienceScore:
|
||
type: number
|
||
format: double
|
||
dependencyFlags:
|
||
type: array
|
||
items:
|
||
type: string
|
||
enum:
|
||
- DEPENDENCY_FLAG_UNSPECIFIED
|
||
- DEPENDENCY_FLAG_SINGLE_SOURCE_CRITICAL
|
||
- DEPENDENCY_FLAG_SINGLE_CORRIDOR_CRITICAL
|
||
- DEPENDENCY_FLAG_COMPOUND_RISK
|
||
- DEPENDENCY_FLAG_DIVERSIFIABLE
|
||
description: DependencyFlag classifies how a country+sector dependency can fail.
|
||
hs2InSeededUniverse:
|
||
type: boolean
|
||
comtradeSource:
|
||
type: string
|
||
fetchedAt:
|
||
type: string
|
||
StrategicProduct:
|
||
type: object
|
||
properties:
|
||
hs4:
|
||
type: string
|
||
label:
|
||
type: string
|
||
totalValueUsd:
|
||
type: number
|
||
format: double
|
||
topExporterIso2:
|
||
type: string
|
||
topExporterShare:
|
||
type: number
|
||
format: double
|
||
primaryChokepointId:
|
||
type: string
|