mirror of
https://github.com/koala73/worldmonitor.git
synced 2026-04-25 17:14:57 +02:00
* fix(country-brief): display bugs — slugs, self-imports, N/A, HS labels (#2970) Six user-facing display fixes that individually looked minor but together eroded trust in the Country Brief panel. 1. Incorrect chokepoint attribution per supplier. Intra-regional pairs (e.g. Greek/Italian refined petroleum to Turkey) overlapped on long pass-through routes like gulf-europe-oil, attributing Hormuz and Bab el-Mandeb to Mediterranean trade. Added a coastSide-based filter: when exporter and importer share the same coast, transit chokepoints are restricted to a regional whitelist (e.g. med -> bosphorus, gibraltar, suez only). 2. Self-imports. Rows where partnerIso2 equals the importer ISO2 are now filtered out of Product Imports. 3. "N/A" supplier rows. Unresolved ISO2 codes (seeder emits partnerIso2 = '' when a UN code does not map) are now dropped from the render instead of surfacing as "N/A" at 14-16% share. 4. Raw slug "hormuz_strait" in shock-scenario prose. buildAssessment() now resolves chokepoint IDs to their display names ("Strait of Hormuz", "Suez Canal", etc.) via a small local map. 5. Raw ISO2 "TR can bridge" in shock-scenario prose. buildAssessment() now uses Intl.DisplayNames to render country names, with a raw-code fallback if instantiation fails. 6. HS chapter numbers instead of sector names in Cost Shock table. The empty-skeleton branch of /api/supply-chain/v1/multi-sector-cost-shock was returning hs2Label = hs2 (raw code); it now uses MULTI_SECTOR_HS2_LABELS. Frontend also adds an HS2_SHORT_LABELS fallback so the table never shows raw codes even if hs2Label is empty. All 4973 data-suite tests pass. Closes #2970. * fix(country-brief): apply supplier filter to recommendations + empty state (#2970) Address PR #3032 review (P2): - The supplier filter (drop self-imports + unmapped ISO2) only reached the table; the recommendation pane still iterated the unfiltered enriched array, so hidden rows could still produce recommendation text and safeAlternative pointers. - Build a single visibleEnriched list and use it for both the row table and the recommendation pane. - Short-circuit to an explicit "No external suppliers in available trade data" empty state when filtering removes every row, so the detail area never goes silently blank. - Skip safeAlternative suggestions that would point at filtered-out partners (self or unmapped). * test(country-brief): defensive ISO2 assertion for ICU variation (#2970) Address PR #3032 review (P2): CLDR behaviour for unrecognised 2-letter codes like 'XZ' varies across ICU versions; allow either raw 'XZ' or the resolved 'Unknown Region' form.
277 lines
10 KiB
JavaScript
277 lines
10 KiB
JavaScript
import { build } from 'esbuild';
|
|
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { dirname, join, resolve } from 'node:path';
|
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
import { createBrowserEnvironment } from './runtime-config-panel-harness.mjs';
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
const root = resolve(__dirname, '..', '..');
|
|
const entry = resolve(root, 'src/components/CountryDeepDivePanel.ts');
|
|
|
|
function snapshotGlobal(name) {
|
|
return {
|
|
exists: Object.prototype.hasOwnProperty.call(globalThis, name),
|
|
value: globalThis[name],
|
|
};
|
|
}
|
|
|
|
function restoreGlobal(name, snapshot) {
|
|
if (snapshot.exists) {
|
|
Object.defineProperty(globalThis, name, {
|
|
configurable: true,
|
|
writable: true,
|
|
value: snapshot.value,
|
|
});
|
|
return;
|
|
}
|
|
delete globalThis[name];
|
|
}
|
|
|
|
function defineGlobal(name, value) {
|
|
Object.defineProperty(globalThis, name, {
|
|
configurable: true,
|
|
writable: true,
|
|
value,
|
|
});
|
|
}
|
|
|
|
async function loadCountryDeepDivePanel() {
|
|
const tempDir = mkdtempSync(join(tmpdir(), 'wm-country-deep-dive-'));
|
|
const outfile = join(tempDir, 'CountryDeepDivePanel.bundle.mjs');
|
|
|
|
const stubModules = new Map([
|
|
['feeds-stub', `
|
|
export function getSourcePropagandaRisk() {
|
|
return { stateAffiliated: '' };
|
|
}
|
|
export function getSourceTier() {
|
|
return 2;
|
|
}
|
|
`],
|
|
['country-geometry-stub', `
|
|
export function getCountryCentroid() {
|
|
return null;
|
|
}
|
|
export const ME_STRIKE_BOUNDS = [];
|
|
`],
|
|
['i18n-stub', `
|
|
export function t(key, params) {
|
|
if (params && typeof params.count === 'number') {
|
|
return key + ':' + params.count;
|
|
}
|
|
return key;
|
|
}
|
|
`],
|
|
['related-assets-stub', `
|
|
export function getNearbyInfrastructure() {
|
|
return [];
|
|
}
|
|
export function getCountryInfrastructure() {
|
|
return [];
|
|
}
|
|
export function haversineDistanceKm() {
|
|
return 0;
|
|
}
|
|
`],
|
|
['sanitize-stub', `
|
|
export function sanitizeUrl(value) { return value ?? ''; }
|
|
export function escapeHtml(value) { return value ?? ''; }
|
|
`],
|
|
['intel-brief-stub', `export function formatIntelBrief(value) { return value; }`],
|
|
['utils-stub', `
|
|
export function getCSSColor() { return '#44ff88'; }
|
|
export function createCircuitBreaker() { return { execute: (fn) => fn() }; }
|
|
export function loadFromStorage() { return null; }
|
|
export function saveToStorage() {}
|
|
`],
|
|
['country-flag-stub', `export function toFlagEmoji(code, fallback = '🌍') { return code ? ':' + code + ':' : fallback; }`],
|
|
['ports-stub', `export const PORTS = [];`],
|
|
['trade-routes-stub', `export function getChokepointRoutes() { return []; } export const TRADE_ROUTES = [];`],
|
|
['geo-stub', `export const STRATEGIC_WATERWAYS = [];`],
|
|
['analytics-stub', `export function trackGateHit() {}`],
|
|
['chokepoint-registry-stub', `export const CHOKEPOINT_REGISTRY = [];`],
|
|
['supplier-route-risk-stub', `
|
|
export function computeAlternativeSuppliers(exporters) {
|
|
return exporters.map(e => ({ ...e, risk: { riskLevel: 'safe', transitChokepoints: [], maxDisruptionScore: 0, recommendation: '', routeIds: [], exporterIso2: e.partnerIso2, importerIso2: '' }, safeAlternative: null }));
|
|
}
|
|
export function computeSupplierRouteRisk() {
|
|
return { riskLevel: 'safe', transitChokepoints: [], maxDisruptionScore: 0, recommendation: '', routeIds: [], exporterIso2: '', importerIso2: '' };
|
|
}
|
|
`],
|
|
['supply-chain-stub', `
|
|
export function fetchBypassOptions() { return Promise.resolve({ corridors: [] }); }
|
|
export function getCountryChokepointIndex() { return null; }
|
|
export function fetchChokepointStatus() { return Promise.resolve({ chokepoints: [], fetchedAt: '', upstreamUnavailable: false }); }
|
|
export function fetchMultiSectorCostShock() { return Promise.resolve({ iso2: '', chokepointId: '', closureDays: 30, warRiskTier: 'WAR_RISK_TIER_UNSPECIFIED', sectors: [], totalAddedCost: 0, fetchedAt: '', unavailableReason: '' }); }
|
|
export const HS2_SHORT_LABELS = { '27': 'Energy', '84': 'Machinery', '85': 'Electronics', '87': 'Vehicles', '30': 'Pharma', '72': 'Iron & Steel', '39': 'Plastics', '29': 'Chemicals', '10': 'Cereals', '62': 'Apparel' };
|
|
`],
|
|
['runtime-stub', `
|
|
export function toApiUrl(path) { return path; }
|
|
export function isDesktopRuntime() { return false; }
|
|
export function getConfiguredWebApiBaseUrl() { return ''; }
|
|
`],
|
|
['intelligence-client-stub', `
|
|
export class IntelligenceServiceClient {}
|
|
`],
|
|
['panel-gating-stub', `
|
|
export function hasPremiumAccess() { return false; }
|
|
export function getPanelGateReason() { return 'none'; }
|
|
`],
|
|
['auth-state-stub', `
|
|
export function getAuthState() { return { user: null }; }
|
|
`],
|
|
['resilience-widget-stub', `
|
|
const state = globalThis.__wmCountryDeepDiveTestState;
|
|
export class ResilienceWidget {
|
|
constructor(code) {
|
|
this.code = code;
|
|
this.destroyCount = 0;
|
|
this.element = document.createElement('section');
|
|
this.element.className = 'resilience-widget-stub';
|
|
this.element.setAttribute('data-country-code', code);
|
|
this.element.textContent = 'Resilience ' + code;
|
|
state.widgets.push(this);
|
|
}
|
|
getElement() {
|
|
return this.element;
|
|
}
|
|
destroy() {
|
|
this.destroyCount += 1;
|
|
}
|
|
}
|
|
`],
|
|
]);
|
|
|
|
const aliasMap = new Map([
|
|
['@/config/feeds', 'feeds-stub'],
|
|
['@/services/country-geometry', 'country-geometry-stub'],
|
|
['@/services/i18n', 'i18n-stub'],
|
|
['@/services/related-assets', 'related-assets-stub'],
|
|
['@/utils/sanitize', 'sanitize-stub'],
|
|
['@/utils/format-intel-brief', 'intel-brief-stub'],
|
|
['@/utils', 'utils-stub'],
|
|
['@/utils/country-flag', 'country-flag-stub'],
|
|
['@/config/ports', 'ports-stub'],
|
|
['@/config/trade-routes', 'trade-routes-stub'],
|
|
['@/config/geo', 'geo-stub'],
|
|
['@/services/analytics', 'analytics-stub'],
|
|
['@/config/chokepoint-registry', 'chokepoint-registry-stub'],
|
|
['@/utils/supplier-route-risk', 'supplier-route-risk-stub'],
|
|
['@/services/supply-chain', 'supply-chain-stub'],
|
|
['./ResilienceWidget', 'resilience-widget-stub'],
|
|
['@/services/runtime', 'runtime-stub'],
|
|
['@/generated/client/worldmonitor/intelligence/v1/service_client', 'intelligence-client-stub'],
|
|
['@/services/panel-gating', 'panel-gating-stub'],
|
|
['@/services/auth-state', 'auth-state-stub'],
|
|
]);
|
|
|
|
const plugin = {
|
|
name: 'country-deep-dive-test-stubs',
|
|
setup(buildApi) {
|
|
buildApi.onResolve({ filter: /.*/ }, (args) => {
|
|
const target = aliasMap.get(args.path);
|
|
return target ? { path: target, namespace: 'stub' } : null;
|
|
});
|
|
|
|
buildApi.onLoad({ filter: /.*/, namespace: 'stub' }, (args) => ({
|
|
contents: stubModules.get(args.path),
|
|
loader: 'js',
|
|
}));
|
|
},
|
|
};
|
|
|
|
const result = await build({
|
|
entryPoints: [entry],
|
|
bundle: true,
|
|
format: 'esm',
|
|
platform: 'browser',
|
|
target: 'es2020',
|
|
write: false,
|
|
plugins: [plugin],
|
|
});
|
|
|
|
writeFileSync(outfile, result.outputFiles[0].text, 'utf8');
|
|
|
|
const mod = await import(`${pathToFileURL(outfile).href}?t=${Date.now()}`);
|
|
return {
|
|
CountryDeepDivePanel: mod.CountryDeepDivePanel,
|
|
cleanupBundle() {
|
|
rmSync(tempDir, { recursive: true, force: true });
|
|
},
|
|
};
|
|
}
|
|
|
|
export async function createCountryDeepDivePanelHarness() {
|
|
const originalGlobals = {
|
|
document: snapshotGlobal('document'),
|
|
window: snapshotGlobal('window'),
|
|
localStorage: snapshotGlobal('localStorage'),
|
|
requestAnimationFrame: snapshotGlobal('requestAnimationFrame'),
|
|
cancelAnimationFrame: snapshotGlobal('cancelAnimationFrame'),
|
|
navigator: snapshotGlobal('navigator'),
|
|
HTMLElement: snapshotGlobal('HTMLElement'),
|
|
HTMLButtonElement: snapshotGlobal('HTMLButtonElement'),
|
|
};
|
|
const browserEnvironment = createBrowserEnvironment();
|
|
const state = { widgets: [] };
|
|
|
|
defineGlobal('document', browserEnvironment.document);
|
|
defineGlobal('window', browserEnvironment.window);
|
|
defineGlobal('localStorage', browserEnvironment.localStorage);
|
|
defineGlobal('requestAnimationFrame', browserEnvironment.requestAnimationFrame);
|
|
defineGlobal('cancelAnimationFrame', browserEnvironment.cancelAnimationFrame);
|
|
defineGlobal('navigator', browserEnvironment.window.navigator);
|
|
defineGlobal('HTMLElement', browserEnvironment.HTMLElement);
|
|
defineGlobal('HTMLButtonElement', browserEnvironment.HTMLButtonElement);
|
|
globalThis.__wmCountryDeepDiveTestState = state;
|
|
|
|
let CountryDeepDivePanel;
|
|
let cleanupBundle;
|
|
try {
|
|
({ CountryDeepDivePanel, cleanupBundle } = await loadCountryDeepDivePanel());
|
|
} catch (error) {
|
|
delete globalThis.__wmCountryDeepDiveTestState;
|
|
restoreGlobal('document', originalGlobals.document);
|
|
restoreGlobal('window', originalGlobals.window);
|
|
restoreGlobal('localStorage', originalGlobals.localStorage);
|
|
restoreGlobal('requestAnimationFrame', originalGlobals.requestAnimationFrame);
|
|
restoreGlobal('cancelAnimationFrame', originalGlobals.cancelAnimationFrame);
|
|
restoreGlobal('navigator', originalGlobals.navigator);
|
|
restoreGlobal('HTMLElement', originalGlobals.HTMLElement);
|
|
restoreGlobal('HTMLButtonElement', originalGlobals.HTMLButtonElement);
|
|
throw error;
|
|
}
|
|
|
|
function createPanel() {
|
|
return new CountryDeepDivePanel(null);
|
|
}
|
|
|
|
function getPanelRoot() {
|
|
return browserEnvironment.document.getElementById('country-deep-dive-panel');
|
|
}
|
|
|
|
function cleanup() {
|
|
cleanupBundle();
|
|
delete globalThis.__wmCountryDeepDiveTestState;
|
|
restoreGlobal('document', originalGlobals.document);
|
|
restoreGlobal('window', originalGlobals.window);
|
|
restoreGlobal('localStorage', originalGlobals.localStorage);
|
|
restoreGlobal('requestAnimationFrame', originalGlobals.requestAnimationFrame);
|
|
restoreGlobal('cancelAnimationFrame', originalGlobals.cancelAnimationFrame);
|
|
restoreGlobal('navigator', originalGlobals.navigator);
|
|
restoreGlobal('HTMLElement', originalGlobals.HTMLElement);
|
|
restoreGlobal('HTMLButtonElement', originalGlobals.HTMLButtonElement);
|
|
}
|
|
|
|
return {
|
|
createPanel,
|
|
document: browserEnvironment.document,
|
|
getPanelRoot,
|
|
getWidgets() {
|
|
return state.widgets;
|
|
},
|
|
cleanup,
|
|
};
|
|
}
|