mirror of
https://github.com/koala73/worldmonitor.git
synced 2026-04-25 17:14:57 +02:00
* chore(api): enforce sebuf contract via exceptions manifest (#3207) Adds api/api-route-exceptions.json as the single source of truth for non-proto /api/ endpoints, with scripts/enforce-sebuf-api-contract.mjs gating every PR via npm run lint:api-contract. Fixes the root-only blind spot in the prior allowlist (tests/edge-functions.test.mjs), which only scanned top-level *.js files and missed nested paths and .ts endpoints — the gap that let api/supply-chain/v1/country-products.ts and friends drift under proto domain URL prefixes unchallenged. Checks both directions: every api/<domain>/v<N>/[rpc].ts must pair with a generated service_server.ts (so a deleted proto fails CI), and every generated service must have an HTTP gateway (no orphaned generated code). Manifest entries require category + reason + owner, with removal_issue mandatory for temporary categories (deferred, migration-pending) and forbidden for permanent ones. .github/CODEOWNERS pins the manifest to @SebastienMelki so new exceptions don't slip through review. The manifest only shrinks: migration-pending entries (19 today) will be removed as subsequent commits in this PR land each migration. * refactor(maritime): migrate /api/ais-snapshot → maritime/v1.GetVesselSnapshot (#3207) The proto VesselSnapshot was carrying density + disruptions but the frontend also needed sequence, relay status, and candidate_reports to drive the position-callback system. Those only lived on the raw relay passthrough, so the client had to keep hitting /api/ais-snapshot whenever callbacks were registered and fall back to the proto RPC only when the relay URL was gone. This commit pushes all three missing fields through the proto contract and collapses the dual-fetch-path into one proto client call. Proto changes (proto/worldmonitor/maritime/v1/): - VesselSnapshot gains sequence, status, candidate_reports. - GetVesselSnapshotRequest gains include_candidates (query: include_candidates). Handler (server/worldmonitor/maritime/v1/get-vessel-snapshot.ts): - Forwards include_candidates to ?candidates=... on the relay. - Separate 5-min in-memory caches for the candidates=on and candidates=off variants; they have very different payload sizes and should not share a slot. - Per-request in-flight dedup preserved per-variant. Frontend (src/services/maritime/index.ts): - fetchSnapshotPayload now calls MaritimeServiceClient.getVesselSnapshot directly with includeCandidates threaded through. The raw-relay path, SNAPSHOT_PROXY_URL, DIRECT_RAILWAY_SNAPSHOT_URL and LOCAL_SNAPSHOT_FALLBACK are gone — production already routed via Vercel, the "direct" branch only ever fired on localhost, and the proto gateway covers both. - New toLegacyCandidateReport helper mirrors toDensityZone/toDisruptionEvent. api/ais-snapshot.js deleted; manifest entry removed. Only reduced the codegen scope to worldmonitor.maritime.v1 (buf generate --path) — regenerating the full tree drops // @ts-nocheck from every client/server file and surfaces pre-existing type errors across 30+ unrelated services, which is not in scope for this PR. Shape-diff vs legacy payload: - disruptions / density: proto carries the same fields, just with the GeoCoordinates wrapper and enum strings (remapped client-side via existing toDisruptionEvent / toDensityZone helpers). - sequence, status.{connected,vessels,messages}: now populated from the proto response — was hardcoded to 0/false in the prior proto fallback. - candidateReports: same shape; optional numeric fields come through as 0 instead of undefined, which the legacy consumer already handled. * refactor(sanctions): migrate /api/sanctions-entity-search → LookupSanctionEntity (#3207) The proto docstring already claimed "OFAC + OpenSanctions" coverage but the handler only fuzzy-matched a local OFAC Redis index — narrower than the legacy /api/sanctions-entity-search, which proxied OpenSanctions live (the source advertised in docs/api-proxies.mdx). Deleting the legacy without expanding the handler would have been a silent coverage regression for external consumers. Handler changes (server/worldmonitor/sanctions/v1/lookup-entity.ts): - Primary path: live search against api.opensanctions.org/search/default with an 8s timeout and the same User-Agent the legacy edge fn used. - Fallback path: the existing OFAC local fuzzy match, kept intact for when OpenSanctions is unreachable / rate-limiting. - Response source field flips between 'opensanctions' (happy path) and 'ofac' (fallback) so clients can tell which index answered. - Query validation tightened: rejects q > 200 chars (matches legacy cap). Rate limiting: - Added /api/sanctions/v1/lookup-entity to ENDPOINT_RATE_POLICIES at 30/min per IP — matches the legacy createIpRateLimiter budget. The gateway already enforces per-endpoint policies via checkEndpointRateLimit. Docs: - docs/api-proxies.mdx — dropped the /api/sanctions-entity-search row (plus the orphaned /api/ais-snapshot row left over from the previous commit in this PR). - docs/panels/sanctions-pressure.mdx — points at the new RPC URL and describes the OpenSanctions-primary / OFAC-fallback semantics. api/sanctions-entity-search.js deleted; manifest entry removed. * refactor(military): migrate /api/military-flights → ListMilitaryFlights (#3207) Legacy /api/military-flights read a pre-baked Redis blob written by the seed-military-flights cron and returned flights in a flat app-friendly shape (lat/lon, lowercase enums, lastSeenMs). The proto RPC takes a bbox, fetches OpenSky live, classifies server-side, and returns nested GeoCoordinates + MILITARY_*_TYPE_* enum strings + lastSeenAt — same data, different contract. fetchFromRedis in src/services/military-flights.ts was doing nothing sebuf-aware. Renamed it to fetchViaProto and rewrote to: - Instantiate MilitaryServiceClient against getRpcBaseUrl(). - Iterate MILITARY_QUERY_REGIONS (PACIFIC + WESTERN) in parallel — same regions the desktop OpenSky path and the seed cron already use, so dashboard coverage tracks the analytic pipeline. - Dedup by hexCode across regions. - Map proto → app shape via new mapProtoFlight helper plus three reverse enum maps (AIRCRAFT_TYPE_REVERSE, OPERATOR_REVERSE, CONFIDENCE_REVERSE). The seed cron (scripts/seed-military-flights.mjs) stays put: it feeds regional-snapshot mobility, cross-source signals, correlation, and the health freshness check (api/health.js: 'military:flights:v1'). None of those read the legacy HTTP endpoint; they read the Redis key directly. The proto handler uses its own per-bbox cache keys under the same prefix, so dashboard traffic no longer races the seed cron's blob — the two paths diverge by a small refresh lag, which is acceptable. Docs: dropped the /api/military-flights row from docs/api-proxies.mdx. api/military-flights.js deleted; manifest entry removed. Shape-diff vs legacy: - f.location.{latitude,longitude} → f.lat, f.lon - f.aircraftType: MILITARY_AIRCRAFT_TYPE_TANKER → 'tanker' via reverse map - f.operator: MILITARY_OPERATOR_USAF → 'usaf' via reverse map - f.confidence: MILITARY_CONFIDENCE_LOW → 'low' via reverse map - f.lastSeenAt (number) → f.lastSeen (Date) - f.enrichment → f.enriched (with field renames) - Extra fields registration / aircraftModel / origin / destination / firstSeenAt now flow through where proto populates them. * fix(supply-chain): thread includeCandidates through chokepoint status (#3207) Caught by tsconfig.api.json typecheck in the pre-push hook (not covered by the plain tsc --noEmit run that ran before I pushed the ais-snapshot commit). The chokepoint status handler calls getVesselSnapshot internally with a static no-auth request — now required to include the new includeCandidates bool from the proto extension. Passing false: server-internal callers don't need per-vessel reports. * test(maritime): update getVesselSnapshot cache assertions (#3207) The ais-snapshot migration replaced the single cachedSnapshot/cacheTimestamp pair with a per-variant cache so candidates-on and candidates-off payloads don't evict each other. Pre-push hook surfaced that tests/server-handlers still asserted the old variable names. Rewriting the assertions to match the new shape while preserving the invariants they actually guard: - Freshness check against slot TTL. - Cache read before relay call. - Per-slot in-flight dedup. - Stale-serve on relay failure (result ?? slot.snapshot). * chore(proto): restore // @ts-nocheck on regenerated maritime files (#3207) I ran 'buf generate --path worldmonitor/maritime/v1' to scope the proto regen to the one service I was changing (to avoid the toolchain drift that drops @ts-nocheck from 60+ unrelated files — separate issue). But the repo convention is the 'make generate' target, which runs buf and then sed-prepends '// @ts-nocheck' to every generated .ts file. My scoped command skipped the sed step. The proto-check CI enforces the sed output, so the two maritime files need the directive restored. * refactor(enrichment): decomm /api/enrichment/{company,signals} legacy edge fns (#3207) Both endpoints were already ported to IntelligenceService: - getCompanyEnrichment (/api/intelligence/v1/get-company-enrichment) - listCompanySignals (/api/intelligence/v1/list-company-signals) No frontend callers of the legacy /api/enrichment/* paths exist. Removes: - api/enrichment/company.js, signals.js, _domain.js - api-route-exceptions.json migration-pending entries (58 remain) - docs/api-proxies.mdx rows for /api/enrichment/{company,signals} - docs/architecture.mdx reference updated to the IntelligenceService RPCs Verified: typecheck, typecheck:api, lint:api-contract (89 files / 58 entries), lint:boundaries, tests/edge-functions.test.mjs (136 pass), tests/enrichment-caching.test.mjs (14 pass — still guards the intelligence/v1 handlers), make generate is zero-diff. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(leads): migrate /api/{contact,register-interest} → LeadsService (#3207) New leads/v1 sebuf service with two POST RPCs: - SubmitContact → /api/leads/v1/submit-contact - RegisterInterest → /api/leads/v1/register-interest Handler logic ported 1:1 from api/contact.js + api/register-interest.js: - Turnstile verification (desktop sources bypass, preserved) - Honeypot (website field) silently accepts without upstream calls - Free-email-domain gate on SubmitContact (422 ApiError) - validateEmail (disposable/offensive/typo-TLD/MX) on RegisterInterest - Convex writes via ConvexHttpClient (contactMessages:submit, registerInterest:register) - Resend notification + confirmation emails (HTML templates unchanged) Shared helpers moved to server/_shared/: - turnstile.ts (getClientIp + verifyTurnstile) - email-validation.ts (disposable/offensive/MX checks) Rate limits preserved via ENDPOINT_RATE_POLICIES: - submit-contact: 3/hour per IP (was in-memory 3/hr) - register-interest: 5/hour per IP (was in-memory 5/hr; desktop sources previously capped at 2/hr via shared in-memory map — now 5/hr like everyone else, accepting the small regression in exchange for Upstash-backed global limiting) Callers updated: - pro-test/src/App.tsx contact form → new submit-contact path - src-tauri/sidecar/local-api-server.mjs cloud-fallback rewrites /api/register-interest → /api/leads/v1/register-interest when proxying; keeps local path for older desktop builds - src/services/runtime.ts isKeyFreeApiTarget allows both old and new paths through the WORLDMONITOR_API_KEY-optional gate Tests: - tests/contact-handler.test.mjs rewritten to call submitContact handler directly; asserts on ValidationError / ApiError - tests/email-validation.test.mjs + tests/turnstile.test.mjs point at the new server/_shared/ modules Deleted: api/contact.js, api/register-interest.js, api/_ip-rate-limit.js, api/_turnstile.js, api/_email-validation.js, api/_turnstile.test.mjs. Manifest entries removed (58 → 56). Docs updated (api-platform, api-commerce, usage-rate-limits). Verified: npm run typecheck + typecheck:api + lint:api-contract (88 files / 56 entries) + lint:boundaries pass; full test:data (5852 tests) passes; make generate is zero-diff. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(pro-test): rebuild bundle for leads/v1 contact form (#3207) Updates the enterprise contact form to POST to /api/leads/v1/submit-contact (old path /api/contact removed in the previous commit). Bundle is rebuilt from pro-test/src/App.tsx source change in9ccd309d. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(review): address HIGH review findings 1-3 (#3207) Three review findings from @koala73 on the sebuf-migration PR, all silent bugs that would have shipped to prod: ### 1. Sanctions rate-limit policy was dead code ENDPOINT_RATE_POLICIES keyed the 30/min budget under /api/sanctions/v1/lookup-entity, but the generated route (from the proto RPC LookupSanctionEntity) is /api/sanctions/v1/lookup-sanction-entity. hasEndpointRatePolicy / getEndpointRatelimit are exact-string pathname lookups, so the mismatch meant the endpoint fell through to the generic 600/min global limiter instead of the advertised 30/min. Net effect: the live OpenSanctions proxy endpoint (unauthenticated, external upstream) had 20x the intended rate budget. Fixed by renaming the policy key to match the generated route. ### 2. Lost stale-seed fallback on military-flights Legacy api/military-flights.js cascaded military:flights:v1 → military:flights:stale:v1 before returning empty. The new proto handler went straight to live OpenSky/relay and returned null on miss. Relay or OpenSky hiccup used to serve stale seeded data (24h TTL); under the new handler it showed an empty map. Both keys are still written by scripts/seed-military-flights.mjs on every run — fix just reads the stale key when the live fetch returns null, converts the seed's app-shape flights (flat lat/lon, lowercase enums, lastSeenMs) to the proto shape (nested GeoCoordinates, enum strings, lastSeenAt), and filters to the request bbox. Read via getRawJson (unprefixed) to match the seed cron's writes, which bypass the env-prefix system. ### 3. Hex-code casing mismatch broke getFlightByHex The seed cron writes hexCode: icao24.toUpperCase() (uppercase); src/services/military-flights.ts:getFlightByHex uppercases the lookup input: f.hexCode === hexCode.toUpperCase(). The new proto handler preserved OpenSky's lowercase icao24, and mapProtoFlight is a pass-through. getFlightByHex was silently returning undefined for every call after the migration. Fix: uppercase in the proto handler (live + stale paths), and document the invariant in a comment on MilitaryFlight.hex_code in military_flight.proto so future handlers don't re-break it. ### Verified - typecheck + typecheck:api clean - lint:api-contract (56 entries) / lint:boundaries clean - tests/edge-functions.test.mjs 130 pass - make generate zero-diff (openapi spec regenerated for proto comment) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(review): restore desktop 2/hr rate cap on register-interest (#3207) Addresses HIGH review finding #4 from @koala73. The legacy api/register-interest.js applied a nested 2/hr per-IP cap when `source === 'desktop-settings'`, on top of the generic 5/hr endpoint budget. The sebuf migration lost this — desktop-source requests now enjoy the full 5/hr cap. Since `source` is an unsigned client-supplied field, anyone sending `source: 'desktop-settings'` skips Turnstile AND gets 5/hr. Without the tighter cap the Turnstile bypass is cheaper to abuse. Added `checkScopedRateLimit` to `server/_shared/rate-limit.ts` — a reusable second-stage Upstash limiter keyed on an opaque scope string + caller identifier. Fail-open on Redis errors to match existing checkRateLimit / checkEndpointRateLimit semantics. Handlers that need per-subscope caps on top of the gateway-level endpoint budget use this helper. In register-interest: when `isDesktopSource`, call checkScopedRateLimit with scope `/api/leads/v1/register-interest#desktop`, limit=2, window=1h, IP as identifier. On exceeded → throw ApiError(429). ### What this does not fix This caps the blast radius of the Turnstile bypass but does not close it — an attacker sending `source: 'desktop-settings'` still skips Turnstile (just at 2/hr instead of 5/hr). The proper fix is a signed desktop-secret header that authenticates the bypass; filed as follow-up #3252. That requires coordinated Tauri build + Vercel env changes out of scope for #3207. ### Verified - typecheck + typecheck:api clean - lint:api-contract (56 entries) - tests/edge-functions.test.mjs + contact-handler.test.mjs (147 pass) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(review): MEDIUM + LOW + rate-limit-policy CI check (#3207) Closes out the remaining @koala73 review findings from #3242 that didn't already land in the HIGH-fix commits, plus the requested CI check that would have caught HIGH #1 (dead-code policy key) at review time. ### MEDIUM #5 — Turnstile missing-secret policy default Flip `verifyTurnstile`'s default `missingSecretPolicy` from `'allow'` to `'allow-in-development'`. Dev with no secret = pass (expected local); prod with no secret = reject + log. submit-contact was already explicitly overriding to `'allow-in-development'`; register-interest was silently getting `'allow'`. Safe default now means a future missing-secret misconfiguration in prod gets caught instead of silently letting bots through. Removed the now-redundant override in submit-contact. ### MEDIUM #6 — Silent enum fallbacks in maritime client `toDisruptionEvent` mapped `AIS_DISRUPTION_TYPE_UNSPECIFIED` / unknown enum values → `gap_spike` / `low` silently. Refactored to return null when either enum is unknown; caller filters nulls out of the array. Handler doesn't produce UNSPECIFIED today, but the `gap_spike` default would have mislabeled the first new enum value the proto ever adds — dropping unknowns is safer than shipping wrong labels. ### LOW — Copy drift in register-interest email Email template hardcoded `435+ Sources`; PR #3241 bumped marketing to `500+`. Bumped in the rewritten file to stay consistent. The `as any` on Convex mutation names carried over from legacy and filed as follow-up #3253. ### Rate-limit-policy coverage lint `scripts/enforce-rate-limit-policies.mjs` validates every key in `ENDPOINT_RATE_POLICIES` resolves to a proto-generated gateway route by cross-referencing `docs/api/*.openapi.yaml`. Fails with the sanctions-entity-search incident referenced in the error message so future drift has a paper trail. Wired into package.json (`lint:rate-limit-policies`) and the pre-push hook alongside `lint:boundaries`. Smoke-tested both directions — clean repo passes (5 policies / 175 routes), seeded drift (the exact HIGH #1 typo) fails with the advertised remedy text. ### Verified - `lint:rate-limit-policies` ✓ - `typecheck` + `typecheck:api` ✓ - `lint:api-contract` ✓ (56 entries) - `lint:boundaries` ✓ - edge-functions + contact-handler tests (147 pass) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(commit 5): decomm /api/eia/* + migrate /api/satellites → IntelligenceService (#3207) Both targets turned out to be decomm-not-migration cases. The original plan called for two new services (economic/v1.GetEiaSeries + natural/v1.ListSatellitePositions) but research found neither was needed: ### /api/eia/[[...path]].js — pure decomm, zero consumers The "catch-all" is a misnomer — only two paths actually worked, /api/eia/health and /api/eia/petroleum, both Redis-only readers. Zero frontend callers in src/. Zero server-side readers. Nothing consumes the `energy:eia-petroleum:v1` key that seed-eia-petroleum.mjs writes daily. The EIA data the frontend actually uses goes through existing typed RPCs in economic/v1: GetEnergyPrices, GetCrudeInventories, GetNatGasStorage, GetEnergyCapacity. None of those touch /api/eia/*. Building GetEiaSeries would have been dead code. Deleted the legacy file + its test (tests/api-eia-petroleum.test.mjs — it only covered the legacy endpoint, no behavior to preserve). Empty api/eia/ dir removed. **Note for review:** the Redis seed cron keeps running daily and nothing consumes it. If that stays unused, seed-eia-petroleum.mjs should be retired too (separate PR). Out of scope for sebuf-migration. ### /api/satellites.js — Learning #2 strikes again IntelligenceService.ListSatellites already exists at /api/intelligence/v1/list-satellites, reads the same Redis key (intelligence:satellites:tle:v1), and supports an optional country filter the legacy didn't have. One frontend caller in src/services/satellites.ts needed to switch from `fetch(toApiUrl('/api/satellites'))` to the typed IntelligenceServiceClient.listSatellites. Shape diff was tiny — legacy `noradId` became proto `id` (handler line 36 already picks either), everything else identical. alt/velocity/inclination in the proto are ignored by the caller since it propagates positions client-side via satellite.js. Kept the client-side cache + failure cooldown + 20s timeout (still valid concerns at the caller level). ### Manifest + docs - api-route-exceptions.json: 56 → 54 entries (both removed) - docs/api-proxies.mdx: dropped the two rows from the Raw-data passthroughs table ### Verified - typecheck + typecheck:api ✓ - lint:api-contract (54 entries) / lint:boundaries / lint:rate-limit-policies ✓ - tests/edge-functions.test.mjs 127 pass (down from 130 — 3 tests were for the deleted eia endpoint) - make generate zero-diff (no proto changes) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(commit 6): migrate /api/supply-chain/v1/{country-products,multi-sector-cost-shock} → SupplyChainService (#3207) Both endpoints were hand-rolled TS handlers sitting under a proto URL prefix — the exact drift the manifest guardrail flagged. Promoted both to typed RPCs: - GetCountryProducts → /api/supply-chain/v1/get-country-products - GetMultiSectorCostShock → /api/supply-chain/v1/get-multi-sector-cost-shock Handlers preserve the existing semantics: PRO-gate via isCallerPremium(ctx.request), iso2 / chokepointId validation, raw bilateral-hs4 Redis read (skip env-prefix to match seeder writes), CHOKEPOINT_STATUS_KEY for war-risk tier, and the math from _multi-sector-shock.ts unchanged. Empty-data and non-PRO paths return the typed empty payload (no 403 — the sebuf gateway pattern is empty-payload-on-deny). Client wrapper switches from premiumFetch to client.getCountryProducts/ client.getMultiSectorCostShock. Legacy MultiSectorShock / MultiSectorShockResponse / CountryProductsResponse names remain as type aliases of the generated proto types so CountryBriefPanel + CountryDeepDivePanel callsites compile with zero churn. Manifest 54 → 52. Rate-limit gateway routes 175 → 177. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(gateway): add cache-tier entries for new supply-chain RPCs (#3207) Pre-push tests/route-cache-tier.test.mjs caught the missing entries. Both PRO-gated, request-varying — match the existing supply-chain PRO cohort (get-country-cost-shock, get-bypass-options, etc.) at slow-browser tier. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(commit 7): migrate /api/scenario/v1/{run,status,templates} → ScenarioService (#3207) Promote the three literal-filename scenario endpoints to a typed sebuf service with three RPCs: POST /api/scenario/v1/run-scenario (RunScenario) GET /api/scenario/v1/get-scenario-status (GetScenarioStatus) GET /api/scenario/v1/list-scenario-templates (ListScenarioTemplates) Preserves all security invariants from the legacy handlers: - 405 for wrong method (sebuf service-config method gate) - scenarioId validation against SCENARIO_TEMPLATES registry - iso2 regex ^[A-Z]{2}$ - JOB_ID_RE path-traversal guard on status - Per-IP 10/min rate limit (moved to gateway ENDPOINT_RATE_POLICIES) - Queue-depth backpressure (>100 → 429) - PRO gating via isCallerPremium - AbortSignal.timeout on every Redis pipeline (runRedisPipeline helper) Wire-level diffs vs legacy: - Per-user RL now enforced at the gateway (same 10/min/IP budget). - Rate-limit response omits Retry-After header; retryAfter is in the body per error-mapper.ts convention. - ListScenarioTemplates emits affectedHs2: [] when the registry entry is null (all-sectors sentinel); proto repeated cannot carry null. - RunScenario returns { jobId, status } (no statusUrl field — unused by SupplyChainPanel, drop from wire). Gateway wiring: - server/gateway.ts RPC_CACHE_TIER: list-scenario-templates → 'daily' (matches legacy max-age=3600); get-scenario-status → 'slow-browser' (premium short-circuit target, explicit entry required by tests/route-cache-tier.test.mjs). - src/shared/premium-paths.ts: swap old run/status for the new run-scenario/get-scenario-status paths. - api/scenario/v1/{run,status,templates}.ts deleted; 3 manifest exceptions removed (63 → 52 → 49 migration-pending). Client: - src/services/scenario/index.ts — typed client wrapper using premiumFetch (injects Clerk bearer / API key). - src/components/SupplyChainPanel.ts — polling loop swapped from premiumFetch strings to runScenario/getScenarioStatus. Hard 20s timeout on run preserved via AbortSignal.any. Tests: - tests/scenario-handler.test.mjs — 18 new handler-level tests covering every security invariant + the worker envelope coercion. - tests/edge-functions.test.mjs — scenario sections removed, replaced with a breadcrumb pointer to the new test file. Docs: api-scenarios.mdx, scenario-engine.mdx, usage-rate-limits.mdx, usage-errors.mdx, supply-chain.mdx refreshed with new paths. Verified: typecheck, typecheck:api, lint:api-contract (49 entries), lint:rate-limit-policies (6/180), lint:boundaries, route-cache-tier (parity), full edge-functions (117) + scenario-handler (18). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(commit 8): migrate /api/v2/shipping/{route-intelligence,webhooks} → ShippingV2Service (#3207) Partner-facing endpoints promoted to a typed sebuf service. Wire shape preserved byte-for-byte (camelCase field names, ISO-8601 fetchedAt, the same subscriberId/secret formats, the same SET + SADD + EXPIRE 30-day Redis pipeline). Partner URLs /api/v2/shipping/* are unchanged. RPCs landed: - GET /route-intelligence → RouteIntelligence (PRO, slow-browser) - POST /webhooks → RegisterWebhook (PRO) - GET /webhooks → ListWebhooks (PRO, slow-browser) The existing path-parameter URLs remain on the legacy edge-function layout because sebuf's HTTP annotations don't currently model path params (grep proto/**/*.proto for `path: "{…}"` returns zero). Those endpoints are split into two Vercel dynamic-route files under api/v2/shipping/webhooks/, behaviorally identical to the previous hybrid file but cleanly separated: - GET /webhooks/{subscriberId} → [subscriberId].ts - POST /webhooks/{subscriberId}/rotate-secret → [subscriberId]/[action].ts - POST /webhooks/{subscriberId}/reactivate → [subscriberId]/[action].ts Both get manifest entries under `migration-pending` pointing at #3207. Other changes - scripts/enforce-sebuf-api-contract.mjs: extended GATEWAY_RE to accept api/v{N}/{domain}/[rpc].ts (version-first) alongside the canonical api/{domain}/v{N}/[rpc].ts; first-use of the reversed ordering is shipping/v2 because that's the partner contract. - vite.config.ts: dev-server sebuf interceptor regex extended to match both layouts; shipping/v2 import + allRoutes entry added. - server/gateway.ts: RPC_CACHE_TIER entries for /api/v2/shipping/ route-intelligence + /webhooks (slow-browser; premium-gated endpoints short-circuit to slow-browser but the entries are required by tests/route-cache-tier.test.mjs). - src/shared/premium-paths.ts: route-intelligence + webhooks added. - tests/shipping-v2-handler.test.mjs: 18 handler-level tests covering PRO gate, iso2/cargoType/hs2 coercion, SSRF guards (http://, RFC1918, cloud metadata, IMDS), chokepoint whitelist, alertThreshold range, secret/subscriberId format, pipeline shape + 30-day TTL, cross-tenant owner isolation, `secret` omission from list response. Manifest delta - Removed: api/v2/shipping/route-intelligence.ts, api/v2/shipping/webhooks.ts - Added: api/v2/shipping/webhooks/[subscriberId].ts (migration-pending) - Added: api/v2/shipping/webhooks/[subscriberId]/[action].ts (migration-pending) - Added: api/internal/brief-why-matters.ts (internal-helper) — regression surface from the #3248 main merge, which introduced the file without a manifest entry. Filed here to keep the lint green; not strictly in scope for commit 8 but unblocking. Net result: 49 → 47 `migration-pending` entries (one net-removal even though webhook path-params stay pending, because two files collapsed into two dynamic routes). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(review HIGH 1): SupplyChainServiceClient must use premiumFetch (#3207) Signed-in browser pro users were silently hitting 401 on 8 supply-chain premium endpoints (country-products, multi-sector-cost-shock, country-chokepoint-index, bypass-options, country-cost-shock, sector-dependency, route-explorer-lane, route-impact). The shared client was constructed with globalThis.fetch, so no Clerk bearer or X-WorldMonitor-Key was injected. The gateway's validateApiKey runs with forceKey=true for PREMIUM_RPC_PATHS and 401s before isCallerPremium is consulted. The generated client's try/catch collapses the 401 into an empty-fallback return, leaving panels blank with no visible error. Fix is one line at the client constructor: swap globalThis.fetch for premiumFetch. The same pattern is already in use for insider-transactions, stock-analysis, stock-backtest, scenario, trade (premiumClient) — this was an omission on this client, not a new pattern. premiumFetch no-ops safely when no credentials are available, so the 5 non-premium methods on this client (shippingRates, chokepointStatus, chokepointHistory, criticalMinerals, shippingStress) continue to work unchanged. This also fixes two panels that were pre-existing latently broken on main (chokepoint-index, bypass-options, etc. — predating #3207, not regressions from it). Commit 6 expanded the surface by routing two more methods through the same buggy client; this commit fixes the class. From koala73 review (#3242 second-pass, HIGH new #1): > Exact class PR #3233 fixed for RegionalIntelligenceBoard / > DeductionPanel / trade / country-intel. Supply-chain was not in > #3233's scope. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(review HIGH 2): restore 400 on input-shape errors for 2 supply-chain handlers (#3207) Commit 6 collapsed all non-happy paths into empty-200 on `get-country-products` and `get-multi-sector-cost-shock`, including caller-bug cases that legacy returned 400 for: - get-country-products: malformed iso2 → empty 200 (was 400) - get-multi-sector-cost-shock: malformed iso2 / missing chokepointId / unknown chokepointId → empty 200 (was 400) The commit message for 6 called out the 403-for-non-pro → empty-200 shift ("sebuf gateway pattern is empty-payload-on-deny") but not the 400 shift. They're different classes: - Empty-payload-200 for PRO-deny: intentional contract change, already documented and applied across the service. Generated clients treat "you lack PRO" as "no data" — fine. - Empty-payload-200 for malformed input: caller bug silently masked. External API consumers can't distinguish "bad wiring" from "genuinely no data", test harnesses lose the signal, bad calling code doesn't surface in Sentry. Fix: `throw new ValidationError(violations)` on the 3 input-shape branches. The generated sebuf server maps ValidationError → HTTP 400 (see src/generated/server/.../service_server.ts and leads/v1 which already uses this pattern). PRO-gate deny stays as empty-200 — that contract shift was intentional and is preserved. Regression tests added at tests/supply-chain-validation.test.mjs (8 cases) pinning the three-way contract: - bad input → 400 (ValidationError) - PRO-gate deny on valid input → 200 empty - valid PRO input, no data in Redis → 200 empty (unchanged) From koala73 review (#3242 second-pass, HIGH new #2). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(review HIGH 3): restore statusUrl on RunScenarioResponse + document 202→200 wire break (#3207) Commit 7 silently shifted /api/scenario/v1/run-scenario's response contract in two ways that the commit message covered only partially: 1. HTTP 202 Accepted → HTTP 200 OK 2. Dropped `statusUrl` string from the response body The `statusUrl` drop was mentioned as "unused by SupplyChainPanel" but not framed as a contract change. The 202 → 200 shift was not mentioned at all. This is a same-version (v1 → v1) migration, so external callers that key off either signal — `response.status === 202` or `response.body.statusUrl` — silently branch incorrectly. Evaluated options: (a) sebuf per-RPC status-code config — not available. sebuf's HttpConfig only models `path` and `method`; no status annotation. (b) Bump to scenario/v2 — judged heavier than the break itself for a single status-code shift. No in-repo caller uses 202 or statusUrl; the docs-level impact is containable. (c) Accept the break, document explicitly, partially restore. Took option (c): - Restored `statusUrl` in the proto (new field `string status_url = 3` on RunScenarioResponse). Server computes `/api/scenario/v1/get-scenario-status?jobId=<encoded job_id>` and populates it on every successful enqueue. External callers that followed this URL keep working unchanged. - 202 → 200 is not recoverable inside the sebuf generator, so it is called out explicitly in two places: - docs/api-scenarios.mdx now includes a prominent `<Warning>` block documenting the v1→v1 contract shift + the suggested migration (branch on response body shape, not HTTP status). - RunScenarioResponse proto comment explains why 200 is the new success status on enqueue. OpenAPI bundle regenerated to reflect the restored statusUrl field. - Regression test added in tests/scenario-handler.test.mjs pinning `statusUrl` to the exact URL-encoded shape — locks the invariant so a future proto rename or handler refactor can't silently drop it again. From koala73 review (#3242 second-pass, HIGH new #3). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(review HIGH 1/2): close webhook tenant-isolation gap on shipping/v2 (#3207) Koala flagged this as a merge blocker in PR #3242 review. server/worldmonitor/shipping/v2/{register-webhook,list-webhooks}.ts migrated without reinstating validateApiKey(req, { forceKey: true }), diverging from both the sibling api/v2/shipping/webhooks/[subscriberId] routes and the documented "X-WorldMonitor-Key required" contract in docs/api-shipping-v2.mdx. Attack surface: the gateway accepts Clerk bearer auth as a pro signal. A Clerk-authenticated pro user with no X-WorldMonitor-Key reaches the handler, callerFingerprint() falls back to 'anon', and every such caller collapses into a shared webhook:owner:anon:v1 bucket. The defense-in-depth ownerTag !== ownerHash check in list-webhooks.ts doesn't catch it because both sides equal 'anon' — every Clerk-session holder could enumerate / overwrite every other Clerk-session pro tenant's registered webhook URLs. Fix: reinstate validateApiKey(ctx.request, { forceKey: true }) at the top of each handler, throwing ApiError(401) when absent. Matches the sibling routes exactly and the published partner contract. Tests: - tests/shipping-v2-handler.test.mjs: two existing "non-PRO → 403" tests for register/list were using makeCtx() with no key, which now fails at the 401 layer first. Renamed to "no API key → 401 (tenant-isolation gate)" with a comment explaining the failure mode being tested. 18/18 pass. Verified: typecheck:api, lint:api-contract (no change), lint:boundaries, lint:rate-limit-policies, test:data (6005/6005). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(review HIGH 2/2): restore v1 path aliases on scenario + supply-chain (#3207) Koala flagged this as a merge blocker in PR #3242 review. Commits 6 + 7 of #3207 renamed five documented v1 URLs to the sebuf method-derived paths and deleted the legacy edge-function files: POST /api/scenario/v1/run → run-scenario GET /api/scenario/v1/status → get-scenario-status GET /api/scenario/v1/templates → list-scenario-templates GET /api/supply-chain/v1/country-products → get-country-products GET /api/supply-chain/v1/multi-sector-cost-shock → get-multi-sector-cost-shock server/router.ts is an exact static-match table (Map keyed on `METHOD PATH`), so any external caller — docs, partner scripts, grep-the- internet — hitting the old documented URL would 404 on first request after merge. Commit 8 (shipping/v2) preserved partner URLs byte-for- byte; the scenario + supply-chain renames missed that discipline. Fix: add five thin alias edge functions that rewrite the pathname to the canonical sebuf path and delegate to the domain [rpc].ts gateway via a new server/alias-rewrite.ts helper. Premium gating, rate limits, entitlement checks, and cache-tier lookups all fire on the canonical path — aliases are pure URL rewrites, not a duplicate handler pipeline. api/scenario/v1/{run,status,templates}.ts api/supply-chain/v1/{country-products,multi-sector-cost-shock}.ts Vite dev parity: file-based routing at api/ is a Vercel concern, so the dev middleware (vite.config.ts) gets a matching V1_ALIASES rewrite map before the router dispatch. Manifest: 5 new entries under `deferred` with removal_issue=#3282 (tracking their retirement at the next v1→v2 break). lint:api-contract stays green (89 files checked, 55 manifest entries validated). Docs: - docs/api-scenarios.mdx: migration callout at the top with the full old→new URL table and a link to the retirement issue. - CHANGELOG.md + docs/changelog.mdx: Changed entry documenting the rename + alias compat + the 202→200 shift (from commit23c821a1). Verified: typecheck:api, lint:api-contract, lint:rate-limit-policies, lint:boundaries, test:data (6005/6005). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
780 lines
51 KiB
Plaintext
780 lines
51 KiB
Plaintext
---
|
||
title: "Changelog"
|
||
description: "All notable changes to World Monitor, organized by release."
|
||
rss: true
|
||
---
|
||
|
||
All notable changes to World Monitor are documented here. Subscribe via [RSS](/changelog/rss.xml) to stay updated.
|
||
|
||
<Update label="Unreleased" description="Unreleased" tags={["Trade", "Intelligence", "Search", "Supply Chain", "Maritime"]}>
|
||
|
||
### Added
|
||
|
||
- **Route Explorer**: standalone full-screen modal (CMD+K) for planning shipments between any two countries. Current/Alternatives/Land/Impact tabs with keyboard-first navigation, URL state sharing, strategic-product trade data, dependency flags, and free-tier blur with public route highlight (#2980, #2982, #2994, #2996, #2997, #2998)
|
||
- **US Treasury customs revenue** in Trade Policy panel with monthly data, FYTD year-over-year comparison, and revenue spike highlighting (#1663)
|
||
- **Security advisories gold standard migration**: Railway cron seed fetches 24 government RSS feeds hourly, Vercel reads Redis only (#1637)
|
||
- **CMD+K full panel coverage**: all 55 panels now searchable (was 31), including AI forecasts, correlation panels, webcams, displacement, security advisories (#1656)
|
||
- Chokepoint transit intelligence with 3 free data sources: IMF PortWatch, CorridorRisk, AISStream (#1560)
|
||
- 13 monitored chokepoints (was 6): added Cape of Good Hope, Gibraltar, Bosporus Strait (absorbs Dardanelles), Korea, Dover, Kerch, Lombok (#1560, #1572)
|
||
- Expandable chokepoint cards with TradingView lightweight-charts 180-day time-series (#1560)
|
||
- Real-time transit counting with crossing detection on Railway relay (#1560)
|
||
- R2 trace storage for forecast debugging with Cloudflare API upload (#1655)
|
||
- `@ts-nocheck` injection in Makefile generate target for CI proto-freshness parity (#1637)
|
||
|
||
### Changed
|
||
|
||
- **Sebuf API migration (#3207)** — scenario + supply-chain endpoints moved to the typed sebuf contract. RPC URLs now derive from method names; five renamed v1 URLs remain live as thin aliases so existing integrations keep working:
|
||
- `/api/scenario/v1/run` → `/api/scenario/v1/run-scenario`
|
||
- `/api/scenario/v1/status` → `/api/scenario/v1/get-scenario-status`
|
||
- `/api/scenario/v1/templates` → `/api/scenario/v1/list-scenario-templates`
|
||
- `/api/supply-chain/v1/country-products` → `/api/supply-chain/v1/get-country-products`
|
||
- `/api/supply-chain/v1/multi-sector-cost-shock` → `/api/supply-chain/v1/get-multi-sector-cost-shock`
|
||
|
||
Aliases retire at the next v1→v2 break (tracked in [#3282](https://github.com/koala73/worldmonitor/issues/3282)).
|
||
|
||
- `POST /api/scenario/v1/run-scenario` now returns `200 OK` instead of the pre-migration `202 Accepted`. sebuf's HTTP annotations don't carry per-RPC status codes. Branch on response body `status === "pending"` instead of `response.status === 202`. `statusUrl` field is preserved.
|
||
|
||
### Security
|
||
|
||
- **CDN cache bypass closed**: `CDN-Cache-Control` header now only emitted for trusted origins (worldmonitor.app, Vercel previews, Tauri). No-origin server-side requests always invoke the edge function so `validateApiKey` runs, preventing a cached 200 from being served to external scrapers.
|
||
- **Shipping v2 webhook tenant isolation (#3242)**: `POST /api/v2/shipping/webhooks` and `GET /api/v2/shipping/webhooks` now enforce `validateApiKey(req, { forceKey: true })`, matching the sibling `[subscriberId]{,/[action]}` routes. Without this gate, a Clerk-authenticated pro user with no API key would collapse into a shared `'anon'` fingerprint bucket and see/overwrite webhooks owned by other `'anon'`-bucket tenants.
|
||
|
||
### Fixed
|
||
|
||
- Trade Policy panel WTO gate changed from panel-wide to per-tab, so Revenue tab works on desktop without WTO API key (#1663)
|
||
- Conflict-intel seed succeeds without ACLED credentials by accepting empty events when humanitarian/PizzINT data is available (#1651)
|
||
- Seed-forecasts crash from top-level `@aws-sdk/client-s3` import resolved with lazy dynamic import (#1654)
|
||
- Bootstrap desktop timeouts restored (5s/8s) while keeping aggressive web timeouts (1.2s/1.8s) (#1653)
|
||
- Service worker navigation reverted to NetworkOnly to prevent stale HTML caching on deploy (#1653)
|
||
- Railway seed watch paths fixed for 5 services (seed-insights, seed-unrest-events, seed-prediction-markets, seed-infra, seed-gpsjam)
|
||
- PortWatch ArcGIS URL, field names, and chokepoint name mappings (#1572)
|
||
|
||
</Update>
|
||
|
||
<Update label="v2.6.1 - March 11, 2026" description="v2.6.1" tags={["Blog", "Intelligence", "Satellite"]}>
|
||
|
||
### Highlights
|
||
|
||
- **Blog Platform** — Astro-powered blog at /blog with 16 SEO-optimized posts, OG images, and site footer (#1401, #1405, #1409)
|
||
- **Country Intelligence** — country facts section with right-click context menu (#1400)
|
||
- **Satellite Imagery Overhaul** — globe-native rendering, outline-only polygons, CSP fixes (#1381, #1385, #1376)
|
||
|
||
### Added
|
||
|
||
- Astro blog at /blog with 16 SEO posts and build integration (#1401, #1403)
|
||
- Blog redesign to match /pro page design system (#1405)
|
||
- Blog SEO, OG images, favicon fix, and site footer (#1409)
|
||
- Country facts section and right-click context menu for intel panel (#1400)
|
||
- Satellite imagery panel enabled in orbital surveillance layer (#1375)
|
||
- Globe-native satellite imagery, removed sidebar panel (#1381)
|
||
- Layer search filter with synonym support (#1369)
|
||
- Close buttons on panels and Add Panel block (#1354)
|
||
- Enterprise contact form endpoint (#1365)
|
||
- Commodity and happy variants shown on all header versions (#1407)
|
||
|
||
### Fixed
|
||
|
||
- NOTAM closures merged into Aviation layer (#1408)
|
||
- Intel deep dive layout reordered, duplicate headlines removed (#1404)
|
||
- Satellite imagery outline-only polygons to eliminate alpha stacking blue tint (#1385)
|
||
- Enterprise form hardened with mandatory fields and lead qualification (#1382)
|
||
- Country intel silently dismisses when geocode cannot identify a country (#1383)
|
||
- Globe hit targets enlarged for small marker types (#1378)
|
||
- Imagery panel hidden for existing users and viewport refetch deadlock (#1377)
|
||
- CSP violations for satellite preview images (#1376)
|
||
- Safari TypeError filtering and Sentry noise patterns (#1380)
|
||
- Swedish locale 'avbruten' TypeError variant filtered (#1402)
|
||
- Satellite imagery STAC backend fix, merged into Orbital Surveillance (#1364)
|
||
- Aviation "Computed" source replaced with specific labels, reduced cache TTLs (#1374)
|
||
- Close button and hover-pause on all marker tooltips (#1371)
|
||
- Invalid 'satelliteImagery' removed from LAYER_SYNONYMS (#1370)
|
||
- Risk scores seeding gap and seed-meta key mismatch (#1366)
|
||
- Consistent LIVE header pattern across news and webcams panels (#1367)
|
||
- Globe null guards in path accessor callbacks (#1372)
|
||
- Node_modules guard in pre-push hook, pinned Node 22 (#1368)
|
||
- Typecheck CI workflow: removed paths-ignore, added push trigger (#1373)
|
||
- Theme toggle removed from header (#1407)
|
||
|
||
</Update>
|
||
|
||
<Update label="v2.6.0 - March 09, 2026" description="v2.6.0" tags={["Satellite", "Finance", "Map", "Infrastructure", "Pro"]}>
|
||
|
||
### Highlights
|
||
|
||
- **Orbital Surveillance** — real-time satellite tracking layer with TLE propagation (#1278)
|
||
- **Premium Finance Suite** — stock analysis tools for Pro tier (#1268)
|
||
- **Self-hosted Basemap** — migrated from CARTO to PMTiles on Cloudflare R2 (#1064)
|
||
- **GPS Jamming v2** — migrated from gpsjam.org to Wingbits API with H3 hexagons (#1240)
|
||
- **Military Flights Overhaul** — centralized via Redis seed + edge handler with OpenSky/Wingbits fallbacks (#1263, #1274, #1275, #1276)
|
||
- **Pro Waitlist & Landing Page** — referral system, Turnstile CAPTCHA, 21-language localization (#1140, #1187)
|
||
- **Server-side AI Classification** — batch headline classification moves from client to server (#1195)
|
||
- **Commodity Variant** — new app variant focused on commodities with relevant panels & layers (#1040, #1100)
|
||
- **Health Check System** — comprehensive health endpoint with auto seed-meta freshness tracking (#1091, #1127, #1128)
|
||
|
||
### Added
|
||
|
||
- Orbital surveillance layer with real-time satellite tracking via satellite.js (#1278, #1281)
|
||
- Premium finance stock analysis suite for Pro tier (#1268)
|
||
- GPS jamming migration to Wingbits API with H3 hex grid (#1240)
|
||
- Commodity app variant with dedicated panels and map layers (#1040, #1100)
|
||
- Pro waitlist landing page with referral system and Turnstile CAPTCHA (#1140)
|
||
- Pro landing page localization — 21 languages (#1187)
|
||
- Pro page repositioning toward markets, macro & geopolitics (#1261)
|
||
- Referral invite banner when visiting via `?ref=` link (#1232)
|
||
- Server-side batch AI classification for news headlines (#1195)
|
||
- Self-hosted PMTiles basemap on Cloudflare R2, replacing CARTO (#1064)
|
||
- Per-provider map theme selector (#1101)
|
||
- Globe visual preset setting (Earth / Cosmos) with texture selection (#1090, #1076)
|
||
- Comprehensive health check endpoint for UptimeRobot (#1091)
|
||
- Auto seed-meta freshness tracking for all RPC handlers (#1127)
|
||
- Submarine cables expanded to 86 via TeleGeography API (#1224)
|
||
- Pak-Afghan conflict zone and country boundary override system (#1150)
|
||
- Sudan and Myanmar conflict zone polygon improvements (#1216)
|
||
- Iran events: 28 new location coords, 48h TTL (#1251)
|
||
- Tech HQs in Ireland data (#1244)
|
||
- BIS data seed job (#1131)
|
||
- CoinPaprika fallback for crypto/stablecoin data (#1092)
|
||
- Rudaw TV live stream and RSS feed (#1117)
|
||
- Dubai and Riyadh added to default airport watchlist (#1144)
|
||
- Cmd+K: 16 missing layer toggles (#1289), "See all commands" link with category list (#1270)
|
||
- UTM attribution tags on all outbound links (#1233)
|
||
- Performance warning dialog replaces hard layer limit (#1088)
|
||
- Unified error/retry UX with muted styling and countdown (#1115)
|
||
- Settings reorganized into collapsible groups (#1110)
|
||
- Reset Layout button with tooltip (#1267, #1250)
|
||
- Markdown lint in pre-push hook (#1166)
|
||
|
||
### Changed
|
||
|
||
- Military flights centralized via Redis seed + edge handler pattern (#1263)
|
||
- Military flights seed with OpenSky anonymous fallback + Wingbits fallback (#1274, #1275)
|
||
- Theater posture computed directly in relay instead of pinging Vercel RPC (#1259)
|
||
- Countries GeoJSON served from R2 CDN (#1164)
|
||
- Consolidated duplicated market data lists into shared JSON configs (#1212)
|
||
- Eliminate all frontend external API calls — enforce gold standard pattern (#1217)
|
||
- WB indicators seeded on Railway, never called from frontend (#1159, #1157)
|
||
- Temporal baseline for news + fires moved to server-side (#1194)
|
||
- Panel creation guarded by variant config (#1221)
|
||
- Panel tab styles unified to underline pattern across all panels (#1106, #1182, #1190, #1192)
|
||
- Reduce default map layers (#1141)
|
||
- Share dialog dismissals persist across subdomains via cookies (#1286)
|
||
- Country-wide conflict zones use actual country geometry (#1245)
|
||
- Aviation seed interval reduced to 1h (#1258)
|
||
- Replace curl with native Node.js HTTP CONNECT tunnel in seeds (#1287)
|
||
- Seed scripts use `_seed-utils.mjs` shared configs from `scripts/shared/` (#1231, #1234)
|
||
|
||
### Fixed
|
||
|
||
- **Rate Limiting**: prioritize `cf-connecting-ip` over `x-real-ip` for correct per-user rate limiting behind CF proxy (#1241)
|
||
- **Security**: harden cache keys against injection and hash collision (#1103), per-endpoint rate limits for summarize endpoints (#1161)
|
||
- **Map**: prevent ghost layers rendering without a toggle (#1264), DeckGL layer toggles getting stuck (#1248), auto-fallback to OpenFreeMap on basemap failure (#1109), CORS fallback for Carto basemap (#1142), use CORS-enabled R2 URL for PMTiles in Tauri (#1119), CII Instability layer disabled in 3D mode (#1292)
|
||
- **Layout**: reconcile ultrawide zones when map is hidden (#1246), keep settings button visible on scaled desktop widths (#1249), exit fullscreen before switching variants (#1253), apply map-hidden layout class on initial load (#1087), preserve panel column position across refresh (#1170, #1108, #1112)
|
||
- **Panels**: event delegation to survive setContent debounce (#1203), guard RPC response array access with optional chaining (#1174), clear stuck error headers and sanitize error messages (#1175), lazy panel race conditions + server feed gaps (#1113), Tech Readiness panel loading on full variant (#1208), Strategic Risk panel button listeners (#1214), World Clock green home row (#1202), Airline Intelligence CSS grid layouts (#1197)
|
||
- **Pro/Turnstile**: explicit rendering to fix widget race condition (#1189), invisible widget support (#1215), CSP allow Turnstile (#1155), handle `already_registered` state (#1183), reset on enterprise form error (#1222), registration feedback and referral code gen (#1229, #1228), no-cache header for /pro (#1179), correct API endpoint path (#1177), www redirect loop fix (#1198, #1201)
|
||
- **SEO**: comprehensive improvements for /pro and main pages (#1271)
|
||
- **Railway**: remove custom railpack.json install step causing ENOENT builds (#1296, #1290, #1288)
|
||
- **Aviation**: correct cancellation rate calculation and add 12 airports (#1209), unify NOTAM status logic (#1225)
|
||
- **Sentry**: triage 26 issues, fix 3 bugs, add 29 noise filters (#1173, #1098)
|
||
- **Health**: treat missing seed-meta as stale (#1128), resolve BIS credit and theater posture warnings (#1124), add WB seed loop (#1239), UCDP auth handling (#1252)
|
||
- **Country Brief**: formatting, duplication, and news cap fixes (#1219), prevent modal stuck on geocode failure (#1134)
|
||
- **Economic**: guard BIS and spending data against undefined (#1162, #1169)
|
||
- **Webcams**: detect blocked YouTube embeds on web (#1107), use iframe load event fallback (#1123), MTV Lebanon as live stream (#1122)
|
||
- **Desktop**: recover stranded routing fixes and unified error UX (#1160), DRY debounce, error handling, retry cap (#1084), debounce cache writes, batch secret push, lazy panels (#1077)
|
||
- **PWA**: bump SW nuke key to v2 for CF-cached 404s (#1081), one-time SW nuke on first visit (#1079)
|
||
- **Performance**: only show layer warning when adding layers, not removing (#1265), reduce unnecessary Vercel edge invocations (#1176)
|
||
- **i18n**: sync all 20 locales to en.json — zero drift (#1104), correct indentation for geocode error keys (#1147)
|
||
- **Insights**: graceful exit, LKG fallback, swap to Gemini 2.5 Flash (#1153, #1154)
|
||
- **Seeds**: prevent API quota burn and respect rate limits (#1167), gracefully skip write when validation fails (#1089), seed-meta tracking for all bootstrap keys (#1163, #1138)
|
||
|
||
</Update>
|
||
|
||
<Update label="v2.5.25 - March 04, 2026" description="v2.5.25" tags={["Finance", "Infrastructure"]}>
|
||
|
||
### Changed
|
||
|
||
- **Supply Chain v2** — bump chokepoints & minerals cache keys to v2; add `aisDisruptions` field to `ChokepointInfo` (proto, OpenAPI, generated types, handler, UI panel); rename Malacca Strait → Strait of Malacca; reduce chokepoint Redis TTL from 15 min to 5 min; expand description to always show warning + AIS disruption counts; remove Nickel & Copper from critical minerals data (focus on export-controlled minerals); slice top producers to 3; use full FRED series names for shipping indices; add `daily` cache tier (86400s) and move minerals route to it; align client-side circuit breaker TTLs with server TTLs; fix upstream-unavailable banner to only show when no data is present; register supply-chain routes in Vite dev server plugin
|
||
- **Cache migration**: old `supply_chain:chokepoints:v1` and `supply_chain:minerals:v1` Redis keys are no longer read by any consumer — they will expire via TTL with no action required
|
||
|
||
</Update>
|
||
|
||
<Update label="v2.5.24 - March 03, 2026" description="v2.5.24" tags={["Intelligence", "Security", "Performance", "UX"]}>
|
||
|
||
### Highlights
|
||
|
||
- **UCDP conflict data** — integrated Uppsala Conflict Data Program for historical & ongoing armed conflict tracking (#760)
|
||
- **Country brief sharing** — maximize mode, shareable URLs, native browser share button, expanded sections (#743, #854)
|
||
- **Unified Vercel deployment** — consolidated 4 separate deployments into 1 via runtime variant detection (#756)
|
||
- **CDN performance overhaul** — POST→GET conversion, per-domain edge functions, tiered bootstrap for ~46% egress reduction (#753, #795, #838)
|
||
- **Security hardening** — CSP script hashes replace unsafe-inline, crypto.randomUUID() for IDs, XSS-safe i18n, Finnhub token header (#781, #844, #861, #744)
|
||
- **i18n expansion** — French support with Live TV channels, hardcoded English strings replaced with translation keys (#794, #851, #839)
|
||
|
||
### Added
|
||
|
||
- UCDP (Uppsala Conflict Data Program) integration for armed conflict tracking (#760)
|
||
- Iran & Strait of Hormuz conflict zones, upgraded Ukraine polygon (#731)
|
||
- 100 Iran war events seeded with expanded geocoder (#792)
|
||
- Country brief maximize mode, shareable URLs, expanded sections & i18n (#743)
|
||
- Native browser share button for country briefs (#854)
|
||
- French i18n support with French Live TV channels (#851)
|
||
- Geo-restricted live channel support, restored WELT (#765)
|
||
- Manage Channels UX — toggle from grid + show all channels (#745)
|
||
- Command palette: disambiguate Map vs Panel commands, split country into map/brief (#736)
|
||
- Command palette: rotating contextual tips replace static empty state (#737)
|
||
- Download App button for web users with dropdown (#734, #735)
|
||
- Reset layout button to restore default panel sizes and order (#801)
|
||
- System status moved into settings (#735)
|
||
- Vercel cron to pre-warm AviationStack cache (#776)
|
||
- Runtime variant detection — consolidate 4 Vercel deployments into 1 (#756)
|
||
- CJS syntax check in pre-push hook (#769)
|
||
|
||
### Fixed
|
||
|
||
- **Security**: XSS — wrap `t()` calls in `escapeHtml()` (#861), use `crypto.randomUUID()` instead of `Math.random()` for ID generation (#844), move Finnhub API key from query string to `X-Finnhub-Token` header (#744)
|
||
- **i18n**: replace hardcoded English strings with translation keys (#839), i18n improvements (#794)
|
||
- **Market**: parse comma-separated query params and align Railway cache keys (#856), Railway market data cron + complete missing tech feed categories (#850), Yahoo relay fallback + RSS digest relay for blocked feeds (#835), tech UNAVAILABLE feeds + Yahoo batch early-exit + sector heatmap gate (#810)
|
||
- **Aviation**: move AviationStack fetching to Railway relay, reduce to 40 airports (#858)
|
||
- **UI**: cancel pending debounced calls on component destroy (#848), guard async operations against stale DOM references (#843)
|
||
- **Sentry**: guard stale DOM refs, audio.play() compat, add 16 noise filters (#855)
|
||
- **Relay**: exponential backoff for failing RSS feeds (#853), deduplicate UCDP constants crashing Railway container (#766)
|
||
- **API**: remove `[domain]` catch-all that intercepted all RPC routes (#753 regression) (#785), pageSize bounds validation on research handlers (#819), return 405 for wrong HTTP method (#757), pagination cursor for cyber threats (#754)
|
||
- **Conflict**: bump Iran events cache-bust to v7 (#724)
|
||
- **OREF**: prevent LLM translation cache from poisoning Hebrew→English pipeline (#733), strip translation labels from World Brief input (#768)
|
||
- **Military**: harden USNI fleet report ship name regex (#805)
|
||
- **Sidecar**: add required params to ACLED API key validation probe (#804)
|
||
- **Macro**: replace hardcoded BTC mining thresholds with Mayer Multiple (#750)
|
||
- **Cyber**: reduce GeoIP per-IP timeout from 3s to 1.5s (#748)
|
||
- **CSP**: restore unsafe-inline for Vercel bot-challenge pages (#788), add missing script hash and finance variant (#798)
|
||
- **Runtime**: route all /api/* calls through CDN edge instead of direct Vercel (#780)
|
||
- **Desktop**: detect Linux node target from host arch (#742), harden Windows installer update path + map resize (#739), close update toast after clicking download (#738), only open valid http(s) links externally (#723)
|
||
- **Webcams**: replace dead Tel Aviv live stream (#732), replace stale Jerusalem feed (#849)
|
||
- Story header uses full domain WORLDMONITOR.APP (#799)
|
||
- Open variant nav links in same window instead of new tab (#721)
|
||
- Suppress map renders during resize drag (#728)
|
||
- Append deduction panel to DOM after async import resolves (#764)
|
||
- Deduplicate stale-while-revalidate background fetches in CircuitBreaker (#793)
|
||
- CORS fallback, rate-limit bump, RSS proxy allowlist (#814)
|
||
- Unavailable stream error messages updated (#759)
|
||
|
||
### Performance
|
||
|
||
- Tier slow/fast bootstrap data for ~46% CDN egress reduction (#838)
|
||
- Convert POST RPCs to GET for CDN caching (#795)
|
||
- Split monolithic edge function into per-domain functions (#753)
|
||
- Increase CDN cache TTLs + add stale-if-error across edge functions (#777)
|
||
- Bump CDN cache TTLs for oref-alerts and youtube/live (#791)
|
||
- Skip wasted direct fetch for Vercel-blocked domains in RSS proxy (#815)
|
||
|
||
### Security
|
||
|
||
- Replace CSP unsafe-inline with script hashes and add trust signals (#781)
|
||
- Expand Permissions-Policy and tighten CSP connect-src (#779)
|
||
|
||
### Changed
|
||
|
||
- Extend support for larger screens (#740)
|
||
- Green download button + retire sliding popup (#747)
|
||
- Extract shared relay helper into `_relay.js` (#782)
|
||
- Consolidate `SummarizeArticleResponse` status fields (#813)
|
||
- Consolidate `declare const process` into shared `env.d.ts` (#752)
|
||
- Deduplicate `clampInt` into `server/_shared/constants`
|
||
- Add error logging for network errors in error mapper (#746)
|
||
- Redis error logging + reduced timeouts for edge functions (#749)
|
||
|
||
</Update>
|
||
|
||
<Update label="v2.5.21 - March 01, 2026" description="v2.5.21" tags={["Intelligence", "Aviation", "Military", "Infrastructure"]}>
|
||
|
||
### Highlights
|
||
|
||
- **Iran Attacks map layer** — conflict events with severity badges, related event popups, and CII integration (#511, #527, #547, #549)
|
||
- **Telegram Intel panel** — 27 curated OSINT channels via MTProto relay (#550)
|
||
- **OREF Israel Sirens** — real-time alerts with Hebrew→English translation and 24h history bootstrap (#545, #556, #582)
|
||
- **GPS/GNSS jamming layer** — detection overlay with CII integration (#570)
|
||
- **Day/night terminator** — solar terminator overlay on map (#529)
|
||
- **Breaking news alert banner** — audio alerts for critical/high RSS items with cooldown bypass (#508, #516, #533)
|
||
- **AviationStack integration** — global airport delays for 128 airports with NOTAM closure detection (#552, #581, #583)
|
||
- **Strategic risk score** — theater posture + breaking news wired into scoring algorithm (#584)
|
||
|
||
### Added
|
||
|
||
- Iran Attacks map layer with conflict event popups, severity badges, and priority rendering (#511, #527, #549)
|
||
- Telegram Intel panel with curated OSINT channel list (#550, #600)
|
||
- OREF Israel Sirens panel with Hebrew-to-English translation (#545, #556)
|
||
- OREF 24h history bootstrap on relay startup (#582)
|
||
- GPS/GNSS jamming detection map layer + CII integration (#570)
|
||
- Day/night solar terminator overlay (#529)
|
||
- Breaking news active alert banner with audio for critical/high items (#508)
|
||
- AviationStack integration for non-US airports + NOTAM closure detection (#552, #581, #583)
|
||
- RT (Russia Today) HLS livestream + RSS feeds (#585, #586)
|
||
- Iran webcams tab with 4 feeds (#569, #572, #601)
|
||
- CBC News optional live channel (#502)
|
||
- Strategic risk score wired to theater posture + breaking news (#584)
|
||
- CII scoring: security advisories, Iran strikes, OREF sirens, GPS jamming (#547, #559, #570, #579)
|
||
- Country brief + CII signal coverage expansion (#611)
|
||
- Server-side military bases with 125K+ entries + rate limiting (#496)
|
||
- AVIATIONSTACK_API key in desktop settings (#553)
|
||
- Iran events seed script and latest data (#575)
|
||
|
||
### Fixed
|
||
|
||
- **Aviation**: stale IndexedDB cache invalidation + reduced CDN TTL (#607), broken lock replaced with direct cache + cancellation tiers (#591), query all airports instead of rotating batch (#557), NOTAM routing through Railway relay (#599), always show all monitored airports (#603)
|
||
- **Telegram**: AUTH_KEY_DUPLICATED fixes — latch to stop retry spam (#543), 60s startup delay (#587), graceful shutdown + poll guard (#562), ESM import path fixes (#537, #542), missing relay auth headers (#590)
|
||
- **Relay**: Polymarket OOM prevention — circuit breaker + concurrency limiter (#519), request deduplication (#513), queue backpressure + response slicing (#593), cache stampede fix (#592), kill switch (#523); smart quotes crash (#563); graceful shutdown (#562, #565); curl for OREF (#546, #567, #571); maxBuffer ENOBUFS (#609); rsshub.app blocked (#526); ERR_HTTP_HEADERS_SENT guard (#509); Telegram memory cleanup (#531)
|
||
- **Live news**: 7 stale YouTube fallback IDs replaced (#535, #538), broken Europe channel handles (#541), eNCA handle + VTC NOW removal + CTI News (#604), RT HLS recovery (#610), YouTube proxy auth alignment (#554, #555), residential proxy + gzip for detection (#551)
|
||
- **Breaking news**: critical alerts bypass cooldown (#516), keyword gaps filled (#517, #521), fake pubDate filter (#517), SESSION_START gate removed (#533)
|
||
- **Threat classifier**: military/conflict keyword gaps + news-to-conflict bridge (#514), Groq 429 stagger (#520)
|
||
- **Geo**: tokenization-based matching to prevent false positives (#503), 60+ missing locations in hub index (#528)
|
||
- **Iran**: CDN cache-bust pipeline v4 (#524, #532, #544), read-only handler (#518), Gulf misattribution via bbox disambiguation (#532)
|
||
- **CII**: Gulf country strike misattribution (#564), compound escalation for military action (#548)
|
||
- **Bootstrap**: 401/429 rate limiting fix (#512), hydration cache + polling hardening (#504)
|
||
- **Sentry**: guard YT player methods + GM/InvalidState noise (#602), Android OEM WebView bridge injection (#510), setView invalid preset (#580), beforeSend null-filename leak (#561)
|
||
- Rate limiting raised to 300 req/min sliding window (#515)
|
||
- Vercel preview origin regex generalized + bases cache key (#506)
|
||
- Cross-env for Windows-compatible npm scripts (#499)
|
||
- Download banner repositioned to bottom-right (#536)
|
||
- Stale/expired Polymarket markets filtered (#507)
|
||
- Cyber GeoIP centroid fallback jitter made deterministic (#498)
|
||
- Cache-control headers hardened for polymarket and rss-proxy (#613)
|
||
|
||
### Performance
|
||
|
||
- Server-side military base fetches: debounce + static edge cache tier (#497)
|
||
- RSS: refresh interval raised to 10min, cache TTL to 20min (#612)
|
||
- Polymarket cache TTL raised to 10 minutes (#568)
|
||
|
||
### Changed
|
||
|
||
- Stripped 61 debug console.log calls from 20 service files (#501)
|
||
- Bumped version to 2.5.21 (#605)
|
||
|
||
</Update>
|
||
|
||
<Update label="v2.5.20 - February 27, 2026" description="v2.5.20" tags={["Performance", "Security", "API"]}>
|
||
|
||
### Added
|
||
|
||
- **Edge caching**: Complete Cloudflare edge cache tier coverage with degraded-response policy (#484)
|
||
- **Edge caching**: Cloudflare edge caching for proxy.worldmonitor.app (#478) and api.worldmonitor.app (#471)
|
||
- **Edge caching**: Tiered edge Cache-Control aligned to upstream TTLs (#474)
|
||
- **API migration**: Convert 52 API endpoints from POST to GET for edge caching (#468)
|
||
- **Gateway**: Configurable VITE_WS_API_URL + harden POST-to-GET shim (#480)
|
||
- **Cache**: Negative-result caching for cachedFetchJson (#466)
|
||
- **Security advisories**: New panel with government travel alerts (#460)
|
||
- **Settings**: Redesign settings window with VS Code-style sidebar layout (#461)
|
||
|
||
### Fixed
|
||
|
||
- **Commodities panel**: Was showing stocks instead of commodities — circuit breaker SWR returned stale data from a different call when cacheTtlMs=0 (#483)
|
||
- **Analytics**: Use greedy regex in PostHog ingest rewrites (#481)
|
||
- **Sentry**: Add noise filters for 4 unresolved issues (#479)
|
||
- **Gateway**: Convert stale POST requests to GET for backwards compat (#477)
|
||
- **Desktop**: Enable click-to-play YouTube embeds + CISA feed fixes (#476)
|
||
- **Tech variant**: Use rss() for CISA feed, drop build from pre-push hook (#475)
|
||
- **Security advisories**: Route feeds through RSS proxy to avoid CORS blocks (#473)
|
||
- **API routing**: Move 5 path-param endpoints to query params for Vercel routing (#472)
|
||
- **Beta**: Eagerly load T5-small model when beta mode is enabled
|
||
- **Scripts**: Handle escaped apostrophes in feed name regex (#455)
|
||
- **Wingbits**: Add 5-minute backoff on /v1/flights failures (#459)
|
||
- **Ollama**: Strip thinking tokens, raise max_tokens, fix panel summary cache (#456)
|
||
- **RSS/HLS**: RSS feed repairs, HLS native playback, summarization cache fix (#452)
|
||
|
||
### Performance
|
||
|
||
- **AIS proxy**: Increase AIS snapshot edge TTL from 2s to 10s (#482)
|
||
|
||
</Update>
|
||
|
||
<Update label="v2.5.10 - February 26, 2026" description="v2.5.10" tags={["Finance", "Desktop", "UX"]}>
|
||
|
||
### Fixed
|
||
|
||
- **Yahoo Finance rate-limit UX**: Show "rate limited — retrying shortly" instead of generic "Failed to load" on Markets, ETF, Commodities, and Sector panels when Yahoo returns 429 (#407)
|
||
- **Sequential Yahoo calls**: Replace `Promise.all` with staggered batching in commodity quotes, ETF flows, and macro signals to prevent 429 rate limiting (#406)
|
||
- **Sector heatmap Yahoo fallback**: Sector data now loads via Yahoo Finance when `FINNHUB_API_KEY` is missing (#406)
|
||
- **Finnhub-to-Yahoo fallback**: Market quotes route Finnhub symbols through Yahoo when API key is not configured (#407)
|
||
- **ETF early-exit on rate limit**: Skip retry loop and show rate-limit message immediately instead of waiting 60s (#407)
|
||
- **Sidecar auth resilience**: 401-retry with token refresh for stale sidecar tokens after restart; `diagFetch` auth helper for settings window diagnostics (#407)
|
||
- **Verbose toggle persistence**: Write verbose state to writable data directory instead of read-only app bundle on macOS (#407)
|
||
- **AI summary verbosity**: Tighten prompts to 2 sentences / 60 words max with `max_tokens` reduced from 150 to 100 (#404)
|
||
- **Settings modal title**: Rename from "PANELS" to "SETTINGS" across all 17 locales (#403)
|
||
- **Sentry noise filters**: CSS.escape() for news ID selectors, player.destroy guard, 11 new ignoreErrors patterns, blob: URL extension frame filter (#402)
|
||
|
||
</Update>
|
||
|
||
<Update label="v2.5.6 - February 23, 2026" description="v2.5.6" tags={["UX", "Performance", "Maritime"]}>
|
||
|
||
### Added
|
||
|
||
- **Greek (Ελληνικά) locale** — full translation of all 1,397 i18n keys (#256)
|
||
- **Nigeria RSS feeds** — 5 new sources: Premium Times, Vanguard, Channels TV, Daily Trust, ThisDay Live
|
||
- **Greek locale feeds** — Naftemporiki, in.gr, iefimerida.gr for Greek-language news coverage
|
||
- **Brasil Paralelo source** — Brazilian news with RSS feed and source tier (#260)
|
||
|
||
### Performance
|
||
|
||
- **AIS relay optimization** — backpressure queue with configurable watermarks, spatial indexing for chokepoint detection (O(chokepoints) vs O(chokepoints × vessels)), pre-serialized + pre-gzipped snapshot cache eliminating per-request JSON.stringify + gzip CPU (#266)
|
||
|
||
### Fixed
|
||
|
||
- **Vietnam flag country code** — corrected flag emoji in language selector (#245)
|
||
- **Sentry noise filters** — added patterns for SW FetchEvent, PostHog ingest; enabled SW POST method for PostHog analytics (#246)
|
||
- **Service Worker same-origin routing** — restricted SW route patterns to same-origin only, preventing cross-origin fetch interception (#247, #251)
|
||
- **Social preview bot allowlisting** — whitelisted Twitterbot, facebookexternalhit, and other crawlers on OG image assets (#251)
|
||
- **Windows CORS for Tauri** — allow `http://` origin from `tauri.localhost` for Windows desktop builds (#262)
|
||
- **Linux AppImage GLib crash** — fix GLib symbol mismatch on newer distros by bundling compatible libraries (#263)
|
||
|
||
</Update>
|
||
|
||
<Update label="v2.5.2 - February 21, 2026" description="v2.5.2" tags={["Map", "UX"]}>
|
||
|
||
### Fixed
|
||
|
||
- **QuotaExceededError handling** — detect storage quota exhaustion and stop further writes to localStorage/IndexedDB instead of silently failing; shared `markStorageQuotaExceeded()` flag across persistent-cache and utility storage
|
||
- **deck.gl null.getProjection crash** — wrap `setProps()` calls in try/catch to survive map mid-teardown races in debounced/RAF callbacks
|
||
- **MapLibre "Style is not done loading"** — guard `setFilter()` in mousemove/mouseout handlers during theme switches
|
||
- **YouTube invalid video ID** — validate video ID format (`/^[\w-]{10,12}$/`) before passing to IFrame Player constructor
|
||
- **Vercel build skip on empty SHA** — guard `ignoreCommand` against unset `VERCEL_GIT_PREVIOUS_SHA` (first deploy, force deploy) which caused `git diff` to fail and cancel builds
|
||
- **Sentry noise filters** — added 7 patterns: iOS readonly property, SW FetchEvent, toLowerCase/trim/indexOf injections, QuotaExceededError
|
||
|
||
</Update>
|
||
|
||
<Update label="v2.5.1 - February 20, 2026" description="v2.5.1" tags={["Performance", "Security", "API"]}>
|
||
|
||
### Performance
|
||
|
||
- **Batch FRED API requests** — frontend now sends a single request with comma-separated series IDs instead of 7 parallel edge function invocations, eliminating Vercel 25s timeouts
|
||
- **Parallel UCDP page fetches** — replaced sequential loop with Promise.all for up to 12 pages, cutting fetch time from ~96s worst-case to ~8s
|
||
- **Bot protection middleware** — blocks known social-media crawlers from hitting API routes, reducing unnecessary edge function invocations
|
||
- **Extended API cache TTLs** — country-intel 12h→24h, GDELT 2h→4h, nuclear 12h→24h; Vercel ignoreCommand skips non-code deploys
|
||
|
||
### Fixed
|
||
|
||
- **Partial UCDP cache poisoning** — failed page fetches no longer silently produce incomplete results cached for 6h; partial results get 10-min TTL in both Redis and memory, with `partial: true` flag propagated to CDN cache headers
|
||
- **FRED upstream error masking** — single-series failures now return 502 instead of empty 200; batch mode surfaces per-series errors and returns 502 when all fail
|
||
- **Sentry `Load failed` filter** — widened regex from `^TypeError: Load failed$` to `^TypeError: Load failed( \(.*\))?$` to catch host-suffixed variants (e.g., gamma-api.polymarket.com)
|
||
- **Tooltip XSS hardening** — replaced `rawHtml()` with `safeHtml()` allowlist sanitizer for panel info tooltips
|
||
- **UCDP country endpoint** — added missing HTTP method guards (OPTIONS/GET)
|
||
- **Middleware exact path matching** — social preview bot allowlist uses `Set.has()` instead of `startsWith()` prefix matching
|
||
|
||
### Changed
|
||
|
||
- FRED batch API supports up to 15 comma-separated series IDs with deduplication
|
||
- Missing FRED API key returns 200 with `X-Data-Status: skipped-no-api-key` header instead of silent empty response
|
||
- LAYER_TO_SOURCE config extracted from duplicate inline mappings into shared constant
|
||
|
||
</Update>
|
||
|
||
<Update label="v2.5.0 - February 20, 2026" description="v2.5.0" tags={["AI", "Desktop", "Security"]}>
|
||
|
||
### Highlights
|
||
|
||
**Local LLM Support (Ollama / LM Studio)** — Run AI summarization entirely on your own hardware with zero cloud dependency. The desktop app auto-discovers models from any OpenAI-compatible local inference server (Ollama, LM Studio, llama.cpp, vLLM) and populates a selection dropdown. A 4-tier fallback chain ensures summaries always generate: Local LLM → Groq → OpenRouter → browser-side T5. Combined with the Tauri desktop app, this enables fully air-gapped intelligence analysis where no data leaves your machine.
|
||
|
||
### Added
|
||
|
||
- **Ollama / LM Studio integration** — local AI summarization via OpenAI-compatible `/v1/chat/completions` endpoint with automatic model discovery, embedding model filtering, and fallback to manual text input
|
||
- **4-tier summarization fallback chain** — Ollama (local) → Groq (cloud) → OpenRouter (cloud) → Transformers.js T5 (browser), each with 5-second timeout before silently advancing to the next
|
||
- **Shared summarization handler factory** — all three API tiers use identical logic for headline deduplication (Jaccard >0.6), variant-aware prompting, language-aware output, and Redis caching (`summary:v3:{mode}:{variant}:{lang}:{hash}`)
|
||
- **Settings window with 3 tabs** — dedicated **LLMs** tab (Ollama endpoint/model, Groq, OpenRouter), **API Keys** tab (12+ data source credentials), and **Debug & Logs** tab (traffic log, verbose mode, log file access). Each tab runs an independent verification pipeline
|
||
- **Consolidated keychain vault** — all desktop secrets stored as a single JSON blob in one OS keychain entry (`secrets-vault`), reducing macOS Keychain authorization prompts from 20+ to exactly 1 on app startup. One-time auto-migration from individual entries with cleanup
|
||
- **Cross-window secret synchronization** — saving credentials in the Settings window immediately syncs to the main dashboard via `localStorage` broadcast, with no app restart needed
|
||
- **API key verification pipeline** — each credential is validated against its provider's actual API endpoint. Network errors (timeouts, DNS failures) soft-pass to prevent transient failures from blocking key storage; only explicit 401/403 marks a key invalid
|
||
- **Plaintext URL inputs** — endpoint URLs (Ollama API, relay URLs, model names) display as readable text instead of masked password dots in Settings
|
||
- **5 new defense/intel RSS feeds** — Military Times, Task & Purpose, USNI News, Oryx OSINT, UK Ministry of Defence
|
||
- **Koeberg nuclear power plant** — added to the nuclear facilities map layer (the only commercial reactor in Africa, Cape Town, South Africa)
|
||
- **Privacy & Offline Architecture** documentation — README now details the three privacy levels: full cloud, desktop with cloud APIs, and air-gapped local with Ollama
|
||
- **AI Summarization Chain** documentation — README includes provider fallback flow diagram and detailed explanation of headline deduplication, variant-aware prompting, and cross-user cache deduplication
|
||
|
||
### Changed
|
||
|
||
- AI fallback chain now starts with Ollama (local) before cloud providers
|
||
- Feature toggles increased from 14 to 15 (added AI/Ollama)
|
||
- Desktop architecture uses consolidated vault instead of per-key keychain entries
|
||
- README expanded with ~85 lines of new content covering local LLM support, privacy architecture, summarization chain internals, and desktop readiness framework
|
||
|
||
### Fixed
|
||
|
||
- URL and model fields in Settings display as plaintext instead of masked password dots
|
||
- OpenAI-compatible endpoint flow hardened for Ollama/LM Studio response format differences (thinking tokens, missing `choices` array edge cases)
|
||
- Sentry null guard for `getProjection()` crash with 6 additional noise filters
|
||
- PathLayer cache cleared on layer toggle-off to prevent stale WebGL buffer rendering
|
||
|
||
</Update>
|
||
|
||
<Update label="v2.4.1 - February 19, 2026" description="v2.4.1" tags={["Map", "Infrastructure"]}>
|
||
|
||
### Fixed
|
||
|
||
- **Map PathLayer cache**: Clear PathLayer on toggle-off to prevent stale WebGL buffers
|
||
- **Sentry noise**: Null guard for `getProjection()` crash and 6 additional noise filters
|
||
- **Markdown docs**: Resolve lint errors in documentation files
|
||
|
||
</Update>
|
||
|
||
<Update label="v2.4.0 - February 19, 2026" description="v2.4.0" tags={["UX", "Mobile", "Map"]}>
|
||
|
||
### Added
|
||
|
||
- **Live Webcams Panel**: 2x2 grid of live YouTube webcam feeds from global hotspots with region filters (Middle East, Europe, Asia-Pacific, Americas), grid/single view toggle, idle detection, and full i18n support (#111)
|
||
- **Linux download**: added `.AppImage` option to download banner
|
||
|
||
### Changed
|
||
|
||
- **Mobile detection**: use viewport width only for mobile detection; touch-capable notebooks (e.g. ROG Flow X13) now get desktop layout (#113)
|
||
- **Webcam feeds**: curated Tel Aviv, Mecca, LA, Miami; replaced dead Tokyo feed; diverse ALL grid with Jerusalem, Tehran, Kyiv, Washington
|
||
|
||
### Fixed
|
||
|
||
- **Le Monde RSS**: English feed URL updated (`/en/rss/full.xml` → `/en/rss/une.xml`) to fix 404
|
||
- **Workbox precache**: added `html` to `globPatterns` so `navigateFallback` works for offline PWA
|
||
- **Panel ordering**: one-time migration ensures Live Webcams follows Live News for existing users
|
||
- **Mobile popups**: improved sheet/touch/controls layout (#109)
|
||
- **Intelligence alerts**: disabled on mobile to reduce noise (#110)
|
||
- **RSS proxy**: added 8 missing domains to allowlist
|
||
- **HTML tags**: repaired malformed tags in panel template literals
|
||
- **ML worker**: wrapped `unloadModel()` in try/catch to prevent unhandled timeout rejections
|
||
- **YouTube player**: optional chaining on `playVideo?.()` / `pauseVideo?.()` for initialization race
|
||
- **Panel drag**: guarded `.closest()` on non-Element event targets
|
||
- **Beta mode**: resolved race condition and timeout failures
|
||
- **Sentry noise**: added filters for Firefox `too much recursion`, maplibre `_layers`/`id`/`type` null crashes
|
||
|
||
</Update>
|
||
|
||
<Update label="v2.3.9 - February 18, 2026" description="v2.3.9" tags={["UX", "Infrastructure", "Desktop"]}>
|
||
|
||
### Added
|
||
|
||
- **Full internationalization (14 locales)**: English, French, German, Spanish, Italian, Polish, Portuguese, Dutch, Swedish, Russian, Arabic, Chinese Simplified, Japanese — each with 1100+ translated keys
|
||
- **RTL support**: Arabic locale with `dir="rtl"`, dedicated RTL CSS overrides, regional language code normalization (e.g. `ar-SA` correctly triggers RTL)
|
||
- **Language switcher**: in-app locale picker with flag icons, persists to localStorage
|
||
- **i18n infrastructure**: i18next with browser language detection and English fallback
|
||
- **Community discussion widget**: floating pill linking to GitHub Discussions with delayed appearance and permanent dismiss
|
||
- **Linux AppImage**: added `ubuntu-22.04` to CI build matrix with webkit2gtk/appindicator dependencies
|
||
- **NHK World and Nikkei Asia**: added RSS feeds for Japan news coverage
|
||
- **Intelligence Findings badge toggle**: option to disable the findings badge in the UI
|
||
|
||
### Changed
|
||
|
||
- **Zero hardcoded English**: all UI text routed through `t()` — panels, modals, tooltips, popups, map legends, alert templates, signal descriptions
|
||
- **Trending proper-noun detection**: improved mid-sentence capitalization heuristic with all-caps fallback when ML classifier is unavailable
|
||
- **Stopword suppression**: added missing English stopwords to trending keyword filter
|
||
|
||
### Fixed
|
||
|
||
- **Dead UTC clock**: removed `#timeDisplay` element that permanently displayed `--:--:-- UTC`
|
||
- **Community widget duplicates**: added DOM idempotency guard preventing duplicate widgets on repeated news refresh cycles
|
||
- **Settings help text**: suppressed raw i18n key paths rendering when translation is missing
|
||
- **Intelligence Findings badge**: fixed toggle state and listener lifecycle
|
||
- **Context menu styles**: restored intel-findings context menu styles
|
||
- **CSS theme variables**: defined missing `--panel-bg` and `--panel-border` variables
|
||
|
||
</Update>
|
||
|
||
<Update label="v2.3.8 - February 17, 2026" description="v2.3.8" tags={["Finance", "Desktop"]}>
|
||
|
||
### Added
|
||
|
||
- **Finance variant**: Added a dedicated market-first variant (`finance.worldmonitor.app`) with finance/trading-focused feeds, panels, and map defaults
|
||
- **Finance desktop profile**: Added finance-specific desktop config and build profile for Tauri packaging
|
||
|
||
### Changed
|
||
|
||
- **Variant feed loading**: `loadNews` now enumerates categories dynamically and stages category fetches with bounded concurrency across variants
|
||
- **Feed resilience**: Replaced direct MarketWatch RSS usage in finance/full/tech paths with Google News-backed fallback queries
|
||
- **Classification pressure controls**: Tightened AI classification budgets for tech/full and tuned per-feed caps to reduce startup burst pressure
|
||
- **Timeline behavior**: Wired timeline filtering consistently across map and news panels
|
||
- **AI summarization defaults**: Switched OpenRouter summarization to auto-routed free-tier model selection
|
||
|
||
### Fixed
|
||
|
||
- **Finance panel parity**: Kept data-rich panels while adding news panels for finance instead of removing core data surfaces
|
||
- **Desktop finance map parity**: Finance variant now runs first-class Deck.GL map/layer behavior on desktop runtime
|
||
- **Polymarket fallback**: Added one-time direct connectivity probe and memoized fallback to prevent repeated `ERR_CONNECTION_RESET` storms
|
||
- **FRED fallback behavior**: Missing `FRED_API_KEY` now returns graceful empty payloads instead of repeated hard 500s
|
||
- **Preview CSP tooling**: Allowed `https://vercel.live` script in CSP so Vercel preview feedback injection is not blocked
|
||
- **Trending quality**: Suppressed noisy generic finance terms in keyword spike detection
|
||
- **Mobile UX**: Hidden desktop download prompt on mobile devices
|
||
|
||
</Update>
|
||
|
||
<Update label="v2.3.7 - February 16, 2026" description="v2.3.7" tags={["UX", "Desktop", "Map"]}>
|
||
|
||
### Added
|
||
|
||
- **Full light mode theme**: Complete light/dark theme system with CSS custom properties, ThemeManager module, FOUC prevention, and `getCSSColor()` utility for theme-aware inline styles
|
||
- **Theme-aware maps and charts**: Deck.GL basemap, overlay layers, and CountryTimeline charts respond to theme changes in real time
|
||
- **Dark/light mode header toggle**: Sun/moon icon in the header bar for quick theme switching, replacing the duplicate UTC clock
|
||
- **Desktop update checker**: Architecture-aware download links for macOS (ARM/Intel) and Windows
|
||
- **Node.js bundled in Tauri installer**: Sidecar no longer requires system Node.js
|
||
- **Markdown linting**: Added markdownlint config and CI workflow
|
||
|
||
### Changed
|
||
|
||
- **Panels modal**: Reverted from "Settings" back to "Panels" — removed redundant Appearance section now that header has theme toggle
|
||
- **Default panels**: Enabled UCDP Conflict Events, UNHCR Displacement, Climate Anomalies, and Population Exposure panels by default
|
||
|
||
### Fixed
|
||
|
||
- **CORS for Tauri desktop**: Fixed CORS issues for desktop app requests
|
||
- **Markets panel**: Keep Yahoo-backed data visible when Finnhub API key is skipped
|
||
- **Windows UNC paths**: Preserve extended-length path prefix when sanitizing sidecar script path
|
||
- **Light mode readability**: Darkened neon semantic colors and overlay backgrounds for light mode contrast
|
||
|
||
</Update>
|
||
|
||
<Update label="v2.3.6 - February 16, 2026" description="v2.3.6" tags={["Desktop"]}>
|
||
|
||
### Fixed
|
||
|
||
- **Windows console window**: Hide the `node.exe` console window that appeared alongside the desktop app on Windows
|
||
|
||
</Update>
|
||
|
||
<Update label="v2.3.5 - February 16, 2026" description="v2.3.5" tags={["UX", "Desktop"]}>
|
||
|
||
### Changed
|
||
|
||
- **Panel error messages**: Differentiated error messages per panel so users see context-specific guidance instead of generic failures
|
||
- **Desktop config auto-hide**: Desktop configuration panel automatically hides on web deployments where it is not relevant
|
||
|
||
</Update>
|
||
|
||
<Update label="v2.3.4 - February 16, 2026" description="v2.3.4" tags={["Desktop"]}>
|
||
|
||
### Fixed
|
||
|
||
- **Windows sidecar crash**: Strip `\\?\` UNC extended-length prefix from paths before passing to Node.js — Tauri `resource_dir()` on Windows returns UNC-prefixed paths that cause `EISDIR: lstat 'C:'` in Node.js module resolution
|
||
- **Windows sidecar CWD**: Set explicit `current_dir` on the Node.js Command to prevent bare drive-letter working directory issues from NSIS shortcut launcher
|
||
- **Sidecar package scope**: Add `package.json` with `"type": "module"` to sidecar directory, preventing Node.js from walking up the entire directory tree during ESM scope resolution
|
||
|
||
</Update>
|
||
|
||
<Update label="v2.3.3 - February 16, 2026" description="v2.3.3" tags={["Desktop", "Security"]}>
|
||
|
||
### Fixed
|
||
|
||
- **Keychain persistence**: Enable `apple-native` (macOS) and `windows-native` (Windows) features for the `keyring` crate — v3 ships with no default platform backends, so API keys were stored in-memory only and lost on restart
|
||
- **Settings key verification**: Soft-pass network errors during API key verification so transient sidecar failures don't block saving
|
||
- **Resilient keychain reads**: Use `Promise.allSettled` in `loadDesktopSecrets` so a single key failure doesn't discard all loaded secrets
|
||
- **Settings window capabilities**: Add `"settings"` to Tauri capabilities window list for core plugin permissions
|
||
- **Input preservation**: Capture unsaved input values before DOM re-render in settings panel
|
||
|
||
</Update>
|
||
|
||
<Update label="v2.3.0 - February 15, 2026" description="v2.3.0" tags={["Security", "Infrastructure"]}>
|
||
|
||
### Security
|
||
|
||
- **CORS hardening**: Tighten Vercel preview deployment regex to block origin spoofing (`worldmonitorEVIL.vercel.app`)
|
||
- **Sidecar auth bypass**: Move `/api/local-env-update` behind `LOCAL_API_TOKEN` auth check
|
||
- **Env key allowlist**: Restrict sidecar env mutations to 18 known secret keys (matching `SUPPORTED_SECRET_KEYS`)
|
||
- **postMessage validation**: Add `origin` and `source` checks on incoming messages in LiveNewsPanel
|
||
- **postMessage targetOrigin**: Replace wildcard `'*'` with specific embed origin
|
||
- **CORS enforcement**: Add `isDisallowedOrigin()` check to 25+ API endpoints that were missing it
|
||
- **Custom CORS migration**: Migrate `gdelt-geo` and `eia` from custom CORS to shared `_cors.js` module
|
||
- **New CORS coverage**: Add CORS headers + origin check to `firms-fires`, `stock-index`, `youtube/live`
|
||
- **YouTube embed origins**: Tighten `ALLOWED_ORIGINS` regex in `youtube/embed.js`
|
||
- **CSP hardening**: Remove `'unsafe-inline'` from `script-src` in both `index.html` and `tauri.conf.json`
|
||
- **iframe sandbox**: Add `sandbox="allow-scripts allow-same-origin allow-presentation"` to YouTube embed iframe
|
||
- **Meta tag validation**: Validate URL query params with regex allowlist in `parseStoryParams()`
|
||
|
||
### Fixed
|
||
|
||
- **Service worker stale assets**: Add `skipWaiting`, `clientsClaim`, and `cleanupOutdatedCaches` to workbox config — fixes `NS_ERROR_CORRUPTED_CONTENT` / MIME type errors when users have a cached SW serving old HTML after redeployment
|
||
|
||
</Update>
|
||
|
||
<Update label="v2.2.6 - February 14, 2026" description="v2.2.6" tags={["UX", "Infrastructure"]}>
|
||
|
||
### Fixed
|
||
|
||
- Filter trending noise and fix sidecar auth
|
||
- Restore tech variant panels
|
||
- Remove Market Radar and Economic Data panels from tech variant
|
||
|
||
### Docs
|
||
|
||
- Add developer X/Twitter link to Support section
|
||
- Add cyber threat API keys to `.env.example`
|
||
|
||
</Update>
|
||
|
||
<Update label="v2.2.5 - February 13, 2026" description="v2.2.5" tags={["Security", "API"]}>
|
||
|
||
### Security
|
||
|
||
- Migrate all Vercel edge functions to CORS allowlist
|
||
- Restrict Railway relay CORS to allowed origins only
|
||
|
||
### Fixed
|
||
|
||
- Hide desktop config panel on web
|
||
- Route World Bank & Polymarket via Railway relay
|
||
|
||
</Update>
|
||
|
||
<Update label="v2.2.3 - February 12, 2026" description="v2.2.3" tags={["Intelligence", "Map", "UX"]}>
|
||
|
||
### Added
|
||
|
||
- Cyber threat intelligence map layer (Feodo Tracker, URLhaus, C2IntelFeeds, OTX, AbuseIPDB)
|
||
- Trending keyword spike detection with end-to-end flow
|
||
- Download desktop app slide-in banner for web visitors
|
||
- Country briefs in Cmd+K search
|
||
|
||
### Changed
|
||
|
||
- Redesign 4 panels with table layouts and scoped styles
|
||
- Redesign population exposure panel and reorder UCDP columns
|
||
- Dramatically increase cyber threat map density
|
||
|
||
### Fixed
|
||
|
||
- Resolve z-index conflict between pinned map and panels grid
|
||
- Cap geo enrichment at 12s timeout, prevent duplicate download banners
|
||
- Replace ipwho.is/ipapi.co with ipinfo.io/freeipapi.com for geo enrichment
|
||
- Harden trending spike processing and optimize hot paths
|
||
- Improve cyber threat tooltip/popup UX and dot visibility
|
||
|
||
</Update>
|
||
|
||
<Update label="v2.2.2 - February 10, 2026" description="v2.2.2" tags={["Intelligence", "Performance", "UX"]}>
|
||
|
||
### Added
|
||
|
||
- Full-page Country Brief Page replacing modal overlay
|
||
- Download redirect API for platform-specific installers
|
||
|
||
### Fixed
|
||
|
||
- Normalize country name from GeoJSON to canonical TIER1 name
|
||
- Tighten headline relevance, add Top News section, compact markets
|
||
- Hide desktop config panel on web, fix irrelevant prediction markets
|
||
- Tone down climate anomalies heatmap to stop obscuring other layers
|
||
- macOS: hide window on close instead of quitting
|
||
|
||
### Performance
|
||
|
||
- Reduce idle CPU from pulse animation loop
|
||
- Harden regression guardrails in CI, cache, and map clustering
|
||
|
||
</Update>
|
||
|
||
<Update label="v2.2.1 - February 08, 2026" description="v2.2.1" tags={["Desktop", "Infrastructure"]}>
|
||
|
||
### Fixed
|
||
|
||
- Consolidate variant naming and fix PWA tile caching
|
||
- Windows settings window: async command, no menu bar, no white flash
|
||
- Constrain layers menu height in DeckGLMap
|
||
- Allow Cloudflare Insights script in CSP
|
||
- macOS build failures when Apple signing secrets are missing
|
||
|
||
</Update>
|
||
|
||
<Update label="v2.2.0 - February 07, 2026" description="v2.2.0" tags={["Desktop", "Map", "Intelligence"]}>
|
||
|
||
Initial v2.2 release with multi-variant support (World + Tech), desktop app (Tauri), and comprehensive geopolitical intelligence features.
|
||
|
||
</Update>
|