mirror of
https://github.com/koala73/worldmonitor.git
synced 2026-05-05 06:41:59 +02:00
feat(route-explorer): Sprint 4 — strategic-product impact tab + get-route-impact RPC (#2996)
* feat(route-explorer): Sprint 4 — strategic-product impact tab
Adds the Impact tab to the Route Explorer, powered by a new
get-route-impact RPC that returns strategic-product trade data for
any country pair.
Backend:
- New proto get_route_impact.proto with GetRouteImpact{Request,Response}
+ StrategicProduct message
- New handler server/worldmonitor/supply-chain/v1/get-route-impact.ts:
reads comtrade:bilateral-hs4:{iso2}:v1 store, computes lane value for
selected HS2, top 5 strategic products by value with chokepoint
exposure, resilience score (server-side from Redis), dependency flags
- Cache key ROUTE_IMPACT_KEY in cache-keys.ts (NOT in BOOTSTRAP_KEYS)
- Gateway + premium-paths registered as slow-browser premium RPC
- Client wrapper fetchRouteImpact in supply-chain/index.ts
Impact tab UI:
- CountryImpactTab.ts: strategic products table (top 5 by value),
lane value card for selected HS2, hs2InSeededUniverse banner when
HS2 is not in the 14 seeded sectors, comtradeSource states
(missing/empty/bilateral-hs4), drill-sideways on product row click
- LeftRail.updateDependencyFlags: renders flags from Impact response
with color-coded badges (compound_risk/single_source/diversifiable)
Data flow:
- fetchImpact fires in parallel with fetchResilience after lane data
loads, generation-scoped
- Impact response updates left-rail flags + resilience score
- Drill-sideways: clicking a product row switches the explorer's HS2
and re-queries all tabs
Server-side resilience:
- get-route-impact reads resilience:score:v8:{iso2} from Redis directly
so the data is available for future email briefs without client calls
Plan: docs/plans/2026-04-11-001-feat-worldwide-route-explorer-plan.md
* fix(route-explorer): real exposure score for flags + tabstrip sync on drill
P1: computeDependencyFlags hardcoded primaryExposure=80 whenever any
chokepoint existed, fabricating SINGLE_CORRIDOR_CRITICAL without using
real exposure data. Replaced with computeRealExposureScore that uses the
same route-cluster overlap logic as get-sector-dependency, computing the
actual exposure percentage before comparing against the >80 threshold.
P2: handleDrillSideways set state.tab=1 directly without going through
setTab(), leaving the tabstrip visually and semantically on Impact while
content showed Current. Now calls setTab(1) which updates both the
tabstrip active state and aria-selected.
* fix(route-explorer): guard resilience overwrite + normalize HS2 filter
P1: fetchImpact could zero the left-rail resilience score when
get-route-impact returned resilienceScore=0 (Redis miss fallback),
overwriting a valid score set by the concurrent fetchResilience call.
Now only applies the server-side score when it is actually > 0.
P2: HS4-to-HS2 matching used a redundant dual-condition filter
(hs4ToHs2 + startsWith) that masked a potential normalization bug.
Simplified to normalize hs2 once via parseInt then use a single
hs4ToHs2 comparison.
This commit is contained in:
@@ -323,6 +323,47 @@ paths:
|
||||
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:
|
||||
@@ -1065,3 +1106,70 @@ components:
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user