Commit Graph

205 Commits

Author SHA1 Message Date
Elie Habib
a3405cd055 fix(health): prevent NOTAM closures from triggering DEGRADED status (#1536)
Three issues caused intermittent DEGRADED health:

1. When ICAO API returned empty (timeout, challenge page), the relay
   seed updated seed-meta but did not refresh the data key TTL. After
   1h the data key expired, health saw EMPTY, reported CRIT.
   Fix: call EXPIRE on the data key to extend TTL on empty response.

2. health.js dataSize() did not recognize the closedIcaos array field,
   falling back to Object.keys count (always 2). Now properly counts
   the closure array length.

3. 0 airport closures is the normal healthy state, but health treated
   it as EMPTY_DATA (CRIT). Added EMPTY_DATA_OK_KEYS set so NOTAM
   closures with 0 records reports OK when the key exists or seed-meta
   is fresh.
2026-03-13 15:59:40 +04:00
Elie Habib
41380b8e23 fix(health): close monitoring gaps in health and seed-health endpoints (#1531)
Add missing seed-meta write for intlDelays in ais-relay, add untracked
SEED_META entries (intlDelays, faaDelays, theaterPosture) to health.js,
add 6 missing domains to seed-health.js, and return 503 when degraded.
2026-03-13 12:55:06 +04:00
Elie Habib
669c118b82 fix(aviation): keep NOTAM seed freshness updated on empty responses (#1523) 2026-03-13 11:21:14 +04:00
Elie Habib
b7b0ca196a fix(aviation): remove encodeURIComponent from NOTAM proxy handler (#1520)
ICAO API expects literal commas in the locations param. The seed loop
was fixed in #1519 but the proxy handler still encoded commas as %2C.
2026-03-13 10:04:52 +04:00
Elie Habib
4e3668ac47 fix(aviation): don't encodeURIComponent NOTAM locations param (#1519)
ICAO API rejects %2C-encoded commas with 403. The manual seed script
passes literal commas in the locations query param, which works.
Match that behavior.
2026-03-13 09:01:01 +04:00
Elie Habib
cad24d8817 fix(predictions): move prediction-tags.json into scripts/data/ for Railway (#1518)
Railway deploys with rootDirectory=scripts/, so ../shared/ resolves to
/shared/ which doesn't exist. Move the canonical file to scripts/data/
and update all four consumers.
2026-03-13 08:52:49 +04:00
Elie Habib
c2075413bd fix(aviation): add NOTAM closures seed loop to relay (#1516)
NOTAM closures data (aviation:notam:closures:v2) had no scheduled seeder,
causing health endpoint to report CRIT/EMPTY. The manual seed script
(seed-airport-delays.mjs) was never deployed as a cron, and the RPC
handler only populates on-demand with a 30min TTL.

Add a 30-minute seed loop to the AIS relay that fetches from ICAO API
and writes closures to Redis, matching the pattern of all other relay
seed loops. Also add seed-meta tracking in health.js (maxStaleMin: 90).
2026-03-13 08:46:31 +04:00
RaulC
bfcf7d88ec chore(predictions): extract shared tags and remove unused open_interest (#1512)
- Move GEOPOLITICAL_TAGS, TECH_TAGS, FINANCE_TAGS, and EXCLUDE_KEYWORDS
  to shared/prediction-tags.json so seed, RPC handler, and client all
  reference a single source of truth
- Remove open_interest proto field (always 0 for Polymarket, never
  displayed in UI) and corresponding openInterest assignments

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Elie Habib <elie.habib@gmail.com>
2026-03-13 07:59:12 +04:00
Elie Habib
0ef37d353a fix(predictions): gate Kalshi behind KALSHI_API_KEY env var (#1505)
Kalshi trading API returns 401 without authentication. Disable all
Kalshi fetches when KALSHI_API_KEY is not set, and pass it as a
Bearer token when present. Seed logs "disabled" instead of spamming
401 errors on every run.
2026-03-13 00:43:47 +04:00
RaulC
af7496cce1 feat(predictions): add Kalshi as prediction market data source (#1355)
* feat(predictions): add Kalshi as prediction market data source

* fix(predictions): address Kalshi integration review feedback

- Gate Kalshi fetch behind category check to avoid wasted calls on tech-scoped requests
- Replace fragile double-cast bootstrap typing with BootstrapMarket interface
- Fix zero-price falsy bug in seed script using Number.isFinite guard
- Align RPC market selection with seed script (highest-volume via single-pass loop)
- Raise Kalshi volume threshold to 5000 for signal quality parity
- Add missing .prediction-source badge CSS with per-source color variants

* fix(predictions): address P1/P2 review items for Kalshi integration

- Apply isExcluded() filter and volume threshold (5000) to live Kalshi
  RPC path so cache-miss results match seed curation quality
- Include FINANCE_TAGS in seed allTags so 'markets' tag is fetched
- Align Kalshi title mapping (market.title || event.title) between
  seed and RPC handler
- Remove silent geopolitical fallback for finance variant so missing
  finance bootstrap falls through to RPC fetch

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

* fix(predictions): prefer yes_sub_title for Kalshi multi-contract events

For multi-contract Kalshi events (e.g. papal election candidates),
market.title is the generic event question while yes_sub_title
identifies the specific contract. Use yes_sub_title when present
in both seed and RPC paths so titles are accurate and consistent.

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

* fix(predictions): use general Kalshi trading API subdomain

Switch from api.elections.kalshi.com (elections-only) to
trading-api.kalshi.com so economy, crypto, and other non-election
markets are included in the finance variant.

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

---------

Co-authored-by: Elie Habib <elie.habib@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 23:54:20 +04:00
RepairYourTech
0420a54866 fix(acled): add OAuth token manager with automatic refresh (#1437)
* fix(acled): add OAuth token manager with automatic refresh

ACLED access tokens expire every 24 hours, but WorldMonitor stores a
static ACLED_ACCESS_TOKEN with no refresh logic — causing all ACLED
API calls to fail after the first day.

This commit adds `acled-auth.ts`, an OAuth token manager that:
- Exchanges ACLED_EMAIL + ACLED_PASSWORD for an access token (24h)
  and refresh token (14d) via the official ACLED OAuth endpoint
- Caches tokens in memory and auto-refreshes before expiry
- Falls back to static ACLED_ACCESS_TOKEN for backward compatibility
- Deduplicates concurrent refresh attempts
- Degrades gracefully when no credentials are configured

The only change to the existing `acled.ts` is replacing the synchronous
`process.env.ACLED_ACCESS_TOKEN` read with an async call to the new
`getAcledAccessToken()` helper.

Fixes #1283
Relates to #290

* fix: address review feedback on ACLED OAuth PR

- Use Redis (Upstash) as L2 token cache to survive Vercel Edge cold starts
  (in-memory cache retained as fast-path L1)
- Add CHROME_UA User-Agent header on OAuth token exchange and refresh
- Update seed script to use OAuth flow via getAcledToken() helper
  instead of raw process.env.ACLED_ACCESS_TOKEN
- Add security comment to .env.example about plaintext password trade-offs
- Sidecar ACLED_ACCESS_TOKEN case is a validation probe (tests user-provided
  value, not process.env) — data fetching delegates to handler modules

* feat(sidecar): add ACLED_EMAIL/ACLED_PASSWORD to env allowlist and validation

- Add ACLED_EMAIL and ACLED_PASSWORD to ALLOWED_ENV_KEYS set
- Add ACLED_EMAIL validation case (store-only, verified with password)
- Add ACLED_PASSWORD validation case with OAuth token exchange via
  acleddata.com/api/acled/user/login
- On successful login, store obtained OAuth token in ACLED_ACCESS_TOKEN
- Follows existing validation patterns (Cloudflare challenge handling,
  auth failure detection, User-Agent header)

* fix: address remaining review feedback (duplicate OAuth, em dashes, emoji)

- Extract shared ACLED OAuth helper into scripts/shared/acled-oauth.mjs
- Remove ~55 lines of duplicate OAuth logic from seed-unrest-events.mjs,
  now imports getAcledToken from the shared helper
- Replace em dashes with ASCII dashes in acled-auth.ts section comments
- Replace em dash with parentheses in sidecar validation message
- Remove emoji from .env.example security note

Addresses koala73's second review: MEDIUM (duplicate OAuth), LOW (em
dashes), LOW (emoji).

* fix: align sidecar OAuth endpoint, fix L1/L2 cache, cleanup artifacts

- Sidecar: switch from /api/acled/user/login (JSON) to /oauth/token
  (URL-encoded) to match server/_shared/acled-auth.ts exactly
- acled-auth.ts: check L2 Redis when L1 is expired, not only when L1
  is null (fixes stale L1 skipping fresher L2 from another isolate)
- acled-oauth.mjs: remove stray backslash on line 9
- seed-unrest-events.mjs: remove extra blank line at line 13

---------

Co-authored-by: Elie Habib <elie.habib@gmail.com>
Co-authored-by: RepairYourTech <30200484+RepairYourTech@users.noreply.github.com>
2026-03-12 22:24:40 +04:00
Elie Habib
7d72c6844b fix(predictions): composite scoring, finance variant, region ranking (#1486)
* fix(predictions): replace volume-only sort with composite scoring, add finance variant and region ranking

The prediction panel was surfacing irrelevant near-certain markets (1%/99% meme
markets like celebrity presidential bids) because the discrepancy filter was
inverted and sorting was by volume alone.

- Replace broken discrepancy filter with composite scoring (60% uncertainty +
  40% log-scaled volume) in seed script
- Add meme candidate detection and sports/entertainment keyword exclusion
- Add finance variant with dedicated tags for economy/trade/rates topics
- Add region-aware soft ranking outside circuit breaker cache
- Add input validation (category max 50, query max 100) in RPC handler
- Skip events without markets instead of defaulting to yesPrice=50
- Per-bucket relaxation safety valve when <15 markets pass strict filters

* fix(predictions): apply region sort before truncation, add RPC fallback scoring, validate finance seed

- Keep 25 candidates from bootstrap/RPC, apply region sort, then slice to 15
  (previously sliced to 15 first, making region boost ineffective for markets
  ranked 16-25)
- Add client-side uncertainty scoring + near-certain filter (10-90%) for RPC
  fallback path (previously fell back to Gamma's volume-only ordering)
- Include finance array in seed validation (previously only checked
  geopolitical/tech, allowing broken finance data to ship silently)

* test(predictions): add 54 unit tests for scoring, filtering, and region tagging

Extract pure prediction scoring functions into shared module
(_prediction-scoring.mjs) for testability. Tests cover parseYesPrice,
isExcluded, isMemeCandidate, tagRegions, shouldInclude, scoreMarket,
filterAndScore, isExpired, plus regression tests for the meme market
surfacing bug that motivated this fix.
2026-03-12 14:02:58 +04:00
Elie Habib
9c104f413c data(iran): add 13 location coordinates for March 12 events (#1489)
New geolocator entries: Jerusalem, Fardis, Marivan, Salalah,
Palmachim, Umm Qasr, Al-Siba, Taleghan, Persian Gulf,
Eastern Province, Empty Quarter, Ovadia, Shin Bet.
2026-03-12 13:59:19 +04:00
Elie Habib
354c0df1fe feat(tech-events): gold standard pipeline with Railway seed + bootstrap hydration (#1475)
* fix(tech-events): prevent partial fetch results from being cached

Techmeme ICS and dev.events RSS fetches on Vercel edge can partially
fail (timeout, truncation), returning only 1 event instead of 20+.
The handler cached this partial result for 6 hours, causing the Tech
Events panel to show empty.

- Add 8s AbortSignal.timeout on both external fetches
- Require minimum 5 events before caching (at least curated count)

* fix(tech-events): remove MIN_EVENTS threshold and add diagnostic logging

The MIN_EVENTS=5 threshold caused empty results when both external
sources fail on Vercel edge (only 4 curated events available). Now
any events > 0 are cached. Added detailed logging to diagnose why
Techmeme ICS and dev.events RSS fetches fail on Vercel edge.
Also removed past STEP Dubai 2026 event.

* fix(tech-events): route fetches through Railway relay when direct fails

Vercel edge functions cannot reliably reach Techmeme ICS and dev.events
RSS (datacenter IP blocking). Added fetchTextWithRelay() that tries
direct fetch first, then falls back to Railway relay proxy (/rss endpoint)
which fetches from a different IP. Same pattern used by news feed digest
and other handlers that hit blocked external sources.

* feat(tech-events): gold standard pipeline with Railway seed + bootstrap hydration

Full data pipeline overhaul to match project conventions:

- Add tech events seed loop to ais-relay.cjs: fetches Techmeme ICS +
  dev.events RSS every 6h from Railway (avoids Vercel IP blocking),
  parses both sources, merges with curated fallback events, writes to
  Redis (data key + bootstrap key + seed-meta)
- Register in api/bootstrap.js BOOTSTRAP_CACHE_KEYS (SLOW tier)
- Register in api/health.js BOOTSTRAP_KEYS + SEED_META (420min stale)
- Restructure RPC handler: reads from single broad Redis key (populated
  by seed), applies geocoding + filtering in-memory per request params.
  Fallback fetcher only runs on cold start before first seed
- TechEventsPanel: check getHydratedData('techEvents') from bootstrap
  before falling back to RPC call
- data-loader: use hydrated bootstrap data for map layer, RPC fallback
2026-03-12 08:18:59 +04:00
Elie Habib
651cd3d08b feat(desktop): sidecar cloud proxy, domain handlers, and panel fixes (#1454)
* feat(desktop): compile domain handlers + add in-memory sidecar cache

The sidecar was broken for all 23 sebuf/RPC domain routes because
the build script (build-sidecar-handlers.mjs) never existed on main
while package.json already referenced it. This adds the missing script
and an in-memory TTL+LRU cache so the sidecar doesn't need Upstash Redis.

- Add scripts/build-sidecar-handlers.mjs (esbuild multi-entry, 23 domains)
- Add server/_shared/sidecar-cache.ts (500 entries, 50MB max, lazy sweep)
- Modify redis.ts getCachedJson/setCachedJson to use dynamic import for
  sidecar cache when LOCAL_API_MODE=tauri-sidecar (zero cost on Vercel Edge)
- Update tauri.conf.json beforeDevCommand to compile handlers
- Add gitignore pattern for compiled api/*/v1/[rpc].js

* fix(desktop): gate premium panel fetches and open footer links in browser

Skip oref-sirens and telegram-intel HTTP requests on desktop when
WORLDMONITOR_API_KEY is not present. Use absolute URLs for footer
links on desktop so the Tauri external link handler opens them in
the system browser instead of navigating within the webview.

* fix(desktop): cloud proxy, bootstrap timeouts, and panel data fixes

- Set Origin header on cloud proxy requests (fixes 401 from API key validator)
- Strip If-None-Match/If-Modified-Since headers (fixes stale 304 responses)
- Add cloud-preferred routing for market/economic/news/infrastructure/research
- Enable cloud fallback via LOCAL_API_CLOUD_FALLBACK env var in main.rs
- Increase bootstrap timeouts on desktop (8s/12s vs 3s/5s) for sidecar proxy hops
- Force per-feed RSS fallback on desktop (server digest has fewer categories)
- Add finance feeds to commodity variant (client + server)
- Remove desktop diagnostics from ServiceStatusPanel (show cloud statuses only)
- Restore DeductionPanel CSS from PR #1162
- Deduplicate repeated sidecar error logs
2026-03-12 06:50:30 +04:00
Elie Habib
2a7d7fc3fe fix: standardize brand name to "World Monitor" with space (#1463)
Replace "WorldMonitor" with "World Monitor" in all user-facing display
text across blog posts, docs, layouts, structured data, footer, offline
page, and X-Title headers. Technical identifiers (User-Agent strings,
X-WorldMonitor-Key headers, @WorldMonitorApp handle, function names)
are preserved unchanged. Also adds anchors color to Mintlify docs config
to fix blue link color in dark mode.
2026-03-12 01:28:16 +04:00
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