Commit Graph

189 Commits

Author SHA1 Message Date
Elie Habib
406afb0276 chore(vercel): tighten ignore script to allowlist web-relevant paths (#1427)
Switch from exclusion-based (build everything except docs) to
allowlist-based (only build when src/, api/, server/, config files
change). Skips builds for changes to src-tauri/, docker/, deploy/,
convex/, data/, docs/, e2e/, scripts/, .github/, .claude/, tests/.

With 378 feature branches triggering preview deploys, this should
significantly reduce the 99 build-hours burned in 3 days.
2026-03-11 21:02:48 +04:00
Elie Habib
b737105226 chore: skip preview deploys for non-PR branches (#1430)
Checks VERCEL_GIT_PULL_REQUEST_ID before proceeding.
Branch pushes without an open PR are skipped (exit 0),
eliminating wasted build minutes from 378+ feature branches.
Production (main) always builds.
2026-03-11 19:10:25 +04:00
Elie Habib
16ed3271d3 feat(iran): add 24 new locations for Iran events geocoding (#1414)
Borujerd, Lamerd, Chabahar, Shahrekord, Parand, Rabat Karim, Shahriar,
Punak, Bonab, Ghaniabad, Beit Shemesh, Bnei Brak, Quneitra, Khan
Arnabeh, Ruwais, Mehrshahr, Qaim, Prince Sultan, Ramat David, Vietnam,
South Korea, Ilam, Kerman, Lorestan.
2026-03-11 14:54:59 +04:00
Elie Habib
0efd81dcf4 chore(railway): add watch paths utility to prevent unnecessary redeploys (#1416)
Seed services and relay were redeploying on every push (blog, frontend, etc)
because no watchPatterns were configured. Added utility script that sets
watchPatterns via Railway GraphQL API so services only redeploy when their
actual source files change. Already applied to all 23 services.
2026-03-11 13:15:21 +04:00
Elie Habib
b65464c0b2 feat(blog): add Astro blog at /blog with 16 SEO posts (#1401)
* feat(blog): add Astro blog at /blog with 16 SEO-optimized posts

Adds a static Astro blog built during Vercel deploy and served at
worldmonitor.app/blog. Includes 16 marketing/SEO posts covering
features, use cases, and comparisons from customer perspectives.

- blog-site/: Astro static site with content collections, RSS, sitemap
- Vercel build pipeline: build:blog builds Astro and copies to public/blog/
- vercel.json: exclude /blog from SPA catch-all rewrite and no-cache headers
- vercel.json: ignoreCommand triggers deploy on blog-site/ changes
- Cache: /blog/_astro/* immutable, blog HTML uses Vercel defaults

* fix(blog): fix markdown lint errors in blog posts

Add blank lines around headings (MD022) and lists (MD032) across
all 16 blog post files to pass markdownlint checks.

* fix(ci): move ignoreCommand to script to stay under 256 char limit

Vercel schema validates ignoreCommand max length at 256 characters.
Move the logic to scripts/vercel-ignore.sh and reference it inline.

* fix(blog): address PR review findings

- Add blog sitemap to robots.txt for SEO discovery
- Use www.worldmonitor.app consistently (canonical domain)
- Clean public/blog/ before copy to prevent stale files
- Use npm ci for hermetic CI builds

* fix(blog): move blog dependency install to postinstall phase

Separates dependency installation from compilation. Blog deps are
now installed during npm install (postinstall hook), not during build.
2026-03-11 08:20:56 +04:00
Elie Habib
e3fa980163 fix(aviation): replace vague "Computed" source with specific labels, reduce cache TTLs, remove simulated delays (#1374)
- Add AVIATIONSTACK and NOTAM proto enum values for accurate source attribution
- AviationStack flight data alerts now show "Flight Data" instead of "Computed"
- NOTAM closure/restriction alerts now show "NOTAM"
- Remove generateSimulatedDelay() fallback that produced fake random alerts
- Reduce all aviation cache TTLs from 2h to 30min for fresher data
- Reduce relay seed interval from 1h to 30min, TTL from 4h to 1h
- Reduce seed freshness threshold from 45min to 20min
- Update health check maxStaleMin from 90 to 60min
- Update all 21 locale files with new source labels
2026-03-10 10:45:07 +04:00
Elie Habib
601a1028a4 fix(health): fix riskScores seeding gap and seed-meta key mismatch (#1366)
* fix(health): fix riskScores seeding gap and seed-meta key mismatch

- Switch RPC handler to cachedFetchJsonWithMeta so stale key is refreshed
  on every successful response (cache hit or miss), not just cache misses
- Fix seed-meta key mismatch: health.js and seed-health.js now check
  seed-meta:risk:scores:sebuf (matching what cachedFetchJson writes)
- Add warm-ping loop in relay (8min interval) to keep RPC cache fresh
- Remove dead startCiiSeedLoop and 345 lines of unused CII seed code

* fix(scoring): await stale key write to prevent edge runtime drop

Edge/serverless runtimes may terminate the isolate before a
fire-and-forget Redis write completes. Await the setCachedJson
call so the stale key TTL is guaranteed to be extended.
2026-03-10 08:34:48 +04:00
Elie Habib
78e7ae546e feat(natural): add tropical cyclone tracking from NHC and GDACS (#1357)
* feat(natural): add tropical cyclone tracking from NHC and GDACS

Integrate NHC ArcGIS REST API (15 storm slots across AT/EP/CP basins)
and GDACS TC field extraction to provide real-time tropical cyclone data
with forecast tracks, uncertainty cones, and historical track paths.

- Proto: add optional TC fields (storm_id, wind_kt, pressure_mb, etc.)
  plus ForecastPoint, PastTrackPoint, CoordRing messages
- Server/seed: NHC two-pass query (forecast points then detail layers),
  GDACS wind/pressure parsing, Saffir-Simpson classification, dedup
  strategy (NHC > GDACS > EONET), pressureMb validation (850-1050),
  advisory date with Number.isFinite guard
- Globe: dashed red forecast track, per-segment wind-colored past track,
  semi-transparent orange forecast cone polygon
- Popup: TC details panel with color-coded category badge, wind/pressure
- Frontend mapper: forward all TC fields, convert CoordRing to number[][][]

* fix(natural): improve GDACS dedup, NHC classification, and TC popup i18n

- GDACS dedup now checks name + geographic proximity instead of name-only
- NHC classification uses stormtype field for subtropical/post-tropical
- TC popup labels use t() for localization instead of hardcoded English

* feat(map): add cyclone-specific deck.gl layers for 2D map

- Storm center ScatterplotLayer with Saffir-Simpson wind coloring
- Past track PathLayer with per-segment wind-speed color ramp
- Forecast track PathLayer with dashed line via PathStyleExtension
- Cone PolygonLayer for forecast uncertainty visualization
- Tooltip and click routing for all new storm layer IDs

* fix(map): remove click routing for synthetic storm track/cone layers

Track and cone layers carry lightweight objects without full NaturalEvent
fields. Clicking them would pass incomplete data to the popup renderer.
Only storm-centers-layer (which holds the full NaturalEvent) routes to
the natEvent popup. Tracks and cones remain tooltip-only.

* fix(map): attach parent NaturalEvent to synthetic storm layers for clicks

Synthetic track/cone objects now carry _event reference to the parent
NaturalEvent. Click handler unwraps _event before passing to popup,
so clicking any storm element opens the full TC popup.
2026-03-10 07:23:07 +04:00
Elie Habib
fc134647a5 feat(map): add NOTAM overlay + satellite imagery integration (#1356)
* feat(map): add NOTAM overlay + satellite imagery integration

NOTAM Overlay:
- Expand airport monitoring from MENA-only to 64 global airports
- Add ScatterplotLayer (55km red rings) on flat map for airspace closures
- Add CSS-pulsing ring markers on globe for closures
- Independent of flights layer toggle (works when flights OFF)
- Bump NOTAM cache key v1 to v2

Satellite Imagery:
- Add Capella SAR STAC catalog proxy at /api/imagery/v1
- SSRF protection via URL allowlist + bbox/datetime validation
- SatelliteImageryPanel with preview thumbnails and scene metadata
- PolygonLayer footprints on flat map with viewport-triggered search
- Polygon footprints on globe with "Search this area" button
- Full variant only, default disabled

Layer key propagation across all 23+ files including variants,
harnesses, registry, URL state, and renderer channels.

* fix(imagery): wire panel data flow, fix viewport race, add datetime filter

P1 fixes:
- Imagery scenes now flow through MapContainer.setOnImageryUpdate()
  callback, making data available to both renderers and panel
- Add version guard to fetchImageryForViewport() preventing stale
  responses from overwriting newer viewport data
- Wire SatelliteImageryPanel.update() and setOnSearchArea() in
  panel-layout.ts (panel was previously unhooked)
- Globe mode "Search this area" fetches via MapContainer.getBbox()

P2 fix:
- search-imagery.ts now filters STAC items by datetime range when
  the client provides the datetime parameter

Also:
- Add MapContainer.getBbox() for viewport-aware imagery fetching
- Add DeckGLMap.getBbox() public method
- Data-loader layer toggle triggers initial imagery fetch

* fix(imagery): complete source filter + fix date-only end bound

- Filter STAC items by constellation when source param is provided,
  making the API contract match actual behavior
- Date-only end bounds (YYYY-MM-DD without T) now include the full
  day (23:59:59.999Z) instead of only midnight
2026-03-10 07:19:02 +04:00
Elie Habib
c5d196f29e feat(scoring): port frontend CII scoring formulas to server (#1351)
* feat(scoring): port frontend CII scoring formulas to server

Port the frontend's proven scoring formulas (log2/sqrt scaling, fatality
splits, outage/GPS severity tiers, OREF integration, advisory floors and
boosts) to the server-side CII computation so scores are data-driven and
self-correcting.

- Add MX, BR, AE to TIER1_COUNTRIES (21 to 24)
- Disable relay CII seed loop (RPC computes on-demand)
- Add activeAlertCount to OREF Redis payload
- Expand CountrySignals with fatality split, outage tiers, GPS severity,
  OREF fields, advisory level, and high severity strikes
- Port calcUnrestScore (log2 dampening, protest fatality boost, outage
  severity tiers)
- Port calcConflictScore (weighted ACLED events, sqrt fatalities, OREF
  boost, strike severity)
- Port calcSecurityScore (GPS high/medium weighting, cap 35)
- Add advisory floors (do-not-travel 60, reconsider 50) and boosts
- Add OREF blend boost for IL
- Fix fires fallback (empty array is truthy) and climate severity
  nullish coalescing
- Add 14 fixture tests covering floors, boosts, scaling, and edge cases

* docs: update CII scoring documentation to match server-side formulas

Update ALGORITHMS.md and DOCUMENTATION.md to reflect the ported scoring
formulas: 24 tier-1 countries, log2/sqrt scaling, outage/GPS severity
tiers, OREF integration, advisory floors/boosts, and data source table.
2026-03-09 23:36:56 +04:00
Elie Habib
e8cb0f99f2 data(iran): add 5 new location coords and seed March 9 events (#1340)
Add Yehud, Sitra, Sanandaj, Ma'ameer, Northern Cyprus to
LOCATION_COORDS for geolocating new Iran conflict events.
74 events seeded to Redis from LiveUAMap import.
2026-03-09 16:10:44 +04:00
Elie Habib
5493697143 feat(pro): early access promotional banner on dashboard (#1301)
* fix(seed): add new locations and day-ago parsing for Iran events

Add 11 new location coordinates (al-Kharj, Petah Tikva, Beersheba,
Oman, Oslo, Aghdasiyeh, Rey, Beirut, Azraq) and support "a day ago"
/ "N days ago" relative time parsing.

* feat(pro): add early access promotional banner to dashboard

Thin, dismissible top banner promoting WorldMonitor Pro with
"Reserve your spot" CTA linking to /pro. Dismisses for 7 days
via localStorage timestamp. Slide-down animation, responsive,
light/dark theme compatible via CSS variables.
2026-03-09 00:38:32 +04:00
Elie Habib
dd490026b4 fix(satellites): remove dead filter patterns with no public TLEs (#1297)
Remove patterns that match zero satellites in CelesTrak:
- OFEK/EROS (Israel), IGS (Japan) — classified
- LACROSSE/TOPAZ (US NRO) — retired/listed as USA-*
- KONDOR/PERSONA/BARS-M/RESURS-P (Russia) — listed as COSMOS 2xxx
- HISEA/SUPERVIEW (China), CSO-/HELIOS (France) — not in groups
- RISAT/EOS-0x (India) — not in resource group
2026-03-09 00:34:08 +04:00
Elie Habib
cf557746a1 fix(seed): tighten military callsign patterns to reduce commercial false positives (#1294)
Remove patterns that match commercial airline ICAO codes (CCA=China Airlines,
CHH=Hainan Airlines, SVA=Saudia, ELAL=El Al, THK/TUR=Turkish civil, RF=too broad).
Add COMMERCIAL_CALLSIGNS blocklist. Require digit suffixes on ambiguous prefixes
(SAM, PAT, EGY, etc). Narrow overly broad hex ranges for Spain, UAE, Qatar, Canada.
Replace removed patterns with precise military equivalents (PLAAF, TURAF, RSD).
2026-03-08 23:42:12 +04:00
Elie Habib
7896d1a856 feat(satellites): widen intel filter with more nations and active group (#1295)
- Add CelesTrak 'active' group (~6000 sats, filtered down)
- Add Israeli (OFEK, EROS), Indian (RISAT, CARTOSAT, EOS), Japanese (IGS),
  Turkish (GOKTURK, RASAT), French (CSO, HELIOS), US NRO (LACROSSE, TOPAZ, USA-*)
- Add Russian (KONDOR, PERSONA, BARS-M, RESURS-P), Chinese (HISEA, SUPERVIEW, ZIYUAN)
- Widen COSMOS regex to 2[4-9]xx for newer Russian recon sats
- Add country colors for IL, IN, JP, TR on globe
2026-03-08 23:41:55 +04:00
Elie Habib
1bd4d7b4a0 fix(seeds): replace curl with native Node.js HTTP CONNECT tunnel (#1287)
Railway Nixpacks images don't include curl. Replaced curlFetchJson()
with proxyFetchJson() using Node.js http/https/tls modules for
HTTP CONNECT proxy tunneling to OpenSky.
2026-03-08 22:23:18 +04:00
Elie Habib
9772548d83 feat: add orbital surveillance layer with real-time satellite tracking (#1278)
Track ~80-120 intelligence-relevant satellites on the 3D globe using CelesTrak
TLE data and client-side SGP4 propagation (satellite.js). Satellites render at
actual orbital altitude with country-coded colors, 15-min orbit trails, and
ground footprint projections.

Architecture: Railway seeds TLEs every 2h → Redis → Vercel CDN (1h cache) →
browser does SGP4 math every 3s (zero server cost for real-time movement).

- New relay seed loop (ais-relay.cjs) fetching military + resource groups
- New edge handler (api/satellites.js) with 10min cache + negative cache
- Frontend service with circuit breaker and propagation lifecycle
- GlobeMap integration: markers, trails (pathsData), footprints, tooltips
- Layer registry as globe-only "Orbital Surveillance" with i18n (21 locales)
- Full documentation at docs/ORBITAL_SURVEILLANCE.md with roadmap
- Fix pre-existing SearchModal TS error (non-null assertion)
2026-03-08 21:55:46 +04:00
Elie Habib
fbdb0be3a0 fix(seeds): correct confidence level and theater posture freshness key (#1277)
* fix(seeds): correct confidence level and theater posture freshness key

- Callsign-only matches now get 'medium' confidence (was always 'high')
- Theater posture seed-meta key matches relay format 'seed-meta:theater-posture'

* fix(search): narrow array access type for TS strictness
2026-03-08 15:46:50 +04:00
Elie Habib
0d9bbb5f66 feat(seeds): standalone military flights seed + relay cleanup (#1276)
* feat(seeds): standalone military flights seed + relay cleanup

- Create scripts/seed-military-flights.mjs as standalone Railway cron seed
  with 3-tier fallback: OpenSky auth → OpenSky anonymous → Wingbits
- Remove military flights seed from ais-relay.cjs (452 lines)
- Theater posture seed remains in relay with its own OpenSky + Wingbits fallback
- Standalone seed writes military:flights:v1, stale, and theater posture keys

* feat(opensky): HTTP CONNECT tunnel via residential proxy + better logging

- New OPENSKY_PROXY_AUTH env var (falls back to OREF_PROXY_AUTH)
- _openskyProxyConnect() helper for HTTP CONNECT tunneling in relay
- Updated _attemptOpenSkyTokenFetch() and _openskyRawFetch() to route
  through proxy when OPENSKY_PROXY_AUTH is set
- /opensky-diag now shows proxyEnabled status
- Startup log shows (via proxy) or (direct)
- seed-military-flights.mjs: curl-based proxy for OpenSky auth + anon
- seed-military-flights.mjs: verbose Wingbits logging (response shape,
  per-area flight counts, sample data) to debug 0-aircraft issue
- Better HTTP error logging: status code + response body on non-2xx

* fix(wingbits): use correct response key 'data' instead of 'flights'

Wingbits API returns { alias, data: [...] } not { flights: [...] }.
This caused 0 aircraft from Wingbits in both standalone seed and relay
theater posture. Also fixed field mappings: 'c' (country), 'ra' (timestamp),
'og' (onGround) match actual Wingbits response format.

Verified locally: 3761 raw → 52 military matches from WESTERN region alone.

* fix(wingbits): correct field mapping + wingbits-first fetch order

- 'c' field is internal Wingbits classification (A0-C2), NOT country code
  Removed from originCountry mapping to avoid false matches
- Wingbits now tier 1 (no proxy, fast, reliable), OpenSky supplements
  via proxy as tier 2/3 for additional aircraft coverage
- Verified: Wingbits returns altitude in feet, speed in knots already
  (ft→m→ft round-trip through unified pipeline is correct)
2026-03-08 15:37:40 +04:00
Elie Habib
23dd018e05 fix(relay): add Wingbits fallback for military flights seed (#1275)
When OpenSky is unavailable (both proxy auth and anonymous API),
Wingbits now serves as fallback. When both sources are available,
Wingbits supplements OpenSky with additional aircraft (deduped by
icao24). Wingbits data is converted to OpenSky state array format
for unified processing through the existing military detection pipeline.
2026-03-08 14:42:39 +04:00
Elie Habib
6bb0e122d1 fix(relay): anonymous OpenSky fallback for military flights seed (#1274)
When Railway's OpenSky OAuth2 auth times out (IP-level rate limiting),
the military flights seed now falls back to OpenSky's anonymous public
API endpoint directly. This ensures data flows even when the
authenticated proxy is blocked.
2026-03-08 14:36:59 +04:00
Elie Habib
09fc20fbdf fix: remove smartraveller.gov.au from RSS allowed domains (#1273)
Persistent relay timeouts from smartraveller.gov.au. Remove both
bare and www variants from all three allowed-domains files.
2026-03-08 14:35:50 +04:00
Elie Habib
554f5d408c fix(relay): centralize military flights via Redis seed + edge handler (#1263)
* fix(relay): compute theater posture directly instead of pinging Vercel RPC

The relay's theater posture seed loop was pinging the Vercel RPC endpoint
which itself needed WS_RELAY_URL to proxy OpenSky back through the relay —
a circular dependency that resulted in empty theaters.

Now the relay fetches OpenSky directly via its own localhost proxy,
applies military callsign filtering, computes postures for all 9 theaters,
and writes all 3 Redis keys (live/stale/backup) + seed-meta directly.
Wingbits API serves as fallback when OpenSky is unavailable.

* feat(relay): seed military flights to Redis, rewire frontend to read from edge handler

Relay fetches OpenSky every 5 min, filters by ICAO hex ranges (29) and
callsign patterns (83), converts units (m→ft, m/s→kts), and writes to
Redis. Frontend reads pre-filtered data via /api/military-flights edge
handler instead of proxying through OpenSky directly — eliminates
cascading 429 rate limits.

- Add seedMilitaryFlights() with in-flight mutex, 2-region fetch, empty guard
- Add api/military-flights.js edge handler (Redis → stale fallback)
- Rewrite src/services/military-flights.ts to fetchFromRedis()
- Add health.js tracking (STANDALONE_KEYS, SEED_META, CASCADE_GROUPS)
- Theater posture consumes shared flight data (no duplicate OpenSky calls)

* fix(relay): theater posture starvation, aircraftType downgrade, and openskyRelay gate

- Move lastMilFlightsSeedMs after successful posture passthrough so the
  standalone fallback runs when passthrough keeps erroring
- Use f.aircraftType from military seed instead of re-classifying via
  theaterDetectAircraftType (which has no fighter branch)
- Add militaryFlights feature flag with no required secrets; swap gate
  from openskyRelay so desktop users get Redis-backed data without
  legacy OpenSky credentials

* fix(relay): add fighter branch to theaterDetectAircraftType

The standalone theater posture fallback path could not classify fighter
callsigns (BOLT, VIPER, RAPTOR, etc.), causing them to be counted as
unknown and underreporting strike_capable posture levels.

* fix(military-flights): preserve OpenSky direct path for desktop, Redis for web

Desktop (Tauri) cannot reach /api/military-flights (Vercel edge handler).
Restore the original OpenSky direct fetch path for desktop mode while
using the new Redis-backed path for web/cloud. Route based on
isDesktopRuntime() — desktop uses openskyRelay feature gate + direct
OpenSky, web uses militaryFlights feature gate + Redis edge handler.
2026-03-08 14:17:21 +04:00
Elie Habib
f72ace7d67 fix(relay): compute theater posture directly instead of pinging Vercel RPC (#1259)
The relay's theater posture seed loop was pinging the Vercel RPC endpoint
which itself needed WS_RELAY_URL to proxy OpenSky back through the relay —
a circular dependency that resulted in empty theaters.

Now the relay fetches OpenSky directly via its own localhost proxy,
applies military callsign filtering, computes postures for all 9 theaters,
and writes all 3 Redis keys (live/stale/backup) + seed-meta directly.
Wingbits API serves as fallback when OpenSky is unavailable.
2026-03-08 10:00:14 +04:00
Elie Habib
218addd61d fix(health): reduce aviation seed interval to 1h, align maxStaleMin (#1258) 2026-03-08 09:29:14 +04:00
Elie Habib
adc9c462de fix(scripts): sync package-lock.json with h3-js dependency (#1257) 2026-03-08 09:00:45 +04:00
Elie Habib
d6c9176213 Revert "fix(scripts): sync package-lock.json with h3-js dependency (#1254)" (#1256)
This reverts commit 4816e27d3c.
2026-03-08 08:57:20 +04:00
Elie Habib
4816e27d3c fix(scripts): sync package-lock.json with h3-js dependency (#1254)
* Add premium stock analysis for finance variant

* fix(scripts): sync package-lock.json with h3-js dependency

Railway npm ci requires lock file in sync with package.json.

* fix(market): narrow undefined check for TS strict null safety
2026-03-08 08:45:12 +04:00
Elie Habib
406f3b118c fix(health): UCDP auth handling, insights TTL, gpsjam h3-js dep (#1252)
* fix(health): improve UCDP auth error handling, fix insights TTL

- UCDP: detect 401/403 and string error responses with clear message
  about missing UCDP_ACCESS_TOKEN env var (API now requires auth)
- Insights: increase cache TTL from 600s (10min) to 1800s (30min) to
  match health maxStaleMin and survive missed/delayed cron runs

* fix(health): use Promise.any for UCDP version discovery

UCDP v25.1 API hangs (30s timeout) while v24.1 works fine.
Promise.allSettled waited for ALL candidates to settle, wasting 30s.
Promise.any returns as soon as the first version succeeds (~3s for v24.1).

Also adds empty-result check in discovery to skip versions that return
0 events (v24.1 data only goes through 2023).

* fix(scripts): add h3-js dependency for Railway gpsjam cron service

fetch-gpsjam.mjs imports h3-js for H3 hex → lat/lon conversion.
scripts/package.json needs it since Railway builds from rootDirectory: "scripts".
2026-03-08 08:34:44 +04:00
Elie Habib
b72a999156 feat(iran-events): add 28 location coords and bump TTL to 48h (#1251)
Add geocoding for Riyadh, Haifa, Sulaimaniyah, Yazd, Qazvin, Kish,
Mehran, Jubail, Shaybah, Al Dhafra, Juffair, Qeshm, and others.
Increase Redis TTL from 24h to 48h for longer event retention.
2026-03-08 08:18:09 +04:00
Elie Habib
2f7fd6421f feat(gpsjam): migrate GPS jamming from gpsjam.org to Wingbits API (#1240)
* feat(gpsjam): migrate GPS jamming from gpsjam.org to Wingbits API

Replace gpsjam.org CSV scraping with Wingbits customer API for GPS/GNSS
interference data. This is a proper API with structured JSON responses
instead of fragile web scraping.

Key changes:
- Rewrite fetch-gpsjam.mjs seeder for Wingbits API (x-api-key auth)
- Delete ~150-line gpsjam seed loop from ais-relay.cjs (now standalone cron)
- Simplify api/gpsjam.js to Redis-only reads with v1→v2 fallback
- Update data shape: pct/good/bad/total → npAvg/sampleCount/aircraftCount
- Redis key: intelligence:gpsjam:v1 → v2 (with dual-write transition)
- Add vite dev plugin for local development
- Update all frontend components (MapPopup, DeckGLMap, GlobeMap, locales)

Zero-downtime: seeder dual-writes both v1 and v2 keys, edge handler
falls back to v1 with shape normalization. Remove v1 code after 24-72h.

* fix(gpsjam): improve v1 fallback normalization and update all locale files

- v1 fallback now derives npAvg from severity thresholds (high: 0.3,
  medium: 0.8) instead of hardcoding 0, uses bad/total for counts
- Update all 20 non-English locale files to use new gpsJamming keys
  (navPerformance, samples, aircraft) with English fallback values

* fix(gpsjam): dual-write v1 in old schema shape and catch Redis errors

- Seeder now converts v2 data back to v1 shape (pct/good/bad/total)
  for the dual-write, so old deployments and rollbacks parse correctly
- Edge handler wraps readFromRedis calls in try-catch so network/timeout
  errors return graceful 503 instead of platform 500s
2026-03-08 02:15:34 +04:00
Elie Habib
131813847d fix(health): add WB seed loop, fix missing seed-meta writes, unblock CF (#1239)
- Add World Bank seed loop to ais-relay (24h interval) for techReadiness,
  progressData, and renewableEnergy — previously manual-only with no cron
- Add seed-meta writes for UCDP and GPSJAM relay loops so health endpoint
  can track freshness (was always showing STALE/unknown)
- Fix theater posture and service statuses RPC URLs from worldmonitor.app
  to api.worldmonitor.app to bypass Cloudflare bot protection (403)
- Adjust UCDP maxStaleMin from 60 to 420 to match 6h relay interval
2026-03-08 01:19:02 +04:00
Elie Habib
1324f7ee58 fix(scripts): commit shared configs for Railway deploy (#1234)
Railway rootDirectory isolates build context — postinstall cp from
../shared/ fails because parent dirs aren't in the Nixpacks image.
Commit JSON/CJS configs directly into scripts/shared/.

- Remove useless postinstall from scripts/package.json
- Remove scripts/shared/ from .gitignore
- Commit all shared config files into scripts/shared/
- Add sync test to catch drift between shared/ and scripts/shared/
2026-03-08 00:24:33 +04:00
Elie Habib
364e497bd1 fix(scripts): resolve shared JSON configs for Railway rootDirectory (#1231)
Railway deploys seed services with rootDirectory=scripts/, placing files
at /app/ without the parent shared/ directory. The createRequire +
require('../shared/X.json') pattern resolves to /shared/ which doesn't
exist in the container.

- Add loadSharedConfig() to _seed-utils.mjs: tries ../shared/ (local)
  then ./shared/ (Railway) with clear error on miss
- Add requireShared() to ais-relay.cjs with same dual-path fallback
- Add postinstall to scripts/package.json that copies ../shared/ into
  ./shared/ during Railway build
- Update all 6 seed scripts to use loadSharedConfig instead of
  createRequire + require
- Add scripts/shared/ to .gitignore

Fixes crash introduced by #1212 (shared JSON consolidation).
2026-03-08 00:09:24 +04:00
Elie Habib
ddda4fc151 fix(aviation): unify NOTAM status logic between map and ops table (#1225)
* fix(aviation): unify NOTAM status logic between map and ops table

Both endpoints now use a shared NOTAM loader (seed-first with live
fallback) so they see the same closure snapshot. When an airport has
a NOTAM *and* real flight data, the new mergeNotamWithExistingAlert()
preserves observed stats instead of hard-replacing with severe/closure.
This fixes DXB showing NORMAL in the ops table but SEVERE on the map.

- Extract loadNotamClosures() to _shared.ts (used by both handlers)
- Add mergeNotamWithExistingAlert() for NOTAM + flight data merge
- Align ops-summary severity: NOTAM floor = moderate (operating) or
  severe (no flights), matching the map's merge logic
- Fix OMAD → OMAA ICAO typo in seed MENA list (AUH)

* fix: resolve strict tsconfig type errors in API build

* fix(aviation): preserve closure delayType for NOTAM-closed airports

Downstream consumers (MapPopup, data-loader, country-instability) rely
on delayType === 'closure' as the only closure signal. Always set
delayType to closure in mergeNotamWithExistingAlert() since the NOTAM
confirms the airport is closed — severity is still nuanced by flight data.
2026-03-07 22:38:06 +04:00
Elie Habib
20af4e55b0 fix: eliminate frontend external API calls, enforce gold standard pattern (#1217)
* fix: eliminate frontend external API calls, enforce gold standard pattern

- Polymarket: remove browser fan-out (536→105 lines), bootstrap → RPC only
- USASpending: remove direct API calls, read from bootstrap hydration
- NWS Weather: remove direct API calls, read from bootstrap hydration
- Nominatim: proxy through api/reverse-geocode.js with Redis cache + SSRF clamping
- Add seed scripts for weather alerts (15min) and spending (60min)
- Wire both seed loops into ais-relay.cjs
- Register weatherAlerts + spending in bootstrap.js and health.js
- Add 4 missing standalone keys to health.js (cyberThreatsRpc, militaryBases, temporalAnomalies, displacement)

* fix: resolve reload regressions and null-cache poisoning from #1217

- Weather/Spending: fall back to `/api/bootstrap?keys=` on scheduled
  reloads after the one-shot `getHydratedData()` is consumed
- Prediction: add client-side bootstrap filter for country markets
  when RPC fails (server skips bootstrap for query-based requests)
- Reverse-geocode: restore abort/timeout guard so transient network
  errors don't permanently poison the in-memory cache
2026-03-07 22:37:36 +04:00
Elie Habib
cad6b9c4e0 feat(infrastructure): expand submarine cables to 86 via TeleGeography API (#1224)
* feat(infrastructure): expand submarine cables to 86 via TeleGeography API seed

- Add `seed-submarine-cables.mjs` Railway cron script fetching 86 strategic
  cables from TeleGeography API (was 19 hand-curated)
- Update `geo.ts` static baseline with full cable data (routes, landing points,
  owners, RFS year, regions)
- Update `get-cable-health.ts` cable name/landing mappings for new slug-based IDs
- Add `data?.cables?.length` to `_seed-utils.mjs` record count heuristic
- Update `map-harness.ts` cable ID references
- Remove GitHub Actions workflows for UCDP and WB indicators (Railway cron only)

* fix(infrastructure): cable route matching, name false positives, validation threshold

- Fix route geometry: only strip numeric suffix when result matches a known
  cable slug, preventing seamewe-6→seamewe, farice-1→farice, etc.
- Fix name matching: use word-boundary regex instead of substring includes;
  disambiguate short names (ACE→ACE CABLE, SAFE→SAFE CABLE, PEACE→PEACE CABLE,
  TEAMS→TEAMS CABLE) to prevent false matches on common NGA words
- Raise validation threshold from 50 to 75 (88% success required) to prevent
  heavily partial upstream results from overwriting good cached data

* fix(infrastructure): tie validation threshold to 90% of configured cable count

Dynamic threshold based on CABLE_REGIONS length instead of a hardcoded number.
Currently requires >= 78 of 86 cables (90%).
2026-03-07 22:24:58 +04:00
Elie Habib
dd127447c0 refactor: consolidate duplicated market data lists into shared JSON configs (#1212)
Adding a new item (crypto, ETF, stablecoin, gulf symbol, etc.) previously
required editing 2-4 files because the same list was hardcoded independently
in seed scripts, RPC handlers, and frontend config. Following the proven
shared/crypto.json pattern, extract 6 new shared JSON configs so each list
has a single source of truth.

New shared configs:
- shared/stablecoins.json (ids + coinpaprika mappings)
- shared/etfs.json (BTC spot ETF tickers + issuers)
- shared/gulf.json (GCC indices, currencies, oil benchmarks)
- shared/sectors.json (sector ETF symbols + names)
- shared/commodities.json (VIX, gold, oil, gas, silver, copper)
- shared/stocks.json (market symbols + yahoo-only set)

All seed scripts, RPC handlers, and frontend config now import from
these shared JSON files instead of maintaining independent copies.
2026-03-07 22:00:55 +04:00
Elie Habib
736426a8ed fix(aviation): correct cancellation rate calculation and add 12 airports (#1209)
- Use resolved-flights-only denominator (landed+active+cancelled+diverted)
  instead of all flights including scheduled/unknown. DXB was showing 15%
  cancelled (NORMAL) when the real rate among resolved flights is ~58% (MAJOR).
- Add flight_date=today filter to AviationStack API calls to avoid mixing
  historical/future flights into today's cancellation stats.
- Factor cancellation rate into ops summary table severity (was ignored,
  only delay minutes were considered). Uses shared severityFromCancelRate()
  to avoid threshold duplication.
- Add minimum resolved threshold (>=10) before using resolved denominator
  to prevent extreme percentages from tiny samples.
- Add 12 major airports to AviationStack monitoring: YVR, SCL, DUB, LIS,
  ATH, WAW, CAN, TPE, MNL, AMM, KWI, CMN (40→52 airports).
2026-03-07 19:55:16 +04:00
Nicolas Dos Santos
7b9426299d fix: Tech Readiness toggle, Crypto top 10, FIRMS API key check (#1132, #979, #997) (#1135)
* fix: three panel issues — Tech Readiness toggle, Crypto top 10, FIRMS key check

1. #1132 — Add tech-readiness to FULL_PANELS so it appears in the
   Settings toggle list for Full/Geopolitical variant users.

2. #979 — Expand crypto panel from 4 coins to top 10 by market cap
   (BTC, ETH, USDT, BNB, SOL, XRP, USDC, ADA, DOGE, TRX) across
   client config, server metadata, CoinPaprika fallback map, and
   seed script.

3. #997 — Check isFeatureAvailable('nasaFirms') before loading FIRMS
   data. When the API key is missing, show a clear "not configured"
   message instead of the generic "No fire data available".

Closes #1132, closes #979, closes #997

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: replace stablecoins with AVAX/LINK, remove duplicate key, revert FIRMS change

- Replace USDT/USDC (stablecoins pegged ~$1) with AVAX and LINK
- Remove duplicate 'usd-coin' key in COINPAPRIKA_ID_MAP
- Add CoinPaprika fallback IDs for avalanche-2 and chainlink
- Revert FIRMS API key gating (handled differently now)
- Add sync comments across the 3 crypto config locations

* fix: update AIS relay + seed CoinPaprika fallback for all 10 coins

The AIS relay (primary seeder) still had the old 4-coin list.
The seed script's CoinPaprika fallback map was also missing the
new coins. Both now have all 10 entries.

* refactor: DRY crypto config into shared/crypto.json

Single source of truth for crypto IDs, metadata, and CoinPaprika
fallback mappings. All 4 consumers now import from shared/crypto.json:
- src/config/markets.ts (client)
- server/worldmonitor/market/v1/_shared.ts (server)
- scripts/seed-crypto-quotes.mjs (seed script)
- scripts/ais-relay.cjs (primary relay seeder)

Adding a new coin now requires editing only shared/crypto.json.

* chore: fix pre-existing markdown lint errors in README.md

Add blank lines between headings and lists per MD022/MD032 rules.

* fix: correct CoinPaprika XRP mapping and add crypto config test

- Fix xrp-ripple → xrp-xrp (current CoinPaprika id)
- Add tests/crypto-config.test.mjs: validates every coin has meta,
  coinpaprika mapping, unique symbols, no stablecoins, and valid
  id format — bad fallback ids now fail fast

* test: validate CoinPaprika ids against live API

The regex-only check wouldn't have caught the xrp-ripple typo.
New test fetches /v1/coins from CoinPaprika and asserts every
configured id exists. Gracefully skips if API is unreachable.

* fix(test): handle network failures in CoinPaprika API validation

Wrap fetch in try-catch so DNS failures, timeouts, and rate limits
skip gracefully instead of failing the test suite.

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Elie Habib <elie.habib@gmail.com>
2026-03-07 18:23:32 +04:00
Elie Habib
18b72af999 feat(intelligence): server-side batch AI classification for news headlines (#1195)
* feat(intelligence): server-side batch AI classification for news headlines

Move LLM classification from per-client RPCs to a server-side seed loop.
The relay batch-classifies digest titles every 15min via any OpenAI-compatible
endpoint, caches results in Redis, and the digest handler enriches items
from cache before serving — eliminating ~80 classify-event calls per client.

- Remove duplicate digest branch in data-loader.ts (dead code)
- Add _batch-classify.ts with provider-agnostic batch LLM classify
- Update classify-event.ts to use LLM_API_KEY/LLM_API_URL/LLM_MODEL env vars
- Add upstashMGet helper + classify seed loop to ais-relay.cjs
- Add enrichWithAiCache to list-feed-digest.ts (single batch Redis read)
- Preserve high-confidence keyword hits (>= 0.9) via upgrade rule

* fix(intelligence): use protocol-aware transport in relay LLM fetch, tolerate wrapped JSON in classify-event

- classifyFetchLlm: pick http/https module based on URL protocol so
  http://localhost works for local Ollama/vLLM during dev
- classify-event.ts: extract JSON object from fenced/wrapped model output
  (matches relay's tolerant parsing for OpenAI-compatible providers)
2026-03-07 15:35:25 +04:00
Elie Habib
327f499cc2 perf(edge): reduce unnecessary Vercel edge invocations (#1176)
* perf(edge): reduce unnecessary Vercel edge invocations (#6 findings)

Phase 1 — Client-only fixes:
- Remove predictions double getHydratedData read from data-loader.ts;
  fetchPredictions() handles hydration internally
- Fix UCDP delete-on-read race: read hydratedUcdp once in data-loader,
  pass to both fetchUcdpClassifications() and fetchUcdpEvents()

Phase 2 — Batch RPCs (proto + server + client):
- Add GetHumanitarianSummaryBatch RPC: replaces 20-request HAPI fanout
  with single batch call (getCachedJsonBatch + per-key Redis caching)
- Add GetFredSeriesBatch RPC: replaces 7-request FRED fanout with
  single batch call (same pattern)
- Both batch RPCs have 404 deploy-skew fallback to per-item calls

Phase 3 — Seed gap:
- Add seed-service-statuses.mjs standalone seed script
- Add 15-min warm-ping loop in AIS relay for service statuses
- Remove serviceStatuses from ON_DEMAND_KEYS in health.js

Net savings: up to 28 edge calls eliminated on cold miss per page load.

* fix(edge): address code review findings (P1–P3)

P1: Fix dead 404 deploy-skew fallback — circuit breaker was swallowing
the ApiError before the catch block could detect it. Move 404 fallback
inside the breaker callback so it executes before the breaker catches.

P2: Replace 172-line seed-service-statuses.mjs (duplicated parser logic)
with a 60-line warm-ping that triggers the existing RPC handler.

P2: Extract shared ISO2_TO_ISO3 mapping to conflict/v1/_shared.ts,
eliminating duplication between single and batch HAPI handlers.

P3: Remove unnecessary UPSTASH_ENABLED guard from relay warm-ping
(it calls Vercel RPC, not Redis directly).

P3: Clean up unused per-series FRED breakers and fetchSingleFredSeries
(replaced by batch breaker). Update getFredStatus() accordingly.

* fix(edge): use concurrent fetches in batch handlers

HAPI batch: replace serial loop with groups of 5 concurrent fetches
using Promise.allSettled for partial-success resilience. Bump client
timeout to 60s (4 rounds × 15s upstream timeout worst case).

FRED batch: replace serial loop with fully parallel Promise.allSettled
(max 10 series, each hits separate FRED endpoint).

Both changes prevent empty-result regression on cold cache that the
serial approach caused when upstream latency exceeded the client timeout.
2026-03-07 12:32:30 +04:00
Elie Habib
9211339d1c fix(seeds): prevent API quota burn and respect rate limits (#1167)
* fix(cyber): prevent AbuseIPDB quota burn when Redis rate check fails

The catch block in fetchAbuseIpDb() was falling through to the API call
when the Redis rate-limit check failed (e.g. Redis down, first run with
no key). With a 10-minute cron interval, this could exhaust the 100
calls/day free-plan limit in under 17 hours.

Now returns early with { ok: false, threats: [] } so the other 4 IOC
sources still seed normally while AbuseIPDB is safely skipped.

* fix(seeds): respect API rate limits and log fetch failures

1. seed-fire-detections.mjs: increase delay from 200ms to 6s between
   FIRMS API calls. Free tier allows 10 req/min; 27 calls at 200ms
   exceeded this and caused silent failures.

2. ais-relay.cjs (positive events): increase GDELT delay from 500ms to
   5.5s to respect the documented 1 req/5s rate limit.

3. ais-relay.cjs (cyber fetchers): replace 5 silent `catch { return [] }`
   blocks with `console.warn` logging so failures are visible in Railway
   logs. Dead code today (cyber loop disabled) but sets the right example
   for contributors.

* fix(seeds): extend FIRMS lock TTL and restore AbuseIPDB resilience

P1: seed-fire-detections.mjs — the 6s FIRMS pacing makes the job take
~162s minimum, exceeding the default 120s lock TTL. Extend lockTtlMs
to 300s (5 min) to prevent overlapping cron invocations.

P2: seed-cyber-threats.mjs — revert the early return on Redis rate-check
failure. A transient Redis blip should not permanently disable AbuseIPDB
for that run. Instead, log a warning and proceed with caution. The 2h
rate-limit interval + 10-min cron means at most 1 extra call per Redis
outage window, well within the 100/day budget.

* fix(wildfire): extend lock TTL to 10 min for worst-case FIRMS timeouts

27 calls × (6s pacing + 30s per-request timeout) = 972s worst case.
300s lock was still too short under partial upstream slowness.
2026-03-07 10:51:45 +04:00
Fasih M.
6f9af63bad feat(geo): add Pak-Afghan conflict zone and country boundary override system (#1150)
* Add Pakistan–Afghanistan hotspot and conflict zone

Introduce a new INTEL_HOTSPOTS entry (pak_afghan) to track Pakistan–Afghanistan border tensions, including location, keywords, agencies, status, escalation indicators, and humanitarian significance. Also add a CONFLICT_ZONES polygon for 'Pakistan–Afghanistan War' with center, intensity, parties, startDate (Feb 21, 2026), key developments, and displacement/casualty notes to enable monitoring of cross-border strikes, TTP activity, and regional instability.

* Update conflict zone center coordinates

Adjust the center coordinates for the specified conflict zone in src/config/geo.ts from [50, 30] to [69, 31.8] to better reflect the actual Pakistan/Afghanistan border region and improve map centering/visualization accuracy.

* Add country boundary overrides (Pakistan)

Support optional country boundary overrides by loading public/data/country-boundary-overrides.geojson and replacing main country geometries when ISO codes match. Add a script (scripts/fetch-pakistan-boundary-override.mjs) to fetch Pakistan's de facto boundary from Natural Earth and write the override file, and document the override workflow in CONTRIBUTING.md. The country-geometry service now attempts to apply overrides and updates cached polygons/bboxes; failures are ignored since overrides are optional.

* fix: neutralize language, parallel override loading, fetch timeout

- Rename conflict zone from "War" to "Border Conflict", intensity high→medium
- Rewrite description to factual language (no "open war" claim)
- Load country boundary overrides in parallel with main GeoJSON
- Neutralize comments/docs: reference Natural Earth source, remove political terms
- Add 60s timeout to Natural Earth fetch script (~24MB download)
- Add trailing newline to GeoJSON override file

* refactor: serve country boundary overrides from R2 CDN

Move country-boundary-overrides.geojson from public/data/ to R2 bucket
(worldmonitor-maps) to avoid serving large static files through Vercel.
Update fetch URL, docs, and script with rclone upload instructions.

* fix: use maps.worldmonitor.app for R2 override URL (CF-proxied)

* fix(geo): bound optional country override fetch

---------

Co-authored-by: Elie Habib <elie.habib@gmail.com>
2026-03-07 10:00:03 +04:00
Elie Habib
8d83aa02eb fix(economic): guard against undefined BIS and spending data (#1162)
* feat: premium panel gating, code cleanup, and backend simplifications

Recovered stranded changes from fix/desktop-premium-error-unification.

Premium gating:
- Add premium field ('locked'|'enhanced') to PanelConfig and LayerDefinition
- Panel.showLocked() with lock icon, CTA button, and _locked guard
- PRO badge for enhanced panels when no WM API key
- Exponential backoff auto-retry on showError() (15s→30s→60s→180s cap)
- Gate oref-sirens and telegram-intel panels behind WM API key
- Lock gpsJamming and iranAttacks layer toggles, badge ciiChoropleth
- Add tauri-titlebar drag region for custom titlebar

Code cleanup:
- Extract inline CSS from AirlineIntelPanel, WorldClockPanel to panels.css
- Remove unused showGeoError() from CountryBriefPage
- Remove dead geocodeFailed/retryBtn/closeBtn locale keys (20 files)
- Clean up var names and inline styles across 6 components

Backend:
- Remove seed-meta throttle from redis.ts (unnecessary complexity)
- Risk scores: call handler functions directly instead of raw Redis reads
- Update OpenRouter model to gpt-oss-safeguard-20b:nitro
- Add direct UCDP API fetching with version probing

Config:
- Remove titleBarStyle: Overlay from tauri.conf.json
- Add build:pro and build-sidecar-handlers to build:desktop
- Remove DXB/RUH from default aviation watchlist
- Simplify reverse-geocode (remove AbortController wrapper)

* fix: cast handler requests to any for API tsconfig compat

* fix: revert stale changes that conflict with merged PRs

Reverts files to main versions where old branch changes would
overwrite intentional fixes from PRs #1134, #1138, #1144, #1154:

- news/_shared.ts: keep gemini-2.5-flash model (not stale gpt-oss)
- redis.ts: keep seed-meta throttle from PR #1138
- reverse-geocode.ts: keep AbortController timeout from PR #1134
- CountryBriefPage.ts: keep showGeoError() from PR #1134
- country-intel.ts: keep showGeoError usage from PR #1134
- get-risk-scores.ts: revert non-existent imports
- watchlist.ts: keep DXB/RUH airports from PR #1144
- locales: restore geocodeFailed/retryBtn/closeBtn keys

* fix: neutralize language, parallel override loading, fetch timeout

- Rename conflict zone from "War" to "Border Conflict", intensity high→medium
- Rewrite description to factual language (no "open war" claim)
- Load country boundary overrides in parallel with main GeoJSON
- Neutralize comments/docs: reference Natural Earth source, remove political terms
- Add 60s timeout to Natural Earth fetch script (~24MB download)
- Add trailing newline to GeoJSON override file

* fix: restore caller messages in Panel errors and vessel expansion in popups

- Move UCDP direct-fetch cooldown after successful fetch to avoid
  suppressing all data for 10 minutes on a single failure
- Use caller-provided messages in showError/showRetrying instead of
  discarding them; respect autoRetrySeconds parameter
- Restore cluster-toggle click handler and expandable vessel list
  in military cluster popups
2026-03-07 09:43:27 +04:00
Elie Habib
b869fabdb0 fix(health): add seed-meta tracking for all bootstrap keys missing freshness data (#1163)
* fix(health): add seed-meta tracking for all bootstrap keys missing freshness data

6 bootstrap keys had no SEED_META entries in health.js, so health
endpoint could never track their freshness (always seedStale: true).

health.js:
- Add progressData + renewableEnergy to BOOTSTRAP_KEYS (missed in PR #1159)
- Add SEED_META entries for: positiveGeoEvents, riskScores, iranEvents,
  ucdpEvents, sectors, techReadiness, progressData, renewableEnergy

ais-relay.cjs:
- Add seed-meta writes for positive events (GDELT), risk scores (CII),
  and sectors — these loops had no freshness tracking
- iranEvents and ucdpEvents already write seed-meta via their seed scripts

* fix(seed): add seed-meta writes to seed-wb-indicators.mjs

The seed script wrote data keys but never wrote seed-meta keys,
causing health endpoint to report STALE_SEED for techReadiness,
progressData, and renewableEnergy indefinitely.
2026-03-07 09:31:12 +04:00
Elie Habib
a6b7c771ac fix(economic): seed all WB indicators on Railway, never call WB API from frontend (#1159)
* fix(economic): seed all WB indicators on Railway, never call WB API from frontend

Extends seed-wb-indicators.mjs to pre-compute progress data (4 indicators)
and renewable energy data (EG.ELC.RNEW.ZS) alongside tech readiness rankings.

Frontend callers (progress-data.ts, renewable-energy-data.ts, getTechReadinessRankings,
getCountryComparison) now read exclusively from bootstrap/Redis seed data.
Zero Vercel Edge → World Bank API calls remain.

* fix: address code review findings (P1+P2)

- Fix triple JSON.parse in seed verification (P1)
- Graceful fallback for renewable data fetch failure (P2)
- Use Map lookup instead of Array.find in progress-data (P2)
- Update regression test for bootstrap-only getTechReadinessRankings (P2)
2026-03-07 08:00:28 +04:00
Elie Habib
abe170da3e feat(iran): recreate seed-iran-events.mjs for LiveUAMap import (#1158)
Manual seed script for Iran conflict events. Reads from
scripts/data/iran-events-latest.json, geocodes via LOCATION_COORDS,
seeds to conflict:iran-events:v1. Data file stays gitignored.
2026-03-07 07:53:55 +04:00
Elie Habib
dd057bbdfc fix(insights): graceful exit, LKG fallback, delay after RPC warm (#1153)
- Exit 0 on failure so Railway cron doesn't restart container
- Wait 3s after RPC warm before re-reading digest from Redis
- Fall back to existing insights (LKG) when digest key is missing
2026-03-07 06:44:10 +04:00
Elie Habib
f51ac17088 fix(ucdp): graceful exit, 90s timeout, sequential probing, seed-meta write (#1152)
- Increase fetch timeout 30s → 90s (Railway network latency)
- Sequential version probing with per-attempt logging
- Write seed-meta for health endpoint freshness tracking
- Exit 0 on failure so Railway cron doesn't restart container
2026-03-07 06:44:01 +04:00