Merged
Conversation
|
@Konstantysz is attempting to deploy a commit to the Elie Team on Vercel. A member of the Team first needs to authorize it. |
|
Highly recommend it. |
|
This should have been the feature at the very beginning of this project. Bumping this. |
Owner
|
thank you @Konstantysz |
koala73
pushed a commit
that referenced
this pull request
Mar 1, 2026
koala73
added a commit
that referenced
this pull request
Mar 1, 2026
* Add Security Advisories panel with government travel alerts (#460)
* feat: add Security Advisories panel with government travel advisory feeds
Adds a new panel aggregating travel/security advisories from official
government foreign affairs agencies (US State Dept, AU DFAT Smartraveller,
UK FCDO, NZ MFAT). Advisories are categorized by severity level
(Do Not Travel, Reconsider, Caution, Normal) with filter tabs by
source country. Includes summary counts, auto-refresh, and persistent
caching via the existing data-freshness system.
* chore: update package-lock.json
* fix: event delegation, localization, and cleanup for SecurityAdvisories panel
P1 fixes:
- Use event delegation on this.content (bound once in constructor) instead
of direct addEventListener after each innerHTML replacement — prevents
memory leaks and stale listener issues on re-render
- Use setContent() consistently instead of mixing with this.content.innerHTML
- Add securityAdvisories translations to all 16 non-English locale files
(panels name, component strings, common.all key)
- Revert unrelated package-lock.json version bump
P2 fixes:
- Deduplicate loadSecurityAdvisories — loadIntelligenceData now calls the
shared method instead of inlining duplicate fetch+set logic
- Add Accept header to fetch calls for better content negotiation
* feat(advisories): add US embassy alerts, CDC, ECDC, and WHO health feeds
Adds 21 new advisory RSS feeds:
- 13 US Embassy per-country security alerts (TH, AE, DE, UA, MX, IN, PK, CO, PL, BD, IT, DO, MM)
- CDC Travel Notices
- 5 ECDC feeds (epidemiological, threats, risk assessments, avian flu, publications)
- 2 WHO feeds (global news, Africa emergencies)
Panel gains a Health filter tab for CDC/ECDC/WHO sources.
All new domains added to RSS proxy allowlist.
i18n "health" key added across all 17 locales.
* feat(cache): add negative-result caching to cachedFetchJson (#466)
When upstream APIs return errors (HTTP 403, 429, timeout), fetchers
return null. Previously null results were not cached, causing repeated
request storms against broken APIs every refresh cycle.
Now caches a sentinel value ('__WM_NEG__') with a short 2-minute TTL
on null results. Subsequent requests within that window get null
immediately without hitting upstream. Thrown errors (transient) skip
sentinel caching and retry immediately.
Also filters sentinels from getCachedJsonBatch pipeline reads and fixes
theater posture coalescing test (expected 2 OpenSky fetches for 2
theater query regions, not 1).
* feat: convert 52 API endpoints from POST to GET for edge caching (#468)
* feat: convert 52 API endpoints from POST to GET for edge caching
Convert all cacheable sebuf RPC endpoints to HTTP GET with query/path
parameters, enabling CDN edge caching to reduce costs. Flatten nested
request types (TimeRange, PaginationRequest, BoundingBox) into scalar
query params. Add path params for resource lookups (GetFredSeries,
GetHumanitarianSummary, GetCountryStockIndex, GetCountryIntelBrief,
GetAircraftDetails). Rewrite router with hybrid static/dynamic matching
for path param support.
Kept as POST: SummarizeArticle, ClassifyEvent, RecordBaselineSnapshot,
GetAircraftDetailsBatch, RegisterInterest.
Generated with sebuf v0.9.0 (protoc-gen-ts-client, protoc-gen-ts-server).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add rate_limited field to market response protos
The rateLimited field was hand-patched into generated files on main but
never declared in the proto definitions. Regenerating wiped it out,
breaking the build. Now properly defined in both ListEtfFlowsResponse
and ListMarketQuotesResponse protos.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* chore: remove accidentally committed .planning files
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add Cloudflare edge caching infrastructure for api.worldmonitor.app (#471)
Route web production RPC traffic through api.worldmonitor.app via fetch
interceptor (installWebApiRedirect). Add default Cache-Control headers
(s-maxage=300, stale-while-revalidate=60) on GET 200 responses, with
no-store override for real-time endpoints (vessel snapshot). Update CORS
to allow GET method. Skip Vercel bot middleware for API subdomain using
hostname check (non-spoofable, replacing CF-Ray header approach). Update
desktop cloud fallback to route through api.worldmonitor.app.
* fix(beta): eagerly load T5-small model when beta mode is enabled
BETA_MODE now couples the badge AND model loading — the summarization-beta
model starts loading on startup instead of waiting for the first summarization call.
* fix: move 5 path-param endpoints to query params for Vercel routing (#472)
Vercel's `api/[domain]/v1/[rpc].ts` captures one dynamic segment.
Path params like `/get-humanitarian-summary/SA` add an extra segment
that has no matching route file, causing 404 on both OPTIONS preflight
and direct requests. These endpoints were broken in production.
Changes:
- Remove `{param}` from 5 service.proto HTTP paths
- Add `(sebuf.http.query)` annotations to request message fields
- Update generated client/server code to use URLSearchParams
- Update OpenAPI specs (YAML + JSON) to declare query params
- Add early-return guards in 4 handlers for missing required params
- Add happy.worldmonitor.app to runtime.ts redirect hosts
Affected endpoints:
- GET /api/conflict/v1/get-humanitarian-summary?country_code=SA
- GET /api/economic/v1/get-fred-series?series_id=T10Y2Y&limit=120
- GET /api/market/v1/get-country-stock-index?country_code=US
- GET /api/intelligence/v1/get-country-intel-brief?country_code=US
- GET /api/military/v1/get-aircraft-details?icao24=a12345
* fix(security-advisories): route feeds through RSS proxy to avoid CORS blocks (#473)
- Advisory feeds were fetched directly from the browser, hitting CORS
on all 21 feeds (US State Dept, AU Smartraveller, US Embassies, ECDC,
CDC, WHO). Route through /api/rss-proxy on web, keep proxyUrl for desktop.
- Fix double slash in ECDC Avian Influenza URL (323//feed → 323/feed)
- Add feeds.news24.com to RSS proxy allowlist (was returning 403)
* feat(cache): tiered edge Cache-Control aligned to upstream TTLs (#474)
* fix: move 5 path-param endpoints to query params for Vercel routing
Vercel's `api/[domain]/v1/[rpc].ts` captures one dynamic segment.
Path params like `/get-humanitarian-summary/SA` add an extra segment
that has no matching route file, causing 404 on both OPTIONS preflight
and direct requests. These endpoints were broken in production.
Changes:
- Remove `{param}` from 5 service.proto HTTP paths
- Add `(sebuf.http.query)` annotations to request message fields
- Update generated client/server code to use URLSearchParams
- Update OpenAPI specs (YAML + JSON) to declare query params
- Add early-return guards in 4 handlers for missing required params
- Add happy.worldmonitor.app to runtime.ts redirect hosts
Affected endpoints:
- GET /api/conflict/v1/get-humanitarian-summary?country_code=SA
- GET /api/economic/v1/get-fred-series?series_id=T10Y2Y&limit=120
- GET /api/market/v1/get-country-stock-index?country_code=US
- GET /api/intelligence/v1/get-country-intel-brief?country_code=US
- GET /api/military/v1/get-aircraft-details?icao24=a12345
* feat(cache): add tiered edge Cache-Control aligned to upstream TTLs
Replace flat s-maxage=300 with 5 tiers (fast/medium/slow/static/no-store)
mapped per-endpoint to respect upstream Redis TTLs. Adds stale-if-error
resilience headers and X-No-Cache plumbing for future degraded responses.
X-Cache-Tier debug header gated behind ?_debug query param.
* fix(tech): use rss() for CISA feed, drop build from pre-push hook (#475)
- CISA Advisories used dead rss.worldmonitor.app domain (404), switch to rss() helper
- Remove Vite build from pre-push hook (tsc already catches errors)
* fix(desktop): enable click-to-play YouTube embeds + CISA feed fixes (#476)
* fix(tech): use rss() for CISA feed, drop build from pre-push hook
- CISA Advisories used dead rss.worldmonitor.app domain (404), switch to rss() helper
- Remove Vite build from pre-push hook (tsc already catches errors)
* fix(desktop): enable click-to-play for YouTube embeds in WKWebView
WKWebView blocks programmatic autoplay in cross-origin iframes regardless
of allow attributes, Permissions-Policy, mute-first retries, or secure
context. Documented all 10 approaches tested in docs/internal/.
Changes:
- Switch sidecar embed origin from 127.0.0.1 to localhost (secure context)
- Add MutationObserver + retry chain as best-effort autoplay attempts
- Use postMessage('*') to fix tauri://localhost cross-origin messaging
- Make sidecar play overlay non-interactive (pointer-events:none)
- Fix .webcam-iframe pointer-events:none blocking clicks in grid view
- Add expand button to grid cells for switching to single view on desktop
- Add http://localhost:* to CSP frame-src in index.html and tauri.conf.json
* fix(gateway): convert stale POST requests to GET for backwards compat (#477)
Stale cached client bundles still send POST to endpoints converted to
GET in PR #468, causing 404s. The gateway now parses the POST JSON body
into query params and retries the match as GET.
* feat(proxy): add Cloudflare edge caching for proxy.worldmonitor.app (#478)
Add CDN-Cache-Control headers to all proxy endpoints so Cloudflare can
cache responses at the edge independently of browser Cache-Control:
- RSS: 600s edge + stale-while-revalidate=300 (browser: 300s)
- UCDP: 3600s edge (matches browser)
- OpenSky: 15s edge (browser: 30s) for fresher flight data
- WorldBank: 1800s/86400s edge (matches browser)
- Polymarket: 120s edge (matches browser)
- Telegram: 10s edge (matches browser)
- AIS snapshot: 2s edge (matches browser)
Also fixes:
- Vary header merging: sendCompressed/sendPreGzipped now merge existing
Vary: Origin instead of overwriting, preventing cross-origin cache
poisoning at the edge
- Stale fallback responses (OpenSky, WorldBank, Polymarket, RSS) now
set Cache-Control: no-store + CDN-Cache-Control: no-store to prevent
edge caching of degraded responses
- All no-cache branches get CDN-Cache-Control: no-store
- /opensky-reset gets no-store (state-changing endpoint)
* fix(sentry): add noise filters for 4 unresolved issues (#479)
- Tighten AbortError filter to match "AbortError: The operation was aborted"
- Filter "The user aborted a request" (normal navigation cancellation)
- Filter UltraViewer service worker injection errors (/uv/service/)
- Filter Huawei WebView __isInQueue__ injection
* feat: configurable VITE_WS_API_URL + harden POST→GET shim (#480)
* fix(gateway): harden POST→GET shim with scalar guard and size limit
- Only convert string/number/boolean values to query params (skip objects,
nested arrays, __proto__ etc.) to prevent prototype pollution vectors
- Skip body parsing for Content-Length > 1MB to avoid memory pressure
* feat: make API base URL configurable via VITE_WS_API_URL
Replace hardcoded api.worldmonitor.app with VITE_WS_API_URL env var.
When empty, installWebApiRedirect() is skipped entirely — relative
/api/* calls stay on the same domain (local installs). When set,
browser fetch is redirected to that URL.
Also adds VITE_WS_API_URL and VITE_WS_RELAY_URL hostnames to
APP_HOSTS allowlist dynamically.
* fix(analytics): use greedy regex in PostHog ingest rewrites (#481)
Vercel's :path* wildcard doesn't match trailing slashes that
PostHog SDK appends (e.g. /ingest/s/?compression=...), causing 404s.
Switch to :path(.*) which matches all path segments including
trailing slashes. Ref: PostHog/posthog#17596
* perf(proxy): increase AIS snapshot edge TTL from 2s to 10s (#482)
With 20k requests/30min (60% of proxy traffic) and per-PoP caching,
a 2s edge TTL expires before the next request from the same PoP arrives,
resulting in near-zero cache hits. 10s allows same-PoP dedup while
keeping browser TTL at 2s for fresh vessel positions.
* fix(markets): commodities panel showing stocks instead of commodities (#483)
The shared circuit breaker (cacheTtlMs: 0) cached the stocks response,
then the stale-while-revalidate path returned that cached stocks data
for the subsequent commodities fetch. Skip SWR when caching is disabled.
* feat(gateway): complete edge cache tier coverage + degraded-response policy (#484)
- Add 11 missing GET routes to RPC_CACHE_TIER map (8 slow, 3 medium)
- Add response-headers side-channel (WeakMap) so handlers can signal
X-No-Cache without codegen changes; wire into military-flights and
positive-geo-events handlers on upstream failure
- Add env-controlled per-endpoint tier override (CACHE_TIER_OVERRIDE_*)
for incident response rollback
- Add VITE_WS_API_URL hostname allowlist (*.worldmonitor.app + localhost)
- Fix fetch.bind(globalThis) in positive-events-geo.ts (deferred lambda)
- Add CI test asserting every generated GET route has an explicit cache
tier entry (prevents silent default-tier drift)
* chore: bump version to 2.5.20 + changelog
Covers PRs #452–#484: Cloudflare edge caching, commodities SWR fix,
security advisories panel, settings redesign, 52 POST→GET migrations.
* fix(rss): remove stale indianewsnetwork.com from proxy allowlist (#486)
Feed has no <pubDate> fields and latest content is from April 2022.
Not referenced in any feed config — only in the proxy domain allowlist.
* feat(i18n): add Korean (한국어) localization (#487)
- Add ko.json with all 1606 translation keys matching en.json structure
- Register 'ko' in SUPPORTED_LANGUAGES, LANGUAGES display array, and locale map
- Korean appears as 🇰🇷 한국어 in the language dropdown
* feat: add Polish tv livestreams (#488)
* feat(rss): add Axios (api.axios.com/feed) as US news source (#494)
Add api.axios.com to proxy allowlist and CSP connect-src, register
Axios feed under US category as Tier 2 mainstream source.
* perf: bootstrap endpoint + polling optimization (#495)
* perf: bootstrap endpoint + polling optimization (phases 3-4)
Replace 15+ individual RPC calls on startup with a single /api/bootstrap
batch call that fetches pre-cached data from Redis. Consolidate 6 panel
setInterval timers into the central RefreshScheduler for hidden-tab
awareness (10x multiplier) and adaptive backoff (up to 4x for unchanged
data). Convert IntelligenceGapBadge from 10s polling to event-driven
updates with 60s safety fallback.
* fix(bootstrap): inline Redis + cache keys in edge function
Vercel Edge Functions cannot resolve cross-directory TypeScript imports
from server/_shared/. Inline getCachedJsonBatch and BOOTSTRAP_CACHE_KEYS
directly in api/bootstrap.js. Add sync test to ensure inlined keys stay
in sync with the canonical server/_shared/cache-keys.ts registry.
* test: add Edge Function module isolation guard for all api/*.js files
Prevents any Edge Function from importing from ../server/ or ../src/
which breaks Vercel builds. Scans all 12 non-helper Edge Functions.
* fix(bootstrap): read unprefixed cache keys on all environments
Preview deploys set VERCEL_ENV=preview which caused getKeyPrefix() to
prefix Redis keys with preview:<sha>:, but handlers only write to
unprefixed keys on production. Bootstrap is a read-only consumer of
production cache — always read unprefixed keys.
* fix(bootstrap): wire sectors hydration + add coverage guard
- Wire getHydratedData('sectors') in data-loader to skip Yahoo Finance
fetch when bootstrap provides sector data
- Add test ensuring every bootstrap key has a getHydratedData consumer
— prevents adding keys without wiring them
* fix(server): resolve 25 TypeScript errors + add server typecheck to CI
- _shared.ts: remove unused `delay` variable
- list-etf-flows.ts: add missing `rateLimited` field to 3 return literals
- list-market-quotes.ts: add missing `rateLimited` field to 4 return literals
- get-cable-health.ts: add non-null assertions for regex groups and array access
- list-positive-geo-events.ts: add non-null assertion for array index
- get-chokepoint-status.ts: add required fields to request objects
- CI: run `typecheck:api` (tsconfig.api.json) alongside `typecheck` to catch
server/ TS errors before merge
* feat(military): server-side military bases 125K + rate limiting (#496)
* feat(military): server-side military bases with 125K entries + rate limiting (#485)
Migrate military bases from 224 static client-side entries to 125,380
server-side entries stored in Redis GEO sorted sets, served via
bbox-filtered GEOSEARCH endpoint with server-side clustering.
Data pipeline:
- Pizzint/Polyglobe: 79,156 entries (Supabase extraction)
- OpenStreetMap: 45,185 entries
- MIRTA: 821 entries
- Curated strategic: 218 entries
- 277 proximity duplicates removed
Server:
- ListMilitaryBases RPC with GEOSEARCH + HMGET + tier/filter/clustering
- Antimeridian handling (split bbox queries)
- Blue-green Redis deployment with atomic version pointer switch
- geoSearchByBox() + getHashFieldsBatch() helpers in redis.ts
Security:
- @upstash/ratelimit: 60 req/min sliding window per IP
- IP spoofing fix: prioritize x-real-ip (Vercel-injected) over x-forwarded-for
- Require API key for non-browser requests (blocks unauthenticated curl/scripts)
- Input validation: allowlisted types/kinds, regex country, clamped bbox/zoom
Frontend:
- Viewport-driven loading with bbox quantization + debounce
- Server-side grid clustering at low zoom levels
- Enriched popup with kind, category badges (airforce/naval/nuclear/space)
- Static 224 bases kept as search fallback + initial render
* fix(military): fallback to production Redis keys in preview deployments
Preview deployments prefix Redis keys with `preview:{sha}:` but military
bases data is seeded to unprefixed (production) keys. When the prefixed
`military:bases:active` key is missing, fall back to the unprefixed key
and use raw (unprefixed) keys for geo/meta lookups.
* fix: remove unused 'remaining' destructure in rate-limit (TS6133)
* ci: add typecheck:api to pre-push hook to catch server-side TS errors
* debug(military): add X-Bases-Debug response header for preview diagnostics
* fix(bases): trigger initial server fetch on map load
fetchServerBases() was only called on moveend — if the user
never panned/zoomed, the API was never called and only the 224
static fallback bases showed.
* perf(military): debounce base fetches + upgrade edge cache to static tier (#497)
- Add 300ms debounce on moveend to prevent rapid pan flooding
- Fixes stale-bbox bug where pendingFetch returns old viewport data
- Upgrade edge cache tier from medium (5min) to static (1hr) — bases are
static infrastructure, aligned with server-side cachedFetchJson TTL
- Keep error logging in catch blocks for production diagnostics
* fix(cyber): make GeoIP centroid fallback jitter deterministic (#498)
Replace Math.random() jitter with DJB2 hash seeded by the threat
indicator (IP/URL), so the same threat always maps to the same
coordinates across requests while different threats from the same
country still spread out.
Closes #203
Co-authored-by: Chris Chen <fuleinist@users.noreply.github.com>
* fix: use cross-env for Windows-compatible npm scripts (#499)
Replace direct `VAR=value command` syntax with cross-env/cross-env-shell
so dev, build, test, and desktop scripts work on Windows PowerShell/CMD.
Co-authored-by: facusturla <facusturla@users.noreply.github.com>
* feat(live-news): add CBC News to optional North America channels (#502)
YouTube handle @CBCNews with fallback video ID 5vfaDsMhCF4.
* fix(bootstrap): harden hydration cache + polling review fixes (#504)
- Filter null/undefined values before storing in hydration cache to
prevent future consumers using !== undefined from misinterpreting
null as valid data
- Debounce wm:intelligence-updated event handler via requestAnimationFrame
to coalesce rapid alert generation into a single render pass
- Include alert IDs in StrategicRiskPanel change fingerprint so content
changes are detected even when alert count stays the same
- Replace JSON.stringify change detection in ServiceStatusPanel with
lightweight name:status fingerprint
- Document max effective refresh interval (40x base) in scheduler
* fix(geo): tokenization-based keyword matching to prevent false positives (#503)
* fix(geo): tokenization-based keyword matching to prevent false positives
Replace String.includes() with tokenization-based Set.has() matching
across the geo-tagging pipeline. Prevents false positives like "assad"
matching inside "ambassador" and "hts" matching inside "rights".
- Add src/utils/keyword-match.ts as single source of truth
- Decompose possessives/hyphens ("Assad's" → includes "assad")
- Support multi-word phrase matching ("white house" as contiguous)
- Remove false-positive-prone DC keywords ('house', 'us ')
- Update 9 consumer files across geo-hub, map, CII, and asset systems
- Add 44 tests covering false positives, true positives, edge cases
Co-authored-by: karim <mirakijka@gmail.com>
Fixes #324
* fix(geo): add inflection suffix matching + fix test imports
Address code review feedback:
P1a: Add suffix-aware matching for plurals and demonyms so existing
keyword lists don't regress (houthi→houthis, ukraine→ukrainian,
iran→iranian, israel→israeli, russia→russian, taiwan→taiwanese).
Uses curated suffix list + e-dropping rule to avoid false positives.
P1b: Expand conflictTopics arrays in DeckGLMap and Map with demonym
forms so "Iranian senate..." correctly registers as conflict topic.
P2: Replace inline test functions with real module import via tsx.
Tests now exercise the production keyword-match.ts directly.
* fix: wire geo-keyword tests into test:data command
The .mts test file wasn't covered by `node --test tests/*.test.mjs`.
Add `npx tsx --test tests/*.test.mts` so test:data runs both suites.
* fix: cross-platform test:data + pin tsx in devDependencies
- Use tsx as test runner for both .mjs and .mts (single invocation)
- Removes ; separator which breaks on Windows cmd.exe
- Add tsx to devDependencies so it works in offline/CI environments
* fix(geo): multi-word demonym matching + short-keyword suffix guard
- Add wordMatches() for suffix-aware phrase matching so "South Korean"
matches keyword "south korea" and "North Korean" matches "north korea"
- Add MIN_SUFFIX_KEYWORD_LEN=4 guard so short keywords like "ai", "us",
"hts" only do exact-match (prevents "ais"→"ai", "uses"→"us" false positives)
- Add 5 new tests covering both fixes (58 total, all passing)
* fix(geo): support plural demonyms in keyword matching
Add compound suffixes (ians, eans, ans, ns, is) to handle plural
demonym forms like "Iranians"→"iran", "Ukrainians"→"ukraine",
"Russians"→"russia", "Israelis"→"israel". Adds 5 new tests (63 total).
---------
Co-authored-by: karim <mirakijka@gmail.com>
* chore: strip 61 debug console.log calls from 20 service files (#501)
* chore: strip 61 debug console.log calls from services
Remove development/tracing console.log statements from 20 files.
These add noise to production browser consoles and increase bundle size.
Preserved: all console.error (error handling) and console.warn (warnings).
Preserved: debug-gated logs in runtime.ts (controlled by verbose flag).
Removed: debugInjectTestEvents() from geo-convergence.ts (test-only code).
Removed: logSummary()/logReport() methods that were pure console.log wrappers.
* fix: remove orphaned stubs and remaining debug logs from stripped services
- Remove empty logReport() method and unused startTime variable (parallel-analysis.ts)
- Remove orphaned console.group/console.groupEnd pair (parallel-analysis.ts)
- Remove empty logSignalSummary() export (signal-aggregator.ts)
- Remove logSignalSummary import/call and 3 remaining console.logs (InsightsPanel.ts)
- Remove no-op logDirectFetchBlockedOnce() and dead infrastructure (prediction/index.ts)
* fix: generalize Vercel preview origin regex + include filters in bases cache key (#506)
- api/_api-key.js: preview URL pattern was user-specific (-elie-),
rejecting other collaborators' Vercel preview deployments.
Generalized to match any worldmonitor-*.vercel.app origin.
- military-bases.ts: client cache key only checked bbox/zoom, ignoring
type/kind/country filters. Switching filters without panning returned
stale results. Unified into single cacheKey string.
* fix(prediction): filter stale/expired markets from Polymarket panel (#507)
Prediction panel was showing expired markets (e.g. "Will US strike Iran
on Feb 9" at 0%). Root causes: no active/archived API filters, no
end_date_min param, no client-side expiry guard, and sub-market selection
picking highest volume before filtering expired ones.
- Add active=true, archived=false, end_date_min API params to all 3
Gamma API call sites (events, markets, probe)
- Pre-filter sub-markets by closed/expired BEFORE volume selection in
both fetchPredictions() and fetchCountryMarkets()
- Add defense-in-depth isExpired() client-side filter on final results
- Propagate endDate through all market object paths including sebuf
fallback
- Show expiry date in PredictionPanel UI with new .prediction-meta
layout
- Add "closes" i18n key to all 18 locale files
- Add endDate to server handler GammaMarket/GammaEvent interfaces and
map to proto closesAt field
* fix(relay): guard proxy handlers against ERR_HTTP_HEADERS_SENT crash (#509)
Polymarket and World Bank proxy handlers had unguarded res.writeHead()
calls in error/timeout callbacks that race with the response callback.
When upstream partially responds then times out, both paths write
headers → process crash. Replace 5 raw writeHead+end calls with
safeEnd() which checks res.headersSent before writing.
* feat(breaking-news): add active alert banner with audio for critical/high RSS items (#508)
RSS items classified as critical/high threat now trigger a full-width
breaking news banner with audio alert, auto-dismiss (60s/30s by severity),
visibility-aware timer pause, dedup, and a toggle in the Intelligence
Findings dropdown.
* fix(sentry): filter Android OEM WebView bridge injection errors (#510)
Add ignoreErrors pattern for LIDNotifyId, onWebViewAppeared, and
onGetWiFiBSSID — native bridge functions injected by Lenovo/Huawei
device SDKs into Chrome Mobile WebView. No stack frames in our code.
* chore: add validated telegram channels list (global + ME + Iran + cyber) (#249)
* feat(conflict): add Iran Attacks map layer + strip debug logs (#511)
* chore: strip 61 debug console.log calls from services
Remove development/tracing console.log statements from 20 files.
These add noise to production browser consoles and increase bundle size.
Preserved: all console.error (error handling) and console.warn (warnings).
Preserved: debug-gated logs in runtime.ts (controlled by verbose flag).
Removed: debugInjectTestEvents() from geo-convergence.ts (test-only code).
Removed: logSummary()/logReport() methods that were pure console.log wrappers.
* fix: remove orphaned stubs and remaining debug logs from stripped services
- Remove empty logReport() method and unused startTime variable (parallel-analysis.ts)
- Remove orphaned console.group/console.groupEnd pair (parallel-analysis.ts)
- Remove empty logSignalSummary() export (signal-aggregator.ts)
- Remove logSignalSummary import/call and 3 remaining console.logs (InsightsPanel.ts)
- Remove no-op logDirectFetchBlockedOnce() and dead infrastructure (prediction/index.ts)
* feat(conflict): add Iran Attacks map layer
Adds a new Iran-focused conflict events layer that aggregates real-time
events, geocodes via 40-city lookup table, caches 15min in Redis, and
renders as a toggleable DeckGL ScatterplotLayer with severity coloring.
- New proto + codegen for ListIranEvents RPC
- Server handler with HTML parsing, city geocoding, category mapping
- Frontend service with circuit breaker
- DeckGL ScatterplotLayer with severity-based color/size
- MapPopup with sanitized source links
- iranAttacks toggle across all variants, harnesses, and URL state
* fix: resolve bootstrap 401 and 429 rate limiting on page init (#512)
Same-origin browser requests don't send Origin header (per CORS spec),
causing validateApiKey to reject them. Extract origin from Referer as
fallback. Increase rate limit from 60 to 200 req/min to accommodate
the ~50 requests fired during page initialization.
* fix(relay): prevent Polymarket OOM via request deduplication (#513)
Concurrent Polymarket requests for the same cache key each fired
independent https.get() calls. With 12 categories × multiple clients,
740 requests piled up in 10s, all buffering response bodies → 4.1GB
heap → OOM crash on Railway.
Fix: in-flight promise map deduplicates concurrent requests to the
same cache key. 429/error responses are negative-cached for 30s to
prevent retry storms.
* fix(threat-classifier): add military/conflict keyword gaps and news-to-conflict bridge (#514)
Breaking news headlines like "Israel's strike on Iran" were classified as
info level because the keyword classifier lacked standalone conflict phrases.
Additionally, the conflict instability score depended solely on ACLED data
(1-7 day lag) with no bridge from real-time breaking news.
- Add 3 critical + 18 high contextual military/conflict keywords
- Preserve threat classification on semantically merged clusters
- Add news-derived conflict floor when ACLED/HAPI report zero signal
- Upsert news events by cluster ID to prevent duplicates
- Extract newsEventIndex to module-level Map for serialization safety
* fix(breaking-news): let critical alerts bypass global cooldown and replace HIGH alerts (#516)
Global cooldown (60s) was blocking critical alerts when a less important
HIGH alert fired from an earlier RSS batch. Added priority-aware cooldown
so critical alerts always break through. Banner now auto-dismisses HIGH
alerts when a CRITICAL arrives. Added Iran/strikes keywords to classifier.
* fix(rate-limit): increase sliding window to 300 req/min (#515)
App init fires many concurrent classify-event, summarize-article, and
record-baseline-snapshot calls, exhausting the 200/min limit and causing
429s. Bump to 300 as a temporary measure while client-side batching is
implemented.
* fix(breaking-news): fix fake pubDate fallback and filter noisy think-tank alerts (#517)
Two bugs causing stale CrisisWatch article to fire as breaking alert:
1. Non-standard pubDate format ("Friday, February 27, 2026 - 12:38")
failed to parse → fallback was `new Date()` (NOW) → day-old articles
appeared as "just now" and passed recency gate on every fetch
2. Tier 3+ sources (think tanks) firing alerts on keyword-only matches
like "War" in policy analysis titles — too noisy for breaking alerts
Fix: parsePubDate() handles non-standard formats and falls back to
epoch (not now). Tier 3+ sources require LLM classification to fire.
* fix: make iran-events handler read-only from Redis (#518)
Remove server-side LiveUAMap scraper (blocked by Cloudflare 403 on
Vercel IPs). Handler now reads pre-populated Redis cache pushed from
local browser scraping. Change cache tier from slow to fast to prevent
CDN from serving stale empty responses for 30+ minutes.
* fix(relay): Polymarket circuit breaker + concurrency limiter (OOM fix) (#519)
* fix(rate-limit): increase sliding window to 300 req/min
App init fires many concurrent classify-event, summarize-article, and
record-baseline-snapshot calls, exhausting the 200/min limit and causing
429s. Bump to 300 as a temporary measure while client-side batching is
implemented.
* fix(relay): add Polymarket circuit breaker + concurrency limiter to prevent OOM
Railway relay OOM crash: 280 Polymarket 429 errors in 8s, heap hit 3.7GB.
Multiple unique cache keys bypassed per-key dedup, flooding upstream.
- Circuit breaker: trips after 5 consecutive failures, 60s cooldown
- Concurrent upstream limiter: max 3 simultaneous requests
- Negative cache TTL: 30s → 60s to reduce retry frequency
- Upstream slot freed on response.on('end'), not headers, preventing
body buffer accumulation past the concurrency cap
* fix(relay): guard against double-finalization on Polymarket timeout
request.destroy() in timeout handler also fires request.on('error'),
causing double decrement of polymarketActiveUpstream (counter goes
negative, disabling concurrency cap) and double circuit breaker trip.
Add finalized guard so decrement + failure accounting happens exactly
once per request regardless of which error path fires first.
* fix(threat-classifier): stagger AI classification requests to avoid Groq 429 (#520)
flushBatch() fired up to 20 classifyEvent RPCs simultaneously via
Promise.all, instantly hitting Groq's ~30 req/min rate limit.
- Sequential execution with 2s min-gap between requests (~28 req/min)
- waitForGap() enforces hard floor + jitter across batch boundaries
- batchInFlight guard prevents concurrent flush loops
- 429/5xx: requeue failed job (with retry cap) + remaining untouched jobs
- Queue cap at 100 items with warn on overflow
* fix(relay): regenerate package-lock.json with telegram dependency
The lockfile was missing resolved entries for the telegram package,
causing Railway to skip installation despite it being in package.json.
* chore: trigger deploy to flush CDN cache for iran-events endpoint
* Revert "fix(relay): regenerate package-lock.json with telegram dependency"
This reverts commit a8d5e1dbbd3300708a081d31783d93e793a072c0.
* fix(relay): add POLYMARKET_ENABLED env flag kill switch (#523)
Set POLYMARKET_ENABLED=false on Railway to disable all Polymarket
upstream requests. Returns 503 immediately, preventing OOM crashes.
* fix(breaking-news): fill keyword gaps missing real Iran attack headlines (#521)
* fix(breaking-news): fill keyword gaps that miss real Iran attack headlines
Three root causes for zero alerts during the Iran war:
1. Keyword gaps — top Iran headlines failed classification:
- "US and Israel attack Iran" → info (no "attack iran" keyword)
- "attacked Iran" → info (only "attacks iran" existed, plural)
- "Explosions heard in Tehran" → info (no "explosions" keyword)
Added: attack iran, attacked iran, attack on iran, attack against iran,
bombing/bombed iran, war against iran (CRITICAL); explosions,
launched/launches attacks, retaliatory/preemptive/preventive attack (HIGH)
2. 5-item RSS limit — Al Jazeera's CRITICAL "major combat operations"
headline was item #7 and never reached the classifier. Increased
per-feed limit from 5 to 10.
3. False positive — "OpenAI strikes deal with Pentagon" matched HIGH
keyword "strikes". Added "strikes deal/agreement/partnership" to
exclusions.
* fix(threat-classifier): prevent Iran compound keyword false positives
"attack iran" as plain substring matched "Iran-backed" and "Iranian"
in headlines about proxy groups, not direct attacks on Iran.
Added TRAILING_BOUNDARY_KEYWORDS set with negative lookahead (?![\w-])
for all Iran compound keywords. This rejects "Iran-backed militias"
and "Iranian targets" while still matching "attack Iran:" and
"attack Iran" at end of string.
Addresses Codex review comment on PR #521.
* fix(relay): regenerate package-lock.json with telegram dependency (#522)
The lockfile was missing resolved entries for the telegram package,
causing Railway to skip installation despite it being in package.json.
* fix(iran): bypass stale CDN cache for iran-events endpoint (#524)
The CDN cached empty {events:[],scrapedAt:0} from the pre-Redis
deployment and Vercel deploy didn't purge all edge nodes. Add ?_v=2
query param to force cache miss until CDN naturally expires the
stale slow-tier entry.
* fix(focal-points): attribute theater military activity to target nations (#525)
The signal aggregator attributed military flights/vessels to the country
they're physically over (point-in-polygon). Aircraft attacking Iran from
the Persian Gulf got attributed to XX/IQ/SA, not IR — so Iran showed
ELEVATED in Focal Points despite being under active attack (CRIT in
Strategic Posture).
Feed theater-level posture data back into the signal aggregator for
target nations (Iran, Taiwan, North Korea, Gaza, Yemen) so they get
credited for military activity in their theater bounding box. Includes
double-count guard to skip if the nation already has signals.
Also fixes stale "sebuf" comment in threat-classifier.
* fix(relay): block rsshub.app requests with 410 Gone (#526)
Stale clients still send RSS requests to rsshub.app (NHK, MOFCOM, MIIT).
These feeds were migrated to Google News RSS but cached PWA clients keep
hitting the relay, which forwards to rsshub.app and gets 403.
- Add explicit blocklist returning 410 Gone before allowlist check
- Remove rsshub.app from all allowlists (relay, edge proxy, vite)
- Remove dead AP News dev proxy target
* feat(map): prioritize Iran Attacks layer (#527)
* feat(map): move Iran Attacks layer to first position and enable by default
Move iranAttacks to the top of the layer toggle list in the full
(geopolitical) variant so it appears first. Enable it by default on
both desktop and mobile during the active conflict.
* feat(map): add Iran Attacks layer support to SVG/mobile map
- Implement setIranEvents() in SVG Map (was no-op)
- Render severity-colored circle markers matching DeckGL layer
- Add iranAttacks to mobile layer toggles (first position)
- Forward setIranEvents to SVG map in MapContainer
- Add IranEventPopupData to PopupData union for click popups
- Add .iran-event-marker CSS with pulse animation
- Add data-layer-hidden-iranAttacks CSS toggle
* fix(geo): expand geo hub index with 60+ missing world locations (#528)
The geo hub index only had ~30 entries, missing all Gulf states (UAE,
Qatar, Bahrain, Kuwait, Oman), Iraq cities, and many world capitals.
News mentioning Abu Dhabi, Dubai, Baghdad, etc. had no lat/lon assigned
so they never appeared on the map.
Added: Gulf capitals (Abu Dhabi, Dubai, Doha, Manama, Kuwait, Muscat),
Iraq (Baghdad, Erbil, Basra), Jordan, Istanbul, Haifa, Dimona, Isfahan,
Kabul, Mumbai, Shanghai, Hong Kong, Singapore, Manila, Jakarta, Bangkok,
Hanoi, Canberra, all major European capitals (Rome, Madrid, Warsaw,
Bucharest, Helsinki, Stockholm, Oslo, Baltics, Athens, Belgrade, Minsk,
Tbilisi, Chisinau, Yerevan, Baku), Americas (Ottawa, Mexico City,
Brasilia, Buenos Aires, Caracas, Bogota, Havana), Africa (Nairobi,
Pretoria, Lagos, Kinshasa, Mogadishu, Tripoli, Tunis, Algiers, Rabat),
conflict zones (Iraq, Kashmir, Golan), chokepoints (Malacca, Panama,
Gibraltar), and US military bases (Ramstein, Incirlik, Diego Garcia,
Guam, Okinawa).
* fix(iran): bust CDN cache to serve updated Gulf-geocoded events (#532)
CDN edge cache was still serving stale 93-event response without
Gulf state coordinates (UAE, Bahrain, Qatar, Kuwait). Bump cache
key from ?_v=2 to ?_v=3 so browsers fetch fresh 100-event data.
Also gitignore internal/ for private tooling scripts.
* fix(alerts): remove SESSION_START gate that blocked pre-existing breaking news (#533)
The isRecent() function used Math.max(now - 15min, SESSION_START) as
recency cutoff. Since SESSION_START = Date.now() at module load, items
published before page load could never trigger alerts — they failed the
SESSION_START gate in the first 15 min, then aged past the 15-min window.
Now uses only the 15-minute recency window. Spam prevention remains via
per-event dedup (30 min), global cooldown (60s), and source tier filter.
* fix(relay): Telegram + OOM + memory cleanup (#531)
* fix(relay): resolve Telegram missing package, OOM crashes, and memory cleanup
- Add `telegram` and `ws` to root dependencies so Railway's `npm install` installs them
- Log V8 heap limit at startup to confirm NODE_OPTIONS is active
- Make MAX_VESSELS/MAX_VESSEL_HISTORY env-configurable (default 20k, down from 50k)
- Add permanent latch to skip Telegram import retries when package is missing
- Raise memory cleanup threshold from 450MB to 2GB (env-configurable)
- Clear all caches (RSS, Polymarket, WorldBank) during emergency cleanup
* fix(relay): treat blank env vars as unset in safeInt
Number('') === 0 passes isFinite, silently clamping caps to 1000
instead of using the 20000 default. Guard empty/null before parsing.
* fix(live-news): replace 7 stale YouTube fallback video IDs (#535)
Validated all 23 YouTube fallbackVideoIds via oEmbed API and all 9
HLS URLs. Found 5 broken IDs (403 embed-restricted or 404 deleted)
plus 2 previously identified stale IDs:
- Fox News: QaftgYkG-ek → ZvdiJUYGBis
- Sky News Arabia: MN50dHFHMKE → U--OjmpjF5o
- RTVE 24H: 7_srED6k0bE → -7GEFgUKilA
- CNN Brasil: 1kWRw-DA6Ns → 6ZkOlaGfxq4
- C5N: NdQSOItOQ5Y → SF06Qy1Ct6Y
- TBS NEWS DIG: ohI356mwBp8 → Anr15FA9OCI
- TRT World: CV5Fooi8WDI → ABfFhWzWs0s
All 9 HLS URLs validated OK. 16 remaining YouTube IDs validated OK.
* fix(relay): fix telegram ESM import path and broaden latch regex
- `import('telegram/sessions')` fails with "Directory import is not
supported resolving ES modules" — use explicit `telegram/sessions/index.js`
- Broaden permanent-disable latch to also catch "Directory import" errors
* fix(ui): move download banner to bottom-right (#536)
* fix(alerts): remove SESSION_START gate that blocked pre-existing breaking news
The isRecent() function used Math.max(now - 15min, SESSION_START) as
recency cutoff. Since SESSION_START = Date.now() at module load, items
published before page load could never trigger alerts — they failed the
SESSION_START gate in the first 15 min, then aged past the 15-min window.
Now uses only the 15-minute recency window. Spam prevention remains via
per-event dedup (30 min), global cooldown (60s), and source tier filter.
* fix(ui): move download banner to bottom-right of screen
Repositioned from top-right (overlapping content) to bottom-right.
Dismissal already persists via localStorage. Added TODO for header
download link.
* Revert "fix(relay): fix telegram ESM import path and broaden latch regex"
This reverts commit 1f2f0175abdc52d4ee841ac271eb0f48000cbddf.
* Revert "Revert "fix(relay): fix telegram ESM import path and broaden latch regex"" (#537)
This reverts commit ad41a2e2d245850fd4b699af2adbe53acca80325.
* feat: add day/night solar terminator overlay to map (#529)
* Trigger redeploy with preview env vars
* Trigger deployment
* chore: trigger redeploy for PR #41
* chore: trigger Vercel redeploy (edge function transient failure)
* chore: retrigger Vercel deploy
* feat: add Nigeria feeds and Greek locale feeds (#271)
- Add 5 Nigeria news sources to Africa section (Premium Times, Vanguard,
Channels TV, Daily Trust, ThisDay)
- Add 5 Greek feeds with lang: 'el' for locale-aware filtering
(Kathimerini, Naftemporiki, in.gr, iefimerida, Proto Thema)
- Add source tiers for all new outlets
- Allowlist 8 new domains in RSS proxy
* fix: enforce military bbox filtering and add behavioral cache tests (#284)
* fix: add request coalescing to Redis cache layer
Concurrent cache misses for the same key now share a single upstream
fetch instead of each triggering redundant API calls. This eliminates
duplicate work within Edge Function invocations under burst traffic.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: reduce AIS polling frequency from 10s to 30s
Vessel positions do not change meaningfully in 10 seconds at sea.
Reduces Railway relay requests by 66% with negligible UX impact.
Stale threshold bumped to 45s to match the new interval.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: quantize military flights bbox cache keys to 1-degree grid
Precise bounding box coordinates caused near-zero cache hit rate since
every map pan/zoom produced a unique key. Snapping to a 1-degree grid
lets nearby viewports share cache entries, dramatically reducing
redundant OpenSky API calls.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: parallelize ETF chart fetches instead of sequential await loop
The loop awaited each ETF chart fetch individually, blocking on every
Yahoo gate delay. Using Promise.allSettled lets all 10 fetches queue
concurrently through the Yahoo gate, cutting wall time from ~12s to ~6s.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add Redis pipeline batch GET to reduce round-trips
Add getCachedJsonBatch() using the Upstash pipeline API to fetch
multiple keys in a single HTTP call. Refactor aircraft details batch
handler from 20 sequential GETs to 1 pipelined request.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* test: add structural tests for Redis caching optimizations
18 tests covering: cachedFetchJson request coalescing (in-flight dedup,
cache-before-fetch ordering, cleanup), getCachedJsonBatch pipeline API,
aircraft batch handler pipeline usage, bbox grid quantization (1-degree
step, expanded fetch bbox), and ETF parallel fetch.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: enforce military bbox contract and add behavioral cache tests
---------
Co-authored-by: Elias El Khoury <efk@anghami.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add User-Agent and Cloudflare 403 detection to all secret validation probes (#296)
Sidecar validation probes were missing User-Agent headers, causing
Cloudflare-fronted APIs (e.g. Wingbits) to return 403 which was
incorrectly treated as an auth rejection. Added CHROME_UA to all 13
probes and isCloudflare403() helper to soft-pass CDN blocks.
* fix: open external links in system browser on Tauri desktop (#297)
Tauri WKWebView/WebView2 traps target="_blank" navigation, so news
links and other external URLs silently fail to open. Added a global
capture-phase click interceptor that routes cross-origin links through
the existing open_url Tauri command, falling back to window.open.
* fix: add Greek flag mapping to language selector (#307)
* fix: add missing country brief i18n keys and export PDF option (#308)
- Add levels, trends, fallback keys to top-level countryBrief in en/el/th/vi
locales (fixes raw key display in intelligence brief and header badge)
- Add Export PDF option to country brief dropdown using scoped print dialog
- Add exportPdf i18n key to all 17 locale files
* feat: add day/night solar terminator overlay to map
Add a real-time day/night overlay layer using deck.gl PolygonLayer that
renders the solar terminator (boundary between day and night zones).
The overlay uses astronomical formulas (Meeus) to compute the subsolar
point and trace the terminator line at 1° resolution.
- New toggleable "Day/Night" layer in all 3 variants (full/tech/finance)
- Theme-aware styling (lighter fill on light theme, darker on dark)
- Auto-refresh every 5 minutes with conditional timer (only runs when
layer is enabled, pauses when render is paused)
- Cached polygon computation to avoid recomputing on every render
- i18n translations for all 17 locales
- Updated documentation with new layer entry
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address review feedback — equinox terminator + locale indentation
- Replace safeTanDecl epsilon clamp with proper equinox handling:
when |tanDecl| < 1e-6, draw terminator as vertical great circle
through the poles (subsolar meridian ±90°) instead of clamping
- Fix JSON indentation in all 17 locale files: dayNight and
tradeRoutes keys were left-aligned instead of matching 8-space
indentation of surrounding keys
---------
Co-authored-by: Elie Habib <elie.habib@gmail.com>
Co-authored-by: Elias El Khoury <efk@anghami.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* fix(relay): auto-reconnect on Telegram AUTH_KEY_DUPLICATED and fix IranIntl handle (#539)
- On AUTH_KEY_DUPLICATED (406), disconnect client and set to null so
next poll cycle reconnects fresh — self-heals after competing client dies
- Fix IranIntl → iranintltv (correct Telegram channel handle)
* fix(live-news): add fallback video ID for LiveNOW from FOX channel (#538)
The livenow-fox channel had no fallbackVideoId, relying solely on
YouTube handle lookup which fails intermittently. Added ZvdiJUYGBis
(confirmed live stream) as fallback.
* fix(iran): bump CDN cache-bust to v4 for fresh event data (#544)
100 new events pushed to Redis covering active Iran-Israel-US
conflict theater including Gulf states (UAE, Bahrain, Qatar,
Kuwait, Jordan). Bump ?_v=3 to ?_v=4 to bypass stale CDN.
* fix(telegram): fix ESM import path in session-auth script (#542)
telegram/sessions → telegram/sessions/index.js (same fix as relay)
* fix(telegram): latch AUTH_KEY_DUPLICATED to stop retry spam (#543)
AUTH_KEY_DUPLICATED is permanent — the session string is invalidated
and no amount of retrying will fix it. Previously the relay retried
every 60s, spamming logs. Now it logs a clear error message with
instructions to regenerate the session and stops retrying.
Renamed telegramImportFailed → telegramPermanentlyDisabled to cover
both import failures and auth failures under one latch.
* fix(live-news): fix broken Europe channel handles + add fallback video IDs (#541)
* fix(live-news): fix broken Europe channel handles + add fallback video IDs
- Fix France 24 English handle: @FRANCE24English (404) → @France24_en
- Fix WELT handle: @WELTNachrichtensender (hijacked to "Movie Glow") → @WELTVideoTV
- Add fallbackVideoId for BBC News, France 24 EN, TRT Haber, NTV Turkey,
CNN TURK, TVP Info, Telewizja Republika (verified via Playwright)
- Update stale fallback IDs for Fox News, RTVE, CNN Brasil, C5N, TBS News,
Sky News Arabia, TRT World
* fix(live-news): update CBS News fallback video ID
* fix(live-news): update Newsmax fallback video ID
* fix(live-news): add NBC News fallback video ID
* fix(live-news): full channel audit — fix 10 broken handles + update 8 stale fallbacks
Broken handles fixed:
- Bloomberg: @Bloomberg (404) → @markets
- WION: @WIONews (wrong channel "Write It Out") → @WION
- CTI News: @CtiTv (404) → @中天新聞CtiNews
- VTC NOW: @VTCNOW (404) → @VTCNowOfficial
- Record News: @recordnewsoficial (404) → @RecordNews
- T13: @T13 (404) → @Teletrece
- Channels TV: @channelstv (404) → @ChannelsTelevision
- KTN News: @KTNNewsKE (404) → @ktnnews_kenya
- eNCA: @enewschannel (404) → @eNCA
- SABC News: @SABCNews (404) → @SABCDigitalNews
Stale fallback video IDs refreshed:
- Sky News, NASA, CBC News, CNN Brasil, C5N, TBS NEWS DIG,
Sky News Arabia, TRT World
* feat(oref): add OREF sirens panel with Hebrew-to-English translation (#545)
Add real-time Israel Home Front Command (OREF) siren alerts panel:
- Edge Function proxy at api/oref-alerts.js
- OrefSirensPanel component with live/history views
- oref-alerts service with 10s polling and update callbacks
- Hebrew→English translation via existing translateText() LLM chain
with 3-layer caching (in-memory Map → server Redis → circuit breaker)
- i18n strings for all 23 locales
- Panel registration, data-loader integration, CSS styles
* fix(relay): use execFileSync for OREF curl to avoid shell injection (#546)
Proxy credentials with special characters (semicolons, dollar signs)
were interpolated into a shell command via execSync. Switch to
execFileSync which passes args directly without shell parsing.
* gave the user freedom to resize panels "fixes issue #426" (#489)
* gave the user freedom to resize panles
* feat(panels): add horizontal resize with col-span persistence
* feat(cii): integrate Iran strike events into CII scoring, country brief & timeline (#547)
Iran had ~100 geolocated strike events but the CII was unaware of them:
conflict score stuck at 70 (ACLED only), no strike chip in Active Signals,
timeline conflict lane empty, intelligence brief silent on strikes.
Changes:
- Add strikes[] to CountryData and ingestStrikesForCII() with geo-lookup
fallback (bounding boxes when GeoJSON not yet loaded)
- Boost CII conflict score with 7-day freshness window
(min(50, count*3 + highSev*5))
- Cache iranEvents in IntelligenceCache, preserve across refresh cycles
- Wire data loading: always load Iran events (not gated by map layer),
ingest into CII, trigger panel refresh
- Add activeStrikes to CountryBriefSignals with geo-lookup counting
- Render strike chip in Active Signals and include in fallback brief
- Feed strike events into 7-day timeline (conflict lane)
- Add structured strikeCount/highSeverityStrikeCount fields to GeoSignal
(replaces fragile regex parsing in focal-point-detector)
- Add active_strike signal type to InsightsPanel focal points
- Add bounding-box fallback to signal aggregator for conflict events
- Add i18n keys for activeStrikes
* fix(alerts): add compound escalation for military action + geopolitical target (#548)
Keyword matching was too rigid — "attacks on iran" didn't match CRITICAL
because only "attack on iran" (singular) existed. Headlines like
"strikes by US and Israel on Iran" also missed because words weren't
adjacent.
Added compound escalation: if a HIGH military/conflict keyword matches
AND the headline mentions a critical geopolitical target (Iran, Russia,
China, Taiwan, NATO, US forces), escalate to CRITICAL. Also added
missing Iran keyword variants (plural forms, "Iran retaliates/strikes").
* feat(conflict): enhance Iran events popup with severity badge and related events (#549)
Rewrite the Iran events popup to follow the established popup pattern
(conflict/protest) with severity-colored header, badge, close button,
stat rows, and source link using CSS classes instead of inline styles.
- Add normalizeSeverity helper (clamps unknown values to 'low')
- Show related events from same location (normalized matching, max 5)
- Add IranEventPopupData to PopupData union (removes unsafe double cast)
- Add iranEvent header CSS with severity border-left colors
- Add i18n keys for en/ar/fr
* feat(telegram): add Telegram Intel panel (#550)
* feat(telegram): add Telegram Intel panel consuming relay feed
- Service layer: fetchTelegramFeed() with 30s cache, types matching relay shape
- Panel component: topic filter tabs, safe DOM rendering via h()+replaceChildren()
- DataLoader + RefreshScheduler pattern (60s interval, hidden-tab aware)
- Handles enabled=false and empty states from relay
- CSS following existing gdelt-intel pattern
- Panel title localized across all 18 locales
* fix(i18n): add components.telegramIntel translations to 10 remaining locales
* feat(live-news): add residential proxy + gzip decompression for YouTube detection (#551)
YouTube blocks Vercel datacenter IPs — returns HTML without videoDetails/isLive
data. Switch from edge runtime to Node.js serverless to enable HTTP CONNECT
tunnel proxy via YOUTUBE_PROXY_URL env var. Add zlib decompression for gzip
responses (YouTube returns empty body without Accept-Encoding header).
Also adds missing fallback video IDs for WELT, KTN News, CNA NewsAsia,
and updates TBS NEWS DIG fallback.
* debug(live-news): add debug param to diagnose proxy env var on Vercel
* fix(live-news): set explicit runtime: 'nodejs' for proxy support
Vercel defaults to edge runtime when not specified. node:http/https/zlib
imports are unavailable in edge — causing FUNCTION_INVOCATION_FAILED.
Remove debug param added in previous commit.
* fix(live-news): lazy-load node modules + proxy fallback to direct fetch
Top-level import of node:http/https/zlib crashes if Vercel bundles
for edge despite runtime: 'nodejs' config. Use dynamic import() to
lazy-load at call time. Also add try/catch around proxy so it falls
back to direct fetch if proxy connection fails.
* feat(aviation): integrate AviationStack API for non-US airport delays (#552)
Replace 100% simulated delay data for international airports with real
flight data from AviationStack API. Add 28 Middle East/conflict-zone
airports (Iran, Iraq, Lebanon, Syria, Yemen, Pakistan, Libya, Sudan).
Key changes:
- AviationStack integration with bounded concurrency (5 parallel),
rotating batch (20 airports/cycle), and 20s deadline
- Redis SETNX lock prevents cross-isolate cache stampede on expiry
- Split FAA/intl caches (both 30min TTL) with isolated error handling
- Fix severity colors (was checking 'GS'/'GDP', now minor/moderate/major/severe)
- Fix tooltip (was obj.airport, now obj.name + obj.iata)
- Add FLIGHT_DELAY_TYPE_CLOSURE for airport/airspace closures
- Add closure i18n key across all 18 locales
- Graceful fallback: no API key → simulation; API failure → simulation
* feat(live-news): move YouTube proxy scraping to Railway relay
Vercel serverless cannot use node:http/https for HTTP CONNECT proxy
tunnels. Move the residential proxy YouTube scraping to the Railway
relay (ais-relay.cjs) which has full Node.js access.
- Add /youtube-live route to relay with proxy + direct fetch fallback
- Add 5-min in-memory cache for channel lookups, 1hr for oembed
- Revert Vercel api/youtube/live.js to edge runtime — now proxies to
Railway first, falls back to direct scrape
* feat(settings): add AVIATIONSTACK_API to desktop settings page (#553)
Register the key in Rust keychain (SUPPORTED_SECRET_KEYS), frontend
RuntimeSecretKey type, feature toggle, and settings UI under
"Tracking & Sensing" category.
* fix(live-news): use correct relay auth header for YouTube proxy (#554)
Edge function was sending X-Relay-Auth header with RELAY_AUTH_TOKEN env
var, but the Railway relay expects x-relay-key header validated against
RELAY_SHARED_SECRET. This mismatch caused the relay to reject requests
from Vercel, falling back to direct YouTube scrape (which fails from
datacenter IPs for many channels).
* fix(live-news): align YouTube edge function with relay auth pattern (#555)
Use same getRelayBaseUrl/getRelayHeaders as other edge functions:
- WS_RELAY_URL env var instead of VITE_WS_API_URL
- RELAY_SHARED_SECRET + RELAY_AUTH_HEADER for flexible auth
- Dual x-relay-key + Authorization headers
* fix(i18n): rename OREF Sirens panel to Israel Sirens (#556)
Remove internal implementation references (OREF, proxy relay, oref.org.il)
from all user-facing strings across 18 locales and panel config.
* fix(live-news): annotate empty catches and sanitize error output (#560)
- Add context comments to empty catch blocks for debuggability
- Replace error.message leak with generic client-safe message
* fix(sentry): add noise filters and fix beforeSend null-filename leak (#561)
- Add 8 new ignoreErrors patterns: signal timeout, premium gate,
hybridExecute/mag/webkit bridge injections, postMessage null,
NotSupportedError, appendChild injection, luma assignment
- Fix LIDNotify regex to match both LIDNotify and LIDNotifyId
- Fix beforeSend: strip null/anonymous filename frames so deck.gl
TypeErrors (28 events, 8 users) are properly suppressed
* feat(cii): wire OREF sirens into CII score & country brief (#559)
* feat(cii): wire OREF sirens into CII score and country brief
Active OREF sirens now boost Israel's CII score through two channels:
- Conflict component: +25 base + min(25, alertCount*5) for active sirens
- Blended score: +15 for active sirens, +5/+10 for 24h history thresholds
Country brief for Israel shows a siren signal chip when alerts are active.
* refactor(cii): extract getOrefBlendBoost helper to DRY scoring paths
* fix(relay): add graceful shutdown + poll concurrency guard for Telegram (#562)
- SIGTERM/SIGINT handler disconnects Telegram client before container dies
- telegramPollInFlight guard prevents overlapping poll cycles
- Mid-poll AUTH_KEY_DUPLICATED now permanently disables (was reconnect loop)
* fix(aviation): query all airports instead of rotating batch (#557)
* fix(aviation): query all airports instead of rotating batch of 20
The rotating batch (20 airports/cycle) caused major airports like DXB
(52% cancellations) to be missed entirely for multiple cache cycles.
With a paid AviationStack plan, query all ~90 non-US airports per
refresh with concurrency 10 and 50s deadline (~9 chunks × 5s = 45s).
* feat(cii): feed airport disruptions into CII and country brief
Major/severe airport delays and closures now boost the CII security
score and appear as signal chips in country briefs. Only major+
severity alerts are ingested to avoid noise from minor delays.
- Add aviationDisruptions to CountryData and ingestAviationForCII()
- Boost security score: closure +20, severe +15, major +10, moderate +5
- Store flight delays in intelligenceCache for country brief access
- Add aviation disruptions chip in country brief signals grid
* fix(relay): replace smart quotes crashing relay on startup (#563)
* fix(relay): replace Unicode smart quotes crashing Node.js CJS parser
* fix(relay): await Telegram disconnect + guard startup poll
* fix(cii): resolve Gulf country strike misattribution via multi-match bbox disambiguation (#564)
Dubai/Doha/Bahrain/Kuwait coordinates matched Iran's bounding box first
due to iteration order. Now collects ALL matching bboxes, disambiguates
via isCoordinateInCountry() geometry, and falls back to smallest-area bbox.
- Add BH, QA, KW, JO, OM to bounds tables (previously missing entirely)
- Extract ME_STRIKE_BOUNDS + resolveCountryFromBounds() into country-geometry.ts
- All 4 consumer files use shared constant (single source of truth)
- Bump CDN cache-bust param for iran-events endpoint
* fix(relay): upstreamWs → upstreamSocket in graceful shutdown (#565)
* fix(relay): install curl in Railway container for OREF polling (#567)
* fix(relay): increase Polymarket cache TTL to 10 minutes (#568)
* fix(relay): increase Polymarket cache TTL to 10 minutes
All requests were MISS with 2-min TTL under concurrent load.
Bump to 10-min cache and 5-min negative cache to reduce upstream pressure.
* fix(relay): normalize Polymarket cache key from canonical params
Raw url.search as cache key meant ?tag=fed&endpoint=events and
?endpoint=events&tag_slug=fed produced different keys for the same
upstream request — defeating both cache and inflight dedup, causing
121 MISS entries in 3 seconds.
Build cache key from parsed canonical params (endpoint + sorted
query string) so all equivalent requests share one cache entry.
* feat(webcams): add Iran tab to live webcams panel (#569)
Add dedicated Iran region tab as the first/default tab with 4 feeds:
Tehran, Middle East overview, Tehran (alt angle), and Jerusalem.
* fix(relay): replace nixpacks.toml with railpack.json for curl (#571)
Railway uses Railpack (not Nixpacks). nixpacks.toml in scripts/ was
silently skipped. Use railpack.json at repo root with deploy.aptPackages
to install curl at runtime for OREF polling.
* fix(webcams): replace duplicate Tehran feed with Tel Aviv, rename Iran tab (#572)
- Remove duplicate iran-tehran2 feed (same channel/video as iran-tehran)
- Remove iran-mideast feed
- Add Tel Aviv feed (-VLcYT5QBrY) to Iran Attacks tab
- Rename tab label from "IRAN" to "IRAN ATTACKS" across all 18 locales
* feat(scripts): add Iran events seed script and latest data (#575)
Add seed-iran-events.mjs for importing Iran conflict events into Redis
(conflict:iran-events:v1). Includes geocoding by location keywords and
category-to-severity mapping. Data file contains 100 events from
2026-02-28.
* fix(relay): add timeouts and logging to Telegram poll loop (#578)
GramJS getEntity/getMessages have no built-in timeout. When the first
channel hangs (FLOOD_WAIT, MTProto stall), telegramPollInFlight stays
true forever, blocking all future polls — zero messages collected, zero
errors logged, frontend shows "No messages available".
- Add 15s per-channel timeout on getEntity + getMessages calls
- Add 3-min overall poll cycle timeout
- Force-clear stuck in-flight flag after 3.5 minutes
- Detect FLOOD_WAIT errors and break loop early
- Log per-cycle summary: channels polled, new msgs, errors, duration
- Track media-only messages separately (no text → not a bug)
- Expose lastError, pollInFlight, pollInFlightSince on /status endpoint
* feat(cii): hook security advisories into CII scoring & country briefs (#579)
Travel advisories (Do Not Travel, Reconsider, Caution) from US, AU, UK,
NZ now act as a floor and boost on CII scores. Do Not Travel guarantees
a minimum score of 60 (elevated), Reconsider floors at 50. Multi-source
corroboration (3+ govts) adds +5 bonus.
Advisory chips appear in country brief signal grid with level-appropriate
styling, and advisory context is passed to AI brief generation.
- Extract target country from advisory titles via embassy feed tags and
country name matching
- Add advisoryMaxLevel/advisoryCount/advisorySources to CII CountryData
- Wire ingestAdvisoriesForCII into data loader pipeline
- Add travelAdvisories/travelAdvisoryMaxLevel to CountryBriefSignals
- Render advisory signal chips in CountryBriefPage
* fix(sentry): guard setView against invalid preset + filter translateNotifyError (#580)
- DeckGLMap.setView(): early-return if VIEW_PRESETS[view] is undefined,
preventing TypeError on 'longitude' when select value is invalid
- Add ignoreErrors pattern for Google Translate widget crash
* feat(relay): bootstrap OREF 24h history on startup (#582)
* fix(relay): improve OREF curl error logging with stderr capture
-s flag silenced curl errors. Add -S to show errors, capture stderr
via stdio pipes, and log curl's actual er…
koala73
added a commit
that referenced
this pull request
Mar 1, 2026
* Add Security Advisories panel with government travel alerts (#460)
* feat: add Security Advisories panel with government travel advisory feeds
Adds a new panel aggregating travel/security advisories from official
government foreign affairs agencies (US State Dept, AU DFAT Smartraveller,
UK FCDO, NZ MFAT). Advisories are categorized by severity level
(Do Not Travel, Reconsider, Caution, Normal) with filter tabs by
source country. Includes summary counts, auto-refresh, and persistent
caching via the existing data-freshness system.
* chore: update package-lock.json
* fix: event delegation, localization, and cleanup for SecurityAdvisories panel
P1 fixes:
- Use event delegation on this.content (bound once in constructor) instead
of direct addEventListener after each innerHTML replacement — prevents
memory leaks and stale listener issues on re-render
- Use setContent() consistently instead of mixing with this.content.innerHTML
- Add securityAdvisories translations to all 16 non-English locale files
(panels name, component strings, common.all key)
- Revert unrelated package-lock.json version bump
P2 fixes:
- Deduplicate loadSecurityAdvisories — loadIntelligenceData now calls the
shared method instead of inlining duplicate fetch+set logic
- Add Accept header to fetch calls for better content negotiation
* feat(advisories): add US embassy alerts, CDC, ECDC, and WHO health feeds
Adds 21 new advisory RSS feeds:
- 13 US Embassy per-country security alerts (TH, AE, DE, UA, MX, IN, PK, CO, PL, BD, IT, DO, MM)
- CDC Travel Notices
- 5 ECDC feeds (epidemiological, threats, risk assessments, avian flu, publications)
- 2 WHO feeds (global news, Africa emergencies)
Panel gains a Health filter tab for CDC/ECDC/WHO sources.
All new domains added to RSS proxy allowlist.
i18n "health" key added across all 17 locales.
* feat(cache): add negative-result caching to cachedFetchJson (#466)
When upstream APIs return errors (HTTP 403, 429, timeout), fetchers
return null. Previously null results were not cached, causing repeated
request storms against broken APIs every refresh cycle.
Now caches a sentinel value ('__WM_NEG__') with a short 2-minute TTL
on null results. Subsequent requests within that window get null
immediately without hitting upstream. Thrown errors (transient) skip
sentinel caching and retry immediately.
Also filters sentinels from getCachedJsonBatch pipeline reads and fixes
theater posture coalescing test (expected 2 OpenSky fetches for 2
theater query regions, not 1).
* feat: convert 52 API endpoints from POST to GET for edge caching (#468)
* feat: convert 52 API endpoints from POST to GET for edge caching
Convert all cacheable sebuf RPC endpoints to HTTP GET with query/path
parameters, enabling CDN edge caching to reduce costs. Flatten nested
request types (TimeRange, PaginationRequest, BoundingBox) into scalar
query params. Add path params for resource lookups (GetFredSeries,
GetHumanitarianSummary, GetCountryStockIndex, GetCountryIntelBrief,
GetAircraftDetails). Rewrite router with hybrid static/dynamic matching
for path param support.
Kept as POST: SummarizeArticle, ClassifyEvent, RecordBaselineSnapshot,
GetAircraftDetailsBatch, RegisterInterest.
Generated with sebuf v0.9.0 (protoc-gen-ts-client, protoc-gen-ts-server).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add rate_limited field to market response protos
The rateLimited field was hand-patched into generated files on main but
never declared in the proto definitions. Regenerating wiped it out,
breaking the build. Now properly defined in both ListEtfFlowsResponse
and ListMarketQuotesResponse protos.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* chore: remove accidentally committed .planning files
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add Cloudflare edge caching infrastructure for api.worldmonitor.app (#471)
Route web production RPC traffic through api.worldmonitor.app via fetch
interceptor (installWebApiRedirect). Add default Cache-Control headers
(s-maxage=300, stale-while-revalidate=60) on GET 200 responses, with
no-store override for real-time endpoints (vessel snapshot). Update CORS
to allow GET method. Skip Vercel bot middleware for API subdomain using
hostname check (non-spoofable, replacing CF-Ray header approach). Update
desktop cloud fallback to route through api.worldmonitor.app.
* fix(beta): eagerly load T5-small model when beta mode is enabled
BETA_MODE now couples the badge AND model loading — the summarization-beta
model starts loading on startup instead of waiting for the first summarization call.
* fix: move 5 path-param endpoints to query params for Vercel routing (#472)
Vercel's `api/[domain]/v1/[rpc].ts` captures one dynamic segment.
Path params like `/get-humanitarian-summary/SA` add an extra segment
that has no matching route file, causing 404 on both OPTIONS preflight
and direct requests. These endpoints were broken in production.
Changes:
- Remove `{param}` from 5 service.proto HTTP paths
- Add `(sebuf.http.query)` annotations to request message fields
- Update generated client/server code to use URLSearchParams
- Update OpenAPI specs (YAML + JSON) to declare query params
- Add early-return guards in 4 handlers for missing required params
- Add happy.worldmonitor.app to runtime.ts redirect hosts
Affected endpoints:
- GET /api/conflict/v1/get-humanitarian-summary?country_code=SA
- GET /api/economic/v1/get-fred-series?series_id=T10Y2Y&limit=120
- GET /api/market/v1/get-country-stock-index?country_code=US
- GET /api/intelligence/v1/get-country-intel-brief?country_code=US
- GET /api/military/v1/get-aircraft-details?icao24=a12345
* fix(security-advisories): route feeds through RSS proxy to avoid CORS blocks (#473)
- Advisory feeds were fetched directly from the browser, hitting CORS
on all 21 feeds (US State Dept, AU Smartraveller, US Embassies, ECDC,
CDC, WHO). Route through /api/rss-proxy on web, keep proxyUrl for desktop.
- Fix double slash in ECDC Avian Influenza URL (323//feed → 323/feed)
- Add feeds.news24.com to RSS proxy allowlist (was returning 403)
* feat(cache): tiered edge Cache-Control aligned to upstream TTLs (#474)
* fix: move 5 path-param endpoints to query params for Vercel routing
Vercel's `api/[domain]/v1/[rpc].ts` captures one dynamic segment.
Path params like `/get-humanitarian-summary/SA` add an extra segment
that has no matching route file, causing 404 on both OPTIONS preflight
and direct requests. These endpoints were broken in production.
Changes:
- Remove `{param}` from 5 service.proto HTTP paths
- Add `(sebuf.http.query)` annotations to request message fields
- Update generated client/server code to use URLSearchParams
- Update OpenAPI specs (YAML + JSON) to declare query params
- Add early-return guards in 4 handlers for missing required params
- Add happy.worldmonitor.app to runtime.ts redirect hosts
Affected endpoints:
- GET /api/conflict/v1/get-humanitarian-summary?country_code=SA
- GET /api/economic/v1/get-fred-series?series_id=T10Y2Y&limit=120
- GET /api/market/v1/get-country-stock-index?country_code=US
- GET /api/intelligence/v1/get-country-intel-brief?country_code=US
- GET /api/military/v1/get-aircraft-details?icao24=a12345
* feat(cache): add tiered edge Cache-Control aligned to upstream TTLs
Replace flat s-maxage=300 with 5 tiers (fast/medium/slow/static/no-store)
mapped per-endpoint to respect upstream Redis TTLs. Adds stale-if-error
resilience headers and X-No-Cache plumbing for future degraded responses.
X-Cache-Tier debug header gated behind ?_debug query param.
* fix(tech): use rss() for CISA feed, drop build from pre-push hook (#475)
- CISA Advisories used dead rss.worldmonitor.app domain (404), switch to rss() helper
- Remove Vite build from pre-push hook (tsc already catches errors)
* fix(desktop): enable click-to-play YouTube embeds + CISA feed fixes (#476)
* fix(tech): use rss() for CISA feed, drop build from pre-push hook
- CISA Advisories used dead rss.worldmonitor.app domain (404), switch to rss() helper
- Remove Vite build from pre-push hook (tsc already catches errors)
* fix(desktop): enable click-to-play for YouTube embeds in WKWebView
WKWebView blocks programmatic autoplay in cross-origin iframes regardless
of allow attributes, Permissions-Policy, mute-first retries, or secure
context. Documented all 10 approaches tested in docs/internal/.
Changes:
- Switch sidecar embed origin from 127.0.0.1 to localhost (secure context)
- Add MutationObserver + retry chain as best-effort autoplay attempts
- Use postMessage('*') to fix tauri://localhost cross-origin messaging
- Make sidecar play overlay non-interactive (pointer-events:none)
- Fix .webcam-iframe pointer-events:none blocking clicks in grid view
- Add expand button to grid cells for switching to single view on desktop
- Add http://localhost:* to CSP frame-src in index.html and tauri.conf.json
* fix(gateway): convert stale POST requests to GET for backwards compat (#477)
Stale cached client bundles still send POST to endpoints converted to
GET in PR #468, causing 404s. The gateway now parses the POST JSON body
into query params and retries the match as GET.
* feat(proxy): add Cloudflare edge caching for proxy.worldmonitor.app (#478)
Add CDN-Cache-Control headers to all proxy endpoints so Cloudflare can
cache responses at the edge independently of browser Cache-Control:
- RSS: 600s edge + stale-while-revalidate=300 (browser: 300s)
- UCDP: 3600s edge (matches browser)
- OpenSky: 15s edge (browser: 30s) for fresher flight data
- WorldBank: 1800s/86400s edge (matches browser)
- Polymarket: 120s edge (matches browser)
- Telegram: 10s edge (matches browser)
- AIS snapshot: 2s edge (matches browser)
Also fixes:
- Vary header merging: sendCompressed/sendPreGzipped now merge existing
Vary: Origin instead of overwriting, preventing cross-origin cache
poisoning at the edge
- Stale fallback responses (OpenSky, WorldBank, Polymarket, RSS) now
set Cache-Control: no-store + CDN-Cache-Control: no-store to prevent
edge caching of degraded responses
- All no-cache branches get CDN-Cache-Control: no-store
- /opensky-reset gets no-store (state-changing endpoint)
* fix(sentry): add noise filters for 4 unresolved issues (#479)
- Tighten AbortError filter to match "AbortError: The operation was aborted"
- Filter "The user aborted a request" (normal navigation cancellation)
- Filter UltraViewer service worker injection errors (/uv/service/)
- Filter Huawei WebView __isInQueue__ injection
* feat: configurable VITE_WS_API_URL + harden POST→GET shim (#480)
* fix(gateway): harden POST→GET shim with scalar guard and size limit
- Only convert string/number/boolean values to query params (skip objects,
nested arrays, __proto__ etc.) to prevent prototype pollution vectors
- Skip body parsing for Content-Length > 1MB to avoid memory pressure
* feat: make API base URL configurable via VITE_WS_API_URL
Replace hardcoded api.worldmonitor.app with VITE_WS_API_URL env var.
When empty, installWebApiRedirect() is skipped entirely — relative
/api/* calls stay on the same domain (local installs). When set,
browser fetch is redirected to that URL.
Also adds VITE_WS_API_URL and VITE_WS_RELAY_URL hostnames to
APP_HOSTS allowlist dynamically.
* fix(analytics): use greedy regex in PostHog ingest rewrites (#481)
Vercel's :path* wildcard doesn't match trailing slashes that
PostHog SDK appends (e.g. /ingest/s/?compression=...), causing 404s.
Switch to :path(.*) which matches all path segments including
trailing slashes. Ref: PostHog/posthog#17596
* perf(proxy): increase AIS snapshot edge TTL from 2s to 10s (#482)
With 20k requests/30min (60% of proxy traffic) and per-PoP caching,
a 2s edge TTL expires before the next request from the same PoP arrives,
resulting in near-zero cache hits. 10s allows same-PoP dedup while
keeping browser TTL at 2s for fresh vessel positions.
* fix(markets): commodities panel showing stocks instead of commodities (#483)
The shared circuit breaker (cacheTtlMs: 0) cached the stocks response,
then the stale-while-revalidate path returned that cached stocks data
for the subsequent commodities fetch. Skip SWR when caching is disabled.
* feat(gateway): complete edge cache tier coverage + degraded-response policy (#484)
- Add 11 missing GET routes to RPC_CACHE_TIER map (8 slow, 3 medium)
- Add response-headers side-channel (WeakMap) so handlers can signal
X-No-Cache without codegen changes; wire into military-flights and
positive-geo-events handlers on upstream failure
- Add env-controlled per-endpoint tier override (CACHE_TIER_OVERRIDE_*)
for incident response rollback
- Add VITE_WS_API_URL hostname allowlist (*.worldmonitor.app + localhost)
- Fix fetch.bind(globalThis) in positive-events-geo.ts (deferred lambda)
- Add CI test asserting every generated GET route has an explicit cache
tier entry (prevents silent default-tier drift)
* chore: bump version to 2.5.20 + changelog
Covers PRs #452–#484: Cloudflare edge caching, commodities SWR fix,
security advisories panel, settings redesign, 52 POST→GET migrations.
* fix(rss): remove stale indianewsnetwork.com from proxy allowlist (#486)
Feed has no <pubDate> fields and latest content is from April 2022.
Not referenced in any feed config — only in the proxy domain allowlist.
* feat(i18n): add Korean (한국어) localization (#487)
- Add ko.json with all 1606 translation keys matching en.json structure
- Register 'ko' in SUPPORTED_LANGUAGES, LANGUAGES display array, and locale map
- Korean appears as 🇰🇷 한국어 in the language dropdown
* feat: add Polish tv livestreams (#488)
* feat(rss): add Axios (api.axios.com/feed) as US news source (#494)
Add api.axios.com to proxy allowlist and CSP connect-src, register
Axios feed under US category as Tier 2 mainstream source.
* perf: bootstrap endpoint + polling optimization (#495)
* perf: bootstrap endpoint + polling optimization (phases 3-4)
Replace 15+ individual RPC calls on startup with a single /api/bootstrap
batch call that fetches pre-cached data from Redis. Consolidate 6 panel
setInterval timers into the central RefreshScheduler for hidden-tab
awareness (10x multiplier) and adaptive backoff (up to 4x for unchanged
data). Convert IntelligenceGapBadge from 10s polling to event-driven
updates with 60s safety fallback.
* fix(bootstrap): inline Redis + cache keys in edge function
Vercel Edge Functions cannot resolve cross-directory TypeScript imports
from server/_shared/. Inline getCachedJsonBatch and BOOTSTRAP_CACHE_KEYS
directly in api/bootstrap.js. Add sync test to ensure inlined keys stay
in sync with the canonical server/_shared/cache-keys.ts registry.
* test: add Edge Function module isolation guard for all api/*.js files
Prevents any Edge Function from importing from ../server/ or ../src/
which breaks Vercel builds. Scans all 12 non-helper Edge Functions.
* fix(bootstrap): read unprefixed cache keys on all environments
Preview deploys set VERCEL_ENV=preview which caused getKeyPrefix() to
prefix Redis keys with preview:<sha>:, but handlers only write to
unprefixed keys on production. Bootstrap is a read-only consumer of
production cache — always read unprefixed keys.
* fix(bootstrap): wire sectors hydration + add coverage guard
- Wire getHydratedData('sectors') in data-loader to skip Yahoo Finance
fetch when bootstrap provides sector data
- Add test ensuring every bootstrap key has a getHydratedData consumer
— prevents adding keys without wiring them
* fix(server): resolve 25 TypeScript errors + add server typecheck to CI
- _shared.ts: remove unused `delay` variable
- list-etf-flows.ts: add missing `rateLimited` field to 3 return literals
- list-market-quotes.ts: add missing `rateLimited` field to 4 return literals
- get-cable-health.ts: add non-null assertions for regex groups and array access
- list-positive-geo-events.ts: add non-null assertion for array index
- get-chokepoint-status.ts: add required fields to request objects
- CI: run `typecheck:api` (tsconfig.api.json) alongside `typecheck` to catch
server/ TS errors before merge
* feat(military): server-side military bases 125K + rate limiting (#496)
* feat(military): server-side military bases with 125K entries + rate limiting (#485)
Migrate military bases from 224 static client-side entries to 125,380
server-side entries stored in Redis GEO sorted sets, served via
bbox-filtered GEOSEARCH endpoint with server-side clustering.
Data pipeline:
- Pizzint/Polyglobe: 79,156 entries (Supabase extraction)
- OpenStreetMap: 45,185 entries
- MIRTA: 821 entries
- Curated strategic: 218 entries
- 277 proximity duplicates removed
Server:
- ListMilitaryBases RPC with GEOSEARCH + HMGET + tier/filter/clustering
- Antimeridian handling (split bbox queries)
- Blue-green Redis deployment with atomic version pointer switch
- geoSearchByBox() + getHashFieldsBatch() helpers in redis.ts
Security:
- @upstash/ratelimit: 60 req/min sliding window per IP
- IP spoofing fix: prioritize x-real-ip (Vercel-injected) over x-forwarded-for
- Require API key for non-browser requests (blocks unauthenticated curl/scripts)
- Input validation: allowlisted types/kinds, regex country, clamped bbox/zoom
Frontend:
- Viewport-driven loading with bbox quantization + debounce
- Server-side grid clustering at low zoom levels
- Enriched popup with kind, category badges (airforce/naval/nuclear/space)
- Static 224 bases kept as search fallback + initial render
* fix(military): fallback to production Redis keys in preview deployments
Preview deployments prefix Redis keys with `preview:{sha}:` but military
bases data is seeded to unprefixed (production) keys. When the prefixed
`military:bases:active` key is missing, fall back to the unprefixed key
and use raw (unprefixed) keys for geo/meta lookups.
* fix: remove unused 'remaining' destructure in rate-limit (TS6133)
* ci: add typecheck:api to pre-push hook to catch server-side TS errors
* debug(military): add X-Bases-Debug response header for preview diagnostics
* fix(bases): trigger initial server fetch on map load
fetchServerBases() was only called on moveend — if the user
never panned/zoomed, the API was never called and only the 224
static fallback bases showed.
* perf(military): debounce base fetches + upgrade edge cache to static tier (#497)
- Add 300ms debounce on moveend to prevent rapid pan flooding
- Fixes stale-bbox bug where pendingFetch returns old viewport data
- Upgrade edge cache tier from medium (5min) to static (1hr) — bases are
static infrastructure, aligned with server-side cachedFetchJson TTL
- Keep error logging in catch blocks for production diagnostics
* fix(cyber): make GeoIP centroid fallback jitter deterministic (#498)
Replace Math.random() jitter with DJB2 hash seeded by the threat
indicator (IP/URL), so the same threat always maps to the same
coordinates across requests while different threats from the same
country still spread out.
Closes #203
Co-authored-by: Chris Chen <fuleinist@users.noreply.github.com>
* fix: use cross-env for Windows-compatible npm scripts (#499)
Replace direct `VAR=value command` syntax with cross-env/cross-env-shell
so dev, build, test, and desktop scripts work on Windows PowerShell/CMD.
Co-authored-by: facusturla <facusturla@users.noreply.github.com>
* feat(live-news): add CBC News to optional North America channels (#502)
YouTube handle @CBCNews with fallback video ID 5vfaDsMhCF4.
* fix(bootstrap): harden hydration cache + polling review fixes (#504)
- Filter null/undefined values before storing in hydration cache to
prevent future consumers using !== undefined from misinterpreting
null as valid data
- Debounce wm:intelligence-updated event handler via requestAnimationFrame
to coalesce rapid alert generation into a single render pass
- Include alert IDs in StrategicRiskPanel change fingerprint so content
changes are detected even when alert count stays the same
- Replace JSON.stringify change detection in ServiceStatusPanel with
lightweight name:status fingerprint
- Document max effective refresh interval (40x base) in scheduler
* fix(geo): tokenization-based keyword matching to prevent false positives (#503)
* fix(geo): tokenization-based keyword matching to prevent false positives
Replace String.includes() with tokenization-based Set.has() matching
across the geo-tagging pipeline. Prevents false positives like "assad"
matching inside "ambassador" and "hts" matching inside "rights".
- Add src/utils/keyword-match.ts as single source of truth
- Decompose possessives/hyphens ("Assad's" → includes "assad")
- Support multi-word phrase matching ("white house" as contiguous)
- Remove false-positive-prone DC keywords ('house', 'us ')
- Update 9 consumer files across geo-hub, map, CII, and asset systems
- Add 44 tests covering false positives, true positives, edge cases
Co-authored-by: karim <mirakijka@gmail.com>
Fixes #324
* fix(geo): add inflection suffix matching + fix test imports
Address code review feedback:
P1a: Add suffix-aware matching for plurals and demonyms so existing
keyword lists don't regress (houthi→houthis, ukraine→ukrainian,
iran→iranian, israel→israeli, russia→russian, taiwan→taiwanese).
Uses curated suffix list + e-dropping rule to avoid false positives.
P1b: Expand conflictTopics arrays in DeckGLMap and Map with demonym
forms so "Iranian senate..." correctly registers as conflict topic.
P2: Replace inline test functions with real module import via tsx.
Tests now exercise the production keyword-match.ts directly.
* fix: wire geo-keyword tests into test:data command
The .mts test file wasn't covered by `node --test tests/*.test.mjs`.
Add `npx tsx --test tests/*.test.mts` so test:data runs both suites.
* fix: cross-platform test:data + pin tsx in devDependencies
- Use tsx as test runner for both .mjs and .mts (single invocation)
- Removes ; separator which breaks on Windows cmd.exe
- Add tsx to devDependencies so it works in offline/CI environments
* fix(geo): multi-word demonym matching + short-keyword suffix guard
- Add wordMatches() for suffix-aware phrase matching so "South Korean"
matches keyword "south korea" and "North Korean" matches "north korea"
- Add MIN_SUFFIX_KEYWORD_LEN=4 guard so short keywords like "ai", "us",
"hts" only do exact-match (prevents "ais"→"ai", "uses"→"us" false positives)
- Add 5 new tests covering both fixes (58 total, all passing)
* fix(geo): support plural demonyms in keyword matching
Add compound suffixes (ians, eans, ans, ns, is) to handle plural
demonym forms like "Iranians"→"iran", "Ukrainians"→"ukraine",
"Russians"→"russia", "Israelis"→"israel". Adds 5 new tests (63 total).
---------
Co-authored-by: karim <mirakijka@gmail.com>
* chore: strip 61 debug console.log calls from 20 service files (#501)
* chore: strip 61 debug console.log calls from services
Remove development/tracing console.log statements from 20 files.
These add noise to production browser consoles and increase bundle size.
Preserved: all console.error (error handling) and console.warn (warnings).
Preserved: debug-gated logs in runtime.ts (controlled by verbose flag).
Removed: debugInjectTestEvents() from geo-convergence.ts (test-only code).
Removed: logSummary()/logReport() methods that were pure console.log wrappers.
* fix: remove orphaned stubs and remaining debug logs from stripped services
- Remove empty logReport() method and unused startTime variable (parallel-analysis.ts)
- Remove orphaned console.group/console.groupEnd pair (parallel-analysis.ts)
- Remove empty logSignalSummary() export (signal-aggregator.ts)
- Remove logSignalSummary import/call and 3 remaining console.logs (InsightsPanel.ts)
- Remove no-op logDirectFetchBlockedOnce() and dead infrastructure (prediction/index.ts)
* fix: generalize Vercel preview origin regex + include filters in bases cache key (#506)
- api/_api-key.js: preview URL pattern was user-specific (-elie-),
rejecting other collaborators' Vercel preview deployments.
Generalized to match any worldmonitor-*.vercel.app origin.
- military-bases.ts: client cache key only checked bbox/zoom, ignoring
type/kind/country filters. Switching filters without panning returned
stale results. Unified into single cacheKey string.
* fix(prediction): filter stale/expired markets from Polymarket panel (#507)
Prediction panel was showing expired markets (e.g. "Will US strike Iran
on Feb 9" at 0%). Root causes: no active/archived API filters, no
end_date_min param, no client-side expiry guard, and sub-market selection
picking highest volume before filtering expired ones.
- Add active=true, archived=false, end_date_min API params to all 3
Gamma API call sites (events, markets, probe)
- Pre-filter sub-markets by closed/expired BEFORE volume selection in
both fetchPredictions() and fetchCountryMarkets()
- Add defense-in-depth isExpired() client-side filter on final results
- Propagate endDate through all market object paths including sebuf
fallback
- Show expiry date in PredictionPanel UI with new .prediction-meta
layout
- Add "closes" i18n key to all 18 locale files
- Add endDate to server handler GammaMarket/GammaEvent interfaces and
map to proto closesAt field
* fix(relay): guard proxy handlers against ERR_HTTP_HEADERS_SENT crash (#509)
Polymarket and World Bank proxy handlers had unguarded res.writeHead()
calls in error/timeout callbacks that race with the response callback.
When upstream partially responds then times out, both paths write
headers → process crash. Replace 5 raw writeHead+end calls with
safeEnd() which checks res.headersSent before writing.
* feat(breaking-news): add active alert banner with audio for critical/high RSS items (#508)
RSS items classified as critical/high threat now trigger a full-width
breaking news banner with audio alert, auto-dismiss (60s/30s by severity),
visibility-aware timer pause, dedup, and a toggle in the Intelligence
Findings dropdown.
* fix(sentry): filter Android OEM WebView bridge injection errors (#510)
Add ignoreErrors pattern for LIDNotifyId, onWebViewAppeared, and
onGetWiFiBSSID — native bridge functions injected by Lenovo/Huawei
device SDKs into Chrome Mobile WebView. No stack frames in our code.
* chore: add validated telegram channels list (global + ME + Iran + cyber) (#249)
* feat(conflict): add Iran Attacks map layer + strip debug logs (#511)
* chore: strip 61 debug console.log calls from services
Remove development/tracing console.log statements from 20 files.
These add noise to production browser consoles and increase bundle size.
Preserved: all console.error (error handling) and console.warn (warnings).
Preserved: debug-gated logs in runtime.ts (controlled by verbose flag).
Removed: debugInjectTestEvents() from geo-convergence.ts (test-only code).
Removed: logSummary()/logReport() methods that were pure console.log wrappers.
* fix: remove orphaned stubs and remaining debug logs from stripped services
- Remove empty logReport() method and unused startTime variable (parallel-analysis.ts)
- Remove orphaned console.group/console.groupEnd pair (parallel-analysis.ts)
- Remove empty logSignalSummary() export (signal-aggregator.ts)
- Remove logSignalSummary import/call and 3 remaining console.logs (InsightsPanel.ts)
- Remove no-op logDirectFetchBlockedOnce() and dead infrastructure (prediction/index.ts)
* feat(conflict): add Iran Attacks map layer
Adds a new Iran-focused conflict events layer that aggregates real-time
events, geocodes via 40-city lookup table, caches 15min in Redis, and
renders as a toggleable DeckGL ScatterplotLayer with severity coloring.
- New proto + codegen for ListIranEvents RPC
- Server handler with HTML parsing, city geocoding, category mapping
- Frontend service with circuit breaker
- DeckGL ScatterplotLayer with severity-based color/size
- MapPopup with sanitized source links
- iranAttacks toggle across all variants, harnesses, and URL state
* fix: resolve bootstrap 401 and 429 rate limiting on page init (#512)
Same-origin browser requests don't send Origin header (per CORS spec),
causing validateApiKey to reject them. Extract origin from Referer as
fallback. Increase rate limit from 60 to 200 req/min to accommodate
the ~50 requests fired during page initialization.
* fix(relay): prevent Polymarket OOM via request deduplication (#513)
Concurrent Polymarket requests for the same cache key each fired
independent https.get() calls. With 12 categories × multiple clients,
740 requests piled up in 10s, all buffering response bodies → 4.1GB
heap → OOM crash on Railway.
Fix: in-flight promise map deduplicates concurrent requests to the
same cache key. 429/error responses are negative-cached for 30s to
prevent retry storms.
* fix(threat-classifier): add military/conflict keyword gaps and news-to-conflict bridge (#514)
Breaking news headlines like "Israel's strike on Iran" were classified as
info level because the keyword classifier lacked standalone conflict phrases.
Additionally, the conflict instability score depended solely on ACLED data
(1-7 day lag) with no bridge from real-time breaking news.
- Add 3 critical + 18 high contextual military/conflict keywords
- Preserve threat classification on semantically merged clusters
- Add news-derived conflict floor when ACLED/HAPI report zero signal
- Upsert news events by cluster ID to prevent duplicates
- Extract newsEventIndex to module-level Map for serialization safety
* fix(breaking-news): let critical alerts bypass global cooldown and replace HIGH alerts (#516)
Global cooldown (60s) was blocking critical alerts when a less important
HIGH alert fired from an earlier RSS batch. Added priority-aware cooldown
so critical alerts always break through. Banner now auto-dismisses HIGH
alerts when a CRITICAL arrives. Added Iran/strikes keywords to classifier.
* fix(rate-limit): increase sliding window to 300 req/min (#515)
App init fires many concurrent classify-event, summarize-article, and
record-baseline-snapshot calls, exhausting the 200/min limit and causing
429s. Bump to 300 as a temporary measure while client-side batching is
implemented.
* fix(breaking-news): fix fake pubDate fallback and filter noisy think-tank alerts (#517)
Two bugs causing stale CrisisWatch article to fire as breaking alert:
1. Non-standard pubDate format ("Friday, February 27, 2026 - 12:38")
failed to parse → fallback was `new Date()` (NOW) → day-old articles
appeared as "just now" and passed recency gate on every fetch
2. Tier 3+ sources (think tanks) firing alerts on keyword-only matches
like "War" in policy analysis titles — too noisy for breaking alerts
Fix: parsePubDate() handles non-standard formats and falls back to
epoch (not now). Tier 3+ sources require LLM classification to fire.
* fix: make iran-events handler read-only from Redis (#518)
Remove server-side LiveUAMap scraper (blocked by Cloudflare 403 on
Vercel IPs). Handler now reads pre-populated Redis cache pushed from
local browser scraping. Change cache tier from slow to fast to prevent
CDN from serving stale empty responses for 30+ minutes.
* fix(relay): Polymarket circuit breaker + concurrency limiter (OOM fix) (#519)
* fix(rate-limit): increase sliding window to 300 req/min
App init fires many concurrent classify-event, summarize-article, and
record-baseline-snapshot calls, exhausting the 200/min limit and causing
429s. Bump to 300 as a temporary measure while client-side batching is
implemented.
* fix(relay): add Polymarket circuit breaker + concurrency limiter to prevent OOM
Railway relay OOM crash: 280 Polymarket 429 errors in 8s, heap hit 3.7GB.
Multiple unique cache keys bypassed per-key dedup, flooding upstream.
- Circuit breaker: trips after 5 consecutive failures, 60s cooldown
- Concurrent upstream limiter: max 3 simultaneous requests
- Negative cache TTL: 30s → 60s to reduce retry frequency
- Upstream slot freed on response.on('end'), not headers, preventing
body buffer accumulation past the concurrency cap
* fix(relay): guard against double-finalization on Polymarket timeout
request.destroy() in timeout handler also fires request.on('error'),
causing double decrement of polymarketActiveUpstream (counter goes
negative, disabling concurrency cap) and double circuit breaker trip.
Add finalized guard so decrement + failure accounting happens exactly
once per request regardless of which error path fires first.
* fix(threat-classifier): stagger AI classification requests to avoid Groq 429 (#520)
flushBatch() fired up to 20 classifyEvent RPCs simultaneously via
Promise.all, instantly hitting Groq's ~30 req/min rate limit.
- Sequential execution with 2s min-gap between requests (~28 req/min)
- waitForGap() enforces hard floor + jitter across batch boundaries
- batchInFlight guard prevents concurrent flush loops
- 429/5xx: requeue failed job (with retry cap) + remaining untouched jobs
- Queue cap at 100 items with warn on overflow
* fix(relay): regenerate package-lock.json with telegram dependency
The lockfile was missing resolved entries for the telegram package,
causing Railway to skip installation despite it being in package.json.
* chore: trigger deploy to flush CDN cache for iran-events endpoint
* Revert "fix(relay): regenerate package-lock.json with telegram dependency"
This reverts commit a8d5e1dbbd3300708a081d31783d93e793a072c0.
* fix(relay): add POLYMARKET_ENABLED env flag kill switch (#523)
Set POLYMARKET_ENABLED=false on Railway to disable all Polymarket
upstream requests. Returns 503 immediately, preventing OOM crashes.
* fix(breaking-news): fill keyword gaps missing real Iran attack headlines (#521)
* fix(breaking-news): fill keyword gaps that miss real Iran attack headlines
Three root causes for zero alerts during the Iran war:
1. Keyword gaps — top Iran headlines failed classification:
- "US and Israel attack Iran" → info (no "attack iran" keyword)
- "attacked Iran" → info (only "attacks iran" existed, plural)
- "Explosions heard in Tehran" → info (no "explosions" keyword)
Added: attack iran, attacked iran, attack on iran, attack against iran,
bombing/bombed iran, war against iran (CRITICAL); explosions,
launched/launches attacks, retaliatory/preemptive/preventive attack (HIGH)
2. 5-item RSS limit — Al Jazeera's CRITICAL "major combat operations"
headline was item #7 and never reached the classifier. Increased
per-feed limit from 5 to 10.
3. False positive — "OpenAI strikes deal with Pentagon" matched HIGH
keyword "strikes". Added "strikes deal/agreement/partnership" to
exclusions.
* fix(threat-classifier): prevent Iran compound keyword false positives
"attack iran" as plain substring matched "Iran-backed" and "Iranian"
in headlines about proxy groups, not direct attacks on Iran.
Added TRAILING_BOUNDARY_KEYWORDS set with negative lookahead (?![\w-])
for all Iran compound keywords. This rejects "Iran-backed militias"
and "Iranian targets" while still matching "attack Iran:" and
"attack Iran" at end of string.
Addresses Codex review comment on PR #521.
* fix(relay): regenerate package-lock.json with telegram dependency (#522)
The lockfile was missing resolved entries for the telegram package,
causing Railway to skip installation despite it being in package.json.
* fix(iran): bypass stale CDN cache for iran-events endpoint (#524)
The CDN cached empty {events:[],scrapedAt:0} from the pre-Redis
deployment and Vercel deploy didn't purge all edge nodes. Add ?_v=2
query param to force cache miss until CDN naturally expires the
stale slow-tier entry.
* fix(focal-points): attribute theater military activity to target nations (#525)
The signal aggregator attributed military flights/vessels to the country
they're physically over (point-in-polygon). Aircraft attacking Iran from
the Persian Gulf got attributed to XX/IQ/SA, not IR — so Iran showed
ELEVATED in Focal Points despite being under active attack (CRIT in
Strategic Posture).
Feed theater-level posture data back into the signal aggregator for
target nations (Iran, Taiwan, North Korea, Gaza, Yemen) so they get
credited for military activity in their theater bounding box. Includes
double-count guard to skip if the nation already has signals.
Also fixes stale "sebuf" comment in threat-classifier.
* fix(relay): block rsshub.app requests with 410 Gone (#526)
Stale clients still send RSS requests to rsshub.app (NHK, MOFCOM, MIIT).
These feeds were migrated to Google News RSS but cached PWA clients keep
hitting the relay, which forwards to rsshub.app and gets 403.
- Add explicit blocklist returning 410 Gone before allowlist check
- Remove rsshub.app from all allowlists (relay, edge proxy, vite)
- Remove dead AP News dev proxy target
* feat(map): prioritize Iran Attacks layer (#527)
* feat(map): move Iran Attacks layer to first position and enable by default
Move iranAttacks to the top of the layer toggle list in the full
(geopolitical) variant so it appears first. Enable it by default on
both desktop and mobile during the active conflict.
* feat(map): add Iran Attacks layer support to SVG/mobile map
- Implement setIranEvents() in SVG Map (was no-op)
- Render severity-colored circle markers matching DeckGL layer
- Add iranAttacks to mobile layer toggles (first position)
- Forward setIranEvents to SVG map in MapContainer
- Add IranEventPopupData to PopupData union for click popups
- Add .iran-event-marker CSS with pulse animation
- Add data-layer-hidden-iranAttacks CSS toggle
* fix(geo): expand geo hub index with 60+ missing world locations (#528)
The geo hub index only had ~30 entries, missing all Gulf states (UAE,
Qatar, Bahrain, Kuwait, Oman), Iraq cities, and many world capitals.
News mentioning Abu Dhabi, Dubai, Baghdad, etc. had no lat/lon assigned
so they never appeared on the map.
Added: Gulf capitals (Abu Dhabi, Dubai, Doha, Manama, Kuwait, Muscat),
Iraq (Baghdad, Erbil, Basra), Jordan, Istanbul, Haifa, Dimona, Isfahan,
Kabul, Mumbai, Shanghai, Hong Kong, Singapore, Manila, Jakarta, Bangkok,
Hanoi, Canberra, all major European capitals (Rome, Madrid, Warsaw,
Bucharest, Helsinki, Stockholm, Oslo, Baltics, Athens, Belgrade, Minsk,
Tbilisi, Chisinau, Yerevan, Baku), Americas (Ottawa, Mexico City,
Brasilia, Buenos Aires, Caracas, Bogota, Havana), Africa (Nairobi,
Pretoria, Lagos, Kinshasa, Mogadishu, Tripoli, Tunis, Algiers, Rabat),
conflict zones (Iraq, Kashmir, Golan), chokepoints (Malacca, Panama,
Gibraltar), and US military bases (Ramstein, Incirlik, Diego Garcia,
Guam, Okinawa).
* fix(iran): bust CDN cache to serve updated Gulf-geocoded events (#532)
CDN edge cache was still serving stale 93-event response without
Gulf state coordinates (UAE, Bahrain, Qatar, Kuwait). Bump cache
key from ?_v=2 to ?_v=3 so browsers fetch fresh 100-event data.
Also gitignore internal/ for private tooling scripts.
* fix(alerts): remove SESSION_START gate that blocked pre-existing breaking news (#533)
The isRecent() function used Math.max(now - 15min, SESSION_START) as
recency cutoff. Since SESSION_START = Date.now() at module load, items
published before page load could never trigger alerts — they failed the
SESSION_START gate in the first 15 min, then aged past the 15-min window.
Now uses only the 15-minute recency window. Spam prevention remains via
per-event dedup (30 min), global cooldown (60s), and source tier filter.
* fix(relay): Telegram + OOM + memory cleanup (#531)
* fix(relay): resolve Telegram missing package, OOM crashes, and memory cleanup
- Add `telegram` and `ws` to root dependencies so Railway's `npm install` installs them
- Log V8 heap limit at startup to confirm NODE_OPTIONS is active
- Make MAX_VESSELS/MAX_VESSEL_HISTORY env-configurable (default 20k, down from 50k)
- Add permanent latch to skip Telegram import retries when package is missing
- Raise memory cleanup threshold from 450MB to 2GB (env-configurable)
- Clear all caches (RSS, Polymarket, WorldBank) during emergency cleanup
* fix(relay): treat blank env vars as unset in safeInt
Number('') === 0 passes isFinite, silently clamping caps to 1000
instead of using the 20000 default. Guard empty/null before parsing.
* fix(live-news): replace 7 stale YouTube fallback video IDs (#535)
Validated all 23 YouTube fallbackVideoIds via oEmbed API and all 9
HLS URLs. Found 5 broken IDs (403 embed-restricted or 404 deleted)
plus 2 previously identified stale IDs:
- Fox News: QaftgYkG-ek → ZvdiJUYGBis
- Sky News Arabia: MN50dHFHMKE → U--OjmpjF5o
- RTVE 24H: 7_srED6k0bE → -7GEFgUKilA
- CNN Brasil: 1kWRw-DA6Ns → 6ZkOlaGfxq4
- C5N: NdQSOItOQ5Y → SF06Qy1Ct6Y
- TBS NEWS DIG: ohI356mwBp8 → Anr15FA9OCI
- TRT World: CV5Fooi8WDI → ABfFhWzWs0s
All 9 HLS URLs validated OK. 16 remaining YouTube IDs validated OK.
* fix(relay): fix telegram ESM import path and broaden latch regex
- `import('telegram/sessions')` fails with "Directory import is not
supported resolving ES modules" — use explicit `telegram/sessions/index.js`
- Broaden permanent-disable latch to also catch "Directory import" errors
* fix(ui): move download banner to bottom-right (#536)
* fix(alerts): remove SESSION_START gate that blocked pre-existing breaking news
The isRecent() function used Math.max(now - 15min, SESSION_START) as
recency cutoff. Since SESSION_START = Date.now() at module load, items
published before page load could never trigger alerts — they failed the
SESSION_START gate in the first 15 min, then aged past the 15-min window.
Now uses only the 15-minute recency window. Spam prevention remains via
per-event dedup (30 min), global cooldown (60s), and source tier filter.
* fix(ui): move download banner to bottom-right of screen
Repositioned from top-right (overlapping content) to bottom-right.
Dismissal already persists via localStorage. Added TODO for header
download link.
* Revert "fix(relay): fix telegram ESM import path and broaden latch regex"
This reverts commit 1f2f0175abdc52d4ee841ac271eb0f48000cbddf.
* Revert "Revert "fix(relay): fix telegram ESM import path and broaden latch regex"" (#537)
This reverts commit ad41a2e2d245850fd4b699af2adbe53acca80325.
* feat: add day/night solar terminator overlay to map (#529)
* Trigger redeploy with preview env vars
* Trigger deployment
* chore: trigger redeploy for PR #41
* chore: trigger Vercel redeploy (edge function transient failure)
* chore: retrigger Vercel deploy
* feat: add Nigeria feeds and Greek locale feeds (#271)
- Add 5 Nigeria news sources to Africa section (Premium Times, Vanguard,
Channels TV, Daily Trust, ThisDay)
- Add 5 Greek feeds with lang: 'el' for locale-aware filtering
(Kathimerini, Naftemporiki, in.gr, iefimerida, Proto Thema)
- Add source tiers for all new outlets
- Allowlist 8 new domains in RSS proxy
* fix: enforce military bbox filtering and add behavioral cache tests (#284)
* fix: add request coalescing to Redis cache layer
Concurrent cache misses for the same key now share a single upstream
fetch instead of each triggering redundant API calls. This eliminates
duplicate work within Edge Function invocations under burst traffic.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: reduce AIS polling frequency from 10s to 30s
Vessel positions do not change meaningfully in 10 seconds at sea.
Reduces Railway relay requests by 66% with negligible UX impact.
Stale threshold bumped to 45s to match the new interval.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: quantize military flights bbox cache keys to 1-degree grid
Precise bounding box coordinates caused near-zero cache hit rate since
every map pan/zoom produced a unique key. Snapping to a 1-degree grid
lets nearby viewports share cache entries, dramatically reducing
redundant OpenSky API calls.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: parallelize ETF chart fetches instead of sequential await loop
The loop awaited each ETF chart fetch individually, blocking on every
Yahoo gate delay. Using Promise.allSettled lets all 10 fetches queue
concurrently through the Yahoo gate, cutting wall time from ~12s to ~6s.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add Redis pipeline batch GET to reduce round-trips
Add getCachedJsonBatch() using the Upstash pipeline API to fetch
multiple keys in a single HTTP call. Refactor aircraft details batch
handler from 20 sequential GETs to 1 pipelined request.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* test: add structural tests for Redis caching optimizations
18 tests covering: cachedFetchJson request coalescing (in-flight dedup,
cache-before-fetch ordering, cleanup), getCachedJsonBatch pipeline API,
aircraft batch handler pipeline usage, bbox grid quantization (1-degree
step, expanded fetch bbox), and ETF parallel fetch.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: enforce military bbox contract and add behavioral cache tests
---------
Co-authored-by: Elias El Khoury <efk@anghami.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add User-Agent and Cloudflare 403 detection to all secret validation probes (#296)
Sidecar validation probes were missing User-Agent headers, causing
Cloudflare-fronted APIs (e.g. Wingbits) to return 403 which was
incorrectly treated as an auth rejection. Added CHROME_UA to all 13
probes and isCloudflare403() helper to soft-pass CDN blocks.
* fix: open external links in system browser on Tauri desktop (#297)
Tauri WKWebView/WebView2 traps target="_blank" navigation, so news
links and other external URLs silently fail to open. Added a global
capture-phase click interceptor that routes cross-origin links through
the existing open_url Tauri command, falling back to window.open.
* fix: add Greek flag mapping to language selector (#307)
* fix: add missing country brief i18n keys and export PDF option (#308)
- Add levels, trends, fallback keys to top-level countryBrief in en/el/th/vi
locales (fixes raw key display in intelligence brief and header badge)
- Add Export PDF option to country brief dropdown using scoped print dialog
- Add exportPdf i18n key to all 17 locale files
* feat: add day/night solar terminator overlay to map
Add a real-time day/night overlay layer using deck.gl PolygonLayer that
renders the solar terminator (boundary between day and night zones).
The overlay uses astronomical formulas (Meeus) to compute the subsolar
point and trace the terminator line at 1° resolution.
- New toggleable "Day/Night" layer in all 3 variants (full/tech/finance)
- Theme-aware styling (lighter fill on light theme, darker on dark)
- Auto-refresh every 5 minutes with conditional timer (only runs when
layer is enabled, pauses when render is paused)
- Cached polygon computation to avoid recomputing on every render
- i18n translations for all 17 locales
- Updated documentation with new layer entry
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address review feedback — equinox terminator + locale indentation
- Replace safeTanDecl epsilon clamp with proper equinox handling:
when |tanDecl| < 1e-6, draw terminator as vertical great circle
through the poles (subsolar meridian ±90°) instead of clamping
- Fix JSON indentation in all 17 locale files: dayNight and
tradeRoutes keys were left-aligned instead of matching 8-space
indentation of surrounding keys
---------
Co-authored-by: Elie Habib <elie.habib@gmail.com>
Co-authored-by: Elias El Khoury <efk@anghami.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* fix(relay): auto-reconnect on Telegram AUTH_KEY_DUPLICATED and fix IranIntl handle (#539)
- On AUTH_KEY_DUPLICATED (406), disconnect client and set to null so
next poll cycle reconnects fresh — self-heals after competing client dies
- Fix IranIntl → iranintltv (correct Telegram channel handle)
* fix(live-news): add fallback video ID for LiveNOW from FOX channel (#538)
The livenow-fox channel had no fallbackVideoId, relying solely on
YouTube handle lookup which fails intermittently. Added ZvdiJUYGBis
(confirmed live stream) as fallback.
* fix(iran): bump CDN cache-bust to v4 for fresh event data (#544)
100 new events pushed to Redis covering active Iran-Israel-US
conflict theater including Gulf states (UAE, Bahrain, Qatar,
Kuwait, Jordan). Bump ?_v=3 to ?_v=4 to bypass stale CDN.
* fix(telegram): fix ESM import path in session-auth script (#542)
telegram/sessions → telegram/sessions/index.js (same fix as relay)
* fix(telegram): latch AUTH_KEY_DUPLICATED to stop retry spam (#543)
AUTH_KEY_DUPLICATED is permanent — the session string is invalidated
and no amount of retrying will fix it. Previously the relay retried
every 60s, spamming logs. Now it logs a clear error message with
instructions to regenerate the session and stops retrying.
Renamed telegramImportFailed → telegramPermanentlyDisabled to cover
both import failures and auth failures under one latch.
* fix(live-news): fix broken Europe channel handles + add fallback video IDs (#541)
* fix(live-news): fix broken Europe channel handles + add fallback video IDs
- Fix France 24 English handle: @FRANCE24English (404) → @France24_en
- Fix WELT handle: @WELTNachrichtensender (hijacked to "Movie Glow") → @WELTVideoTV
- Add fallbackVideoId for BBC News, France 24 EN, TRT Haber, NTV Turkey,
CNN TURK, TVP Info, Telewizja Republika (verified via Playwright)
- Update stale fallback IDs for Fox News, RTVE, CNN Brasil, C5N, TBS News,
Sky News Arabia, TRT World
* fix(live-news): update CBS News fallback video ID
* fix(live-news): update Newsmax fallback video ID
* fix(live-news): add NBC News fallback video ID
* fix(live-news): full channel audit — fix 10 broken handles + update 8 stale fallbacks
Broken handles fixed:
- Bloomberg: @Bloomberg (404) → @markets
- WION: @WIONews (wrong channel "Write It Out") → @WION
- CTI News: @CtiTv (404) → @中天新聞CtiNews
- VTC NOW: @VTCNOW (404) → @VTCNowOfficial
- Record News: @recordnewsoficial (404) → @RecordNews
- T13: @T13 (404) → @Teletrece
- Channels TV: @channelstv (404) → @ChannelsTelevision
- KTN News: @KTNNewsKE (404) → @ktnnews_kenya
- eNCA: @enewschannel (404) → @eNCA
- SABC News: @SABCNews (404) → @SABCDigitalNews
Stale fallback video IDs refreshed:
- Sky News, NASA, CBC News, CNN Brasil, C5N, TBS NEWS DIG,
Sky News Arabia, TRT World
* feat(oref): add OREF sirens panel with Hebrew-to-English translation (#545)
Add real-time Israel Home Front Command (OREF) siren alerts panel:
- Edge Function proxy at api/oref-alerts.js
- OrefSirensPanel component with live/history views
- oref-alerts service with 10s polling and update callbacks
- Hebrew→English translation via existing translateText() LLM chain
with 3-layer caching (in-memory Map → server Redis → circuit breaker)
- i18n strings for all 23 locales
- Panel registration, data-loader integration, CSS styles
* fix(relay): use execFileSync for OREF curl to avoid shell injection (#546)
Proxy credentials with special characters (semicolons, dollar signs)
were interpolated into a shell command via execSync. Switch to
execFileSync which passes args directly without shell parsing.
* gave the user freedom to resize panels "fixes issue #426" (#489)
* gave the user freedom to resize panles
* feat(panels): add horizontal resize with col-span persistence
* feat(cii): integrate Iran strike events into CII scoring, country brief & timeline (#547)
Iran had ~100 geolocated strike events but the CII was unaware of them:
conflict score stuck at 70 (ACLED only), no strike chip in Active Signals,
timeline conflict lane empty, intelligence brief silent on strikes.
Changes:
- Add strikes[] to CountryData and ingestStrikesForCII() with geo-lookup
fallback (bounding boxes when GeoJSON not yet loaded)
- Boost CII conflict score with 7-day freshness window
(min(50, count*3 + highSev*5))
- Cache iranEvents in IntelligenceCache, preserve across refresh cycles
- Wire data loading: always load Iran events (not gated by map layer),
ingest into CII, trigger panel refresh
- Add activeStrikes to CountryBriefSignals with geo-lookup counting
- Render strike chip in Active Signals and include in fallback brief
- Feed strike events into 7-day timeline (conflict lane)
- Add structured strikeCount/highSeverityStrikeCount fields to GeoSignal
(replaces fragile regex parsing in focal-point-detector)
- Add active_strike signal type to InsightsPanel focal points
- Add bounding-box fallback to signal aggregator for conflict events
- Add i18n keys for activeStrikes
* fix(alerts): add compound escalation for military action + geopolitical target (#548)
Keyword matching was too rigid — "attacks on iran" didn't match CRITICAL
because only "attack on iran" (singular) existed. Headlines like
"strikes by US and Israel on Iran" also missed because words weren't
adjacent.
Added compound escalation: if a HIGH military/conflict keyword matches
AND the headline mentions a critical geopolitical target (Iran, Russia,
China, Taiwan, NATO, US forces), escalate to CRITICAL. Also added
missing Iran keyword variants (plural forms, "Iran retaliates/strikes").
* feat(conflict): enhance Iran events popup with severity badge and related events (#549)
Rewrite the Iran events popup to follow the established popup pattern
(conflict/protest) with severity-colored header, badge, close button,
stat rows, and source link using CSS classes instead of inline styles.
- Add normalizeSeverity helper (clamps unknown values to 'low')
- Show related events from same location (normalized matching, max 5)
- Add IranEventPopupData to PopupData union (removes unsafe double cast)
- Add iranEvent header CSS with severity border-left colors
- Add i18n keys for en/ar/fr
* feat(telegram): add Telegram Intel panel (#550)
* feat(telegram): add Telegram Intel panel consuming relay feed
- Service layer: fetchTelegramFeed() with 30s cache, types matching relay shape
- Panel component: topic filter tabs, safe DOM rendering via h()+replaceChildren()
- DataLoader + RefreshScheduler pattern (60s interval, hidden-tab aware)
- Handles enabled=false and empty states from relay
- CSS following existing gdelt-intel pattern
- Panel title localized across all 18 locales
* fix(i18n): add components.telegramIntel translations to 10 remaining locales
* feat(live-news): add residential proxy + gzip decompression for YouTube detection (#551)
YouTube blocks Vercel datacenter IPs — returns HTML without videoDetails/isLive
data. Switch from edge runtime to Node.js serverless to enable HTTP CONNECT
tunnel proxy via YOUTUBE_PROXY_URL env var. Add zlib decompression for gzip
responses (YouTube returns empty body without Accept-Encoding header).
Also adds missing fallback video IDs for WELT, KTN News, CNA NewsAsia,
and updates TBS NEWS DIG fallback.
* debug(live-news): add debug param to diagnose proxy env var on Vercel
* fix(live-news): set explicit runtime: 'nodejs' for proxy support
Vercel defaults to edge runtime when not specified. node:http/https/zlib
imports are unavailable in edge — causing FUNCTION_INVOCATION_FAILED.
Remove debug param added in previous commit.
* fix(live-news): lazy-load node modules + proxy fallback to direct fetch
Top-level import of node:http/https/zlib crashes if Vercel bundles
for edge despite runtime: 'nodejs' config. Use dynamic import() to
lazy-load at call time. Also add try/catch around proxy so it falls
back to direct fetch if proxy connection fails.
* feat(aviation): integrate AviationStack API for non-US airport delays (#552)
Replace 100% simulated delay data for international airports with real
flight data from AviationStack API. Add 28 Middle East/conflict-zone
airports (Iran, Iraq, Lebanon, Syria, Yemen, Pakistan, Libya, Sudan).
Key changes:
- AviationStack integration with bounded concurrency (5 parallel),
rotating batch (20 airports/cycle), and 20s deadline
- Redis SETNX lock prevents cross-isolate cache stampede on expiry
- Split FAA/intl caches (both 30min TTL) with isolated error handling
- Fix severity colors (was checking 'GS'/'GDP', now minor/moderate/major/severe)
- Fix tooltip (was obj.airport, now obj.name + obj.iata)
- Add FLIGHT_DELAY_TYPE_CLOSURE for airport/airspace closures
- Add closure i18n key across all 18 locales
- Graceful fallback: no API key → simulation; API failure → simulation
* feat(live-news): move YouTube proxy scraping to Railway relay
Vercel serverless cannot use node:http/https for HTTP CONNECT proxy
tunnels. Move the residential proxy YouTube scraping to the Railway
relay (ais-relay.cjs) which has full Node.js access.
- Add /youtube-live route to relay with proxy + direct fetch fallback
- Add 5-min in-memory cache for channel lookups, 1hr for oembed
- Revert Vercel api/youtube/live.js to edge runtime — now proxies to
Railway first, falls back to direct scrape
* feat(settings): add AVIATIONSTACK_API to desktop settings page (#553)
Register the key in Rust keychain (SUPPORTED_SECRET_KEYS), frontend
RuntimeSecretKey type, feature toggle, and settings UI under
"Tracking & Sensing" category.
* fix(live-news): use correct relay auth header for YouTube proxy (#554)
Edge function was sending X-Relay-Auth header with RELAY_AUTH_TOKEN env
var, but the Railway relay expects x-relay-key header validated against
RELAY_SHARED_SECRET. This mismatch caused the relay to reject requests
from Vercel, falling back to direct YouTube scrape (which fails from
datacenter IPs for many channels).
* fix(live-news): align YouTube edge function with relay auth pattern (#555)
Use same getRelayBaseUrl/getRelayHeaders as other edge functions:
- WS_RELAY_URL env var instead of VITE_WS_API_URL
- RELAY_SHARED_SECRET + RELAY_AUTH_HEADER for flexible auth
- Dual x-relay-key + Authorization headers
* fix(i18n): rename OREF Sirens panel to Israel Sirens (#556)
Remove internal implementation references (OREF, proxy relay, oref.org.il)
from all user-facing strings across 18 locales and panel config.
* fix(live-news): annotate empty catches and sanitize error output (#560)
- Add context comments to empty catch blocks for debuggability
- Replace error.message leak with generic client-safe message
* fix(sentry): add noise filters and fix beforeSend null-filename leak (#561)
- Add 8 new ignoreErrors patterns: signal timeout, premium gate,
hybridExecute/mag/webkit bridge injections, postMessage null,
NotSupportedError, appendChild injection, luma assignment
- Fix LIDNotify regex to match both LIDNotify and LIDNotifyId
- Fix beforeSend: strip null/anonymous filename frames so deck.gl
TypeErrors (28 events, 8 users) are properly suppressed
* feat(cii): wire OREF sirens into CII score & country brief (#559)
* feat(cii): wire OREF sirens into CII score and country brief
Active OREF sirens now boost Israel's CII score through two channels:
- Conflict component: +25 base + min(25, alertCount*5) for active sirens
- Blended score: +15 for active sirens, +5/+10 for 24h history thresholds
Country brief for Israel shows a siren signal chip when alerts are active.
* refactor(cii): extract getOrefBlendBoost helper to DRY scoring paths
* fix(relay): add graceful shutdown + poll concurrency guard for Telegram (#562)
- SIGTERM/SIGINT handler disconnects Telegram client before container dies
- telegramPollInFlight guard prevents overlapping poll cycles
- Mid-poll AUTH_KEY_DUPLICATED now permanently disables (was reconnect loop)
* fix(aviation): query all airports instead of rotating batch (#557)
* fix(aviation): query all airports instead of rotating batch of 20
The rotating batch (20 airports/cycle) caused major airports like DXB
(52% cancellations) to be missed entirely for multiple cache cycles.
With a paid AviationStack plan, query all ~90 non-US airports per
refresh with concurrency 10 and 50s deadline (~9 chunks × 5s = 45s).
* feat(cii): feed airport disruptions into CII and country brief
Major/severe airport delays and closures now boost the CII security
score and appear as signal chips in country briefs. Only major+
severity alerts are ingested to avoid noise from minor delays.
- Add aviationDisruptions to CountryData and ingestAviationForCII()
- Boost security score: closure +20, severe +15, major +10, moderate +5
- Store flight delays in intelligenceCache for country brief access
- Add aviation disruptions chip in country brief signals grid
* fix(relay): replace smart quotes crashing relay on startup (#563)
* fix(relay): replace Unicode smart quotes crashing Node.js CJS parser
* fix(relay): await Telegram disconnect + guard startup poll
* fix(cii): resolve Gulf country strike misattribution via multi-match bbox disambiguation (#564)
Dubai/Doha/Bahrain/Kuwait coordinates matched Iran's bounding box first
due to iteration order. Now collects ALL matching bboxes, disambiguates
via isCoordinateInCountry() geometry, and falls back to smallest-area bbox.
- Add BH, QA, KW, JO, OM to bounds tables (previously missing entirely)
- Extract ME_STRIKE_BOUNDS + resolveCountryFromBounds() into country-geometry.ts
- All 4 consumer files use shared constant (single source of truth)
- Bump CDN cache-bust param for iran-events endpoint
* fix(relay): upstreamWs → upstreamSocket in graceful shutdown (#565)
* fix(relay): install curl in Railway container for OREF polling (#567)
* fix(relay): increase Polymarket cache TTL to 10 minutes (#568)
* fix(relay): increase Polymarket cache TTL to 10 minutes
All requests were MISS with 2-min TTL under concurrent load.
Bump to 10-min cache and 5-min negative cache to reduce upstream pressure.
* fix(relay): normalize Polymarket cache key from canonical params
Raw url.search as cache key meant ?tag=fed&endpoint=events and
?endpoint=events&tag_slug=fed produced different keys for the same
upstream request — defeating both cache and inflight dedup, causing
121 MISS entries in 3 seconds.
Build cache key from parsed canonical params (endpoint + sorted
query string) so all equivalent requests share one cache entry.
* feat(webcams): add Iran tab to live webcams panel (#569)
Add dedicated Iran region tab as the first/default tab with 4 feeds:
Tehran, Middle East overview, Tehran (alt angle), and Jerusalem.
* fix(relay): replace nixpacks.toml with railpack.json for curl (#571)
Railway uses Railpack (not Nixpacks). nixpacks.toml in scripts/ was
silently skipped. Use railpack.json at repo root with deploy.aptPackages
to install curl at runtime for OREF polling.
* fix(webcams): replace duplicate Tehran feed with Tel Aviv, rename Iran tab (#572)
- Remove duplicate iran-tehran2 feed (same channel/video as iran-tehran)
- Remove iran-mideast feed
- Add Tel Aviv feed (-VLcYT5QBrY) to Iran Attacks tab
- Rename tab label from "IRAN" to "IRAN ATTACKS" across all 18 locales
* feat(scripts): add Iran events seed script and latest data (#575)
Add seed-iran-events.mjs for importing Iran conflict events into Redis
(conflict:iran-events:v1). Includes geocoding by location keywords and
category-to-severity mapping. Data file contains 100 events from
2026-02-28.
* fix(relay): add timeouts and logging to Telegram poll loop (#578)
GramJS getEntity/getMessages have no built-in timeout. When the first
channel hangs (FLOOD_WAIT, MTProto stall), telegramPollInFlight stays
true forever, blocking all future polls — zero messages collected, zero
errors logged, frontend shows "No messages available".
- Add 15s per-channel timeout on getEntity + getMessages calls
- Add 3-min overall poll cycle timeout
- Force-clear stuck in-flight flag after 3.5 minutes
- Detect FLOOD_WAIT errors and break loop early
- Log per-cycle summary: channels polled, new msgs, errors, duration
- Track media-only messages separately (no text → not a bug)
- Expose lastError, pollInFlight, pollInFlightSince on /status endpoint
* feat(cii): hook security advisories into CII scoring & country briefs (#579)
Travel advisories (Do Not Travel, Reconsider, Caution) from US, AU, UK,
NZ now act as a floor and boost on CII scores. Do Not Travel guarantees
a minimum score of 60 (elevated), Reconsider floors at 50. Multi-source
corroboration (3+ govts) adds +5 bonus.
Advisory chips appear in country brief signal grid with level-appropriate
styling, and advisory context is passed to AI brief generation.
- Extract target country from advisory titles via embassy feed tags and
country name matching
- Add advisoryMaxLevel/advisoryCount/advisorySources to CII CountryData
- Wire ingestAdvisoriesForCII into data loader pipeline
- Add travelAdvisories/travelAdvisoryMaxLevel to CountryBriefSignals
- Render advisory signal chips in CountryBriefPage
* fix(sentry): guard setView against invalid preset + filter translateNotifyError (#580)
- DeckGLMap.setView(): early-return if VIEW_PRESETS[view] is undefined,
preventing TypeError on 'longitude' when select value is invalid
- Add ignoreErrors pattern for Google Translate widget crash
* feat(relay): bootstrap OREF 24h history on startup (#582)
* fix(relay): improve OREF curl error logging with stderr capture
-s flag silenced curl errors. Add -S to show errors, capture stderr
via stdio pipes, and log curl's actual error message inst…
matthewvecchione1-ops
pushed a commit
to matthewvecchione1-ops/worldmonitor
that referenced
this pull request
Mar 4, 2026
…a73#675) * Add Security Advisories panel with government travel alerts (#460) * feat: add Security Advisories panel with government travel advisory feeds Adds a new panel aggregating travel/security advisories from official government foreign affairs agencies (US State Dept, AU DFAT Smartraveller, UK FCDO, NZ MFAT). Advisories are categorized by severity level (Do Not Travel, Reconsider, Caution, Normal) with filter tabs by source country. Includes summary counts, auto-refresh, and persistent caching via the existing data-freshness system. * chore: update package-lock.json * fix: event delegation, localization, and cleanup for SecurityAdvisories panel P1 fixes: - Use event delegation on this.content (bound once in constructor) instead of direct addEventListener after each innerHTML replacement — prevents memory leaks and stale listener issues on re-render - Use setContent() consistently instead of mixing with this.content.innerHTML - Add securityAdvisories translations to all 16 non-English locale files (panels name, component strings, common.all key) - Revert unrelated package-lock.json version bump P2 fixes: - Deduplicate loadSecurityAdvisories — loadIntelligenceData now calls the shared method instead of inlining duplicate fetch+set logic - Add Accept header to fetch calls for better content negotiation * feat(advisories): add US embassy alerts, CDC, ECDC, and WHO health feeds Adds 21 new advisory RSS feeds: - 13 US Embassy per-country security alerts (TH, AE, DE, UA, MX, IN, PK, CO, PL, BD, IT, DO, MM) - CDC Travel Notices - 5 ECDC feeds (epidemiological, threats, risk assessments, avian flu, publications) - 2 WHO feeds (global news, Africa emergencies) Panel gains a Health filter tab for CDC/ECDC/WHO sources. All new domains added to RSS proxy allowlist. i18n "health" key added across all 17 locales. * feat(cache): add negative-result caching to cachedFetchJson (#466) When upstream APIs return errors (HTTP 403, 429, timeout), fetchers return null. Previously null results were not cached, causing repeated request storms against broken APIs every refresh cycle. Now caches a sentinel value ('__WM_NEG__') with a short 2-minute TTL on null results. Subsequent requests within that window get null immediately without hitting upstream. Thrown errors (transient) skip sentinel caching and retry immediately. Also filters sentinels from getCachedJsonBatch pipeline reads and fixes theater posture coalescing test (expected 2 OpenSky fetches for 2 theater query regions, not 1). * feat: convert 52 API endpoints from POST to GET for edge caching (#468) * feat: convert 52 API endpoints from POST to GET for edge caching Convert all cacheable sebuf RPC endpoints to HTTP GET with query/path parameters, enabling CDN edge caching to reduce costs. Flatten nested request types (TimeRange, PaginationRequest, BoundingBox) into scalar query params. Add path params for resource lookups (GetFredSeries, GetHumanitarianSummary, GetCountryStockIndex, GetCountryIntelBrief, GetAircraftDetails). Rewrite router with hybrid static/dynamic matching for path param support. Kept as POST: SummarizeArticle, ClassifyEvent, RecordBaselineSnapshot, GetAircraftDetailsBatch, RegisterInterest. Generated with sebuf v0.9.0 (protoc-gen-ts-client, protoc-gen-ts-server). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: add rate_limited field to market response protos The rateLimited field was hand-patched into generated files on main but never declared in the proto definitions. Regenerating wiped it out, breaking the build. Now properly defined in both ListEtfFlowsResponse and ListMarketQuotesResponse protos. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: remove accidentally committed .planning files Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> * feat: add Cloudflare edge caching infrastructure for api.worldmonitor.app (#471) Route web production RPC traffic through api.worldmonitor.app via fetch interceptor (installWebApiRedirect). Add default Cache-Control headers (s-maxage=300, stale-while-revalidate=60) on GET 200 responses, with no-store override for real-time endpoints (vessel snapshot). Update CORS to allow GET method. Skip Vercel bot middleware for API subdomain using hostname check (non-spoofable, replacing CF-Ray header approach). Update desktop cloud fallback to route through api.worldmonitor.app. * fix(beta): eagerly load T5-small model when beta mode is enabled BETA_MODE now couples the badge AND model loading — the summarization-beta model starts loading on startup instead of waiting for the first summarization call. * fix: move 5 path-param endpoints to query params for Vercel routing (#472) Vercel's `api/[domain]/v1/[rpc].ts` captures one dynamic segment. Path params like `/get-humanitarian-summary/SA` add an extra segment that has no matching route file, causing 404 on both OPTIONS preflight and direct requests. These endpoints were broken in production. Changes: - Remove `{param}` from 5 service.proto HTTP paths - Add `(sebuf.http.query)` annotations to request message fields - Update generated client/server code to use URLSearchParams - Update OpenAPI specs (YAML + JSON) to declare query params - Add early-return guards in 4 handlers for missing required params - Add happy.worldmonitor.app to runtime.ts redirect hosts Affected endpoints: - GET /api/conflict/v1/get-humanitarian-summary?country_code=SA - GET /api/economic/v1/get-fred-series?series_id=T10Y2Y&limit=120 - GET /api/market/v1/get-country-stock-index?country_code=US - GET /api/intelligence/v1/get-country-intel-brief?country_code=US - GET /api/military/v1/get-aircraft-details?icao24=a12345 * fix(security-advisories): route feeds through RSS proxy to avoid CORS blocks (#473) - Advisory feeds were fetched directly from the browser, hitting CORS on all 21 feeds (US State Dept, AU Smartraveller, US Embassies, ECDC, CDC, WHO). Route through /api/rss-proxy on web, keep proxyUrl for desktop. - Fix double slash in ECDC Avian Influenza URL (323//feed → 323/feed) - Add feeds.news24.com to RSS proxy allowlist (was returning 403) * feat(cache): tiered edge Cache-Control aligned to upstream TTLs (#474) * fix: move 5 path-param endpoints to query params for Vercel routing Vercel's `api/[domain]/v1/[rpc].ts` captures one dynamic segment. Path params like `/get-humanitarian-summary/SA` add an extra segment that has no matching route file, causing 404 on both OPTIONS preflight and direct requests. These endpoints were broken in production. Changes: - Remove `{param}` from 5 service.proto HTTP paths - Add `(sebuf.http.query)` annotations to request message fields - Update generated client/server code to use URLSearchParams - Update OpenAPI specs (YAML + JSON) to declare query params - Add early-return guards in 4 handlers for missing required params - Add happy.worldmonitor.app to runtime.ts redirect hosts Affected endpoints: - GET /api/conflict/v1/get-humanitarian-summary?country_code=SA - GET /api/economic/v1/get-fred-series?series_id=T10Y2Y&limit=120 - GET /api/market/v1/get-country-stock-index?country_code=US - GET /api/intelligence/v1/get-country-intel-brief?country_code=US - GET /api/military/v1/get-aircraft-details?icao24=a12345 * feat(cache): add tiered edge Cache-Control aligned to upstream TTLs Replace flat s-maxage=300 with 5 tiers (fast/medium/slow/static/no-store) mapped per-endpoint to respect upstream Redis TTLs. Adds stale-if-error resilience headers and X-No-Cache plumbing for future degraded responses. X-Cache-Tier debug header gated behind ?_debug query param. * fix(tech): use rss() for CISA feed, drop build from pre-push hook (#475) - CISA Advisories used dead rss.worldmonitor.app domain (404), switch to rss() helper - Remove Vite build from pre-push hook (tsc already catches errors) * fix(desktop): enable click-to-play YouTube embeds + CISA feed fixes (#476) * fix(tech): use rss() for CISA feed, drop build from pre-push hook - CISA Advisories used dead rss.worldmonitor.app domain (404), switch to rss() helper - Remove Vite build from pre-push hook (tsc already catches errors) * fix(desktop): enable click-to-play for YouTube embeds in WKWebView WKWebView blocks programmatic autoplay in cross-origin iframes regardless of allow attributes, Permissions-Policy, mute-first retries, or secure context. Documented all 10 approaches tested in docs/internal/. Changes: - Switch sidecar embed origin from 127.0.0.1 to localhost (secure context) - Add MutationObserver + retry chain as best-effort autoplay attempts - Use postMessage('*') to fix tauri://localhost cross-origin messaging - Make sidecar play overlay non-interactive (pointer-events:none) - Fix .webcam-iframe pointer-events:none blocking clicks in grid view - Add expand button to grid cells for switching to single view on desktop - Add http://localhost:* to CSP frame-src in index.html and tauri.conf.json * fix(gateway): convert stale POST requests to GET for backwards compat (#477) Stale cached client bundles still send POST to endpoints converted to GET in PR #468, causing 404s. The gateway now parses the POST JSON body into query params and retries the match as GET. * feat(proxy): add Cloudflare edge caching for proxy.worldmonitor.app (#478) Add CDN-Cache-Control headers to all proxy endpoints so Cloudflare can cache responses at the edge independently of browser Cache-Control: - RSS: 600s edge + stale-while-revalidate=300 (browser: 300s) - UCDP: 3600s edge (matches browser) - OpenSky: 15s edge (browser: 30s) for fresher flight data - WorldBank: 1800s/86400s edge (matches browser) - Polymarket: 120s edge (matches browser) - Telegram: 10s edge (matches browser) - AIS snapshot: 2s edge (matches browser) Also fixes: - Vary header merging: sendCompressed/sendPreGzipped now merge existing Vary: Origin instead of overwriting, preventing cross-origin cache poisoning at the edge - Stale fallback responses (OpenSky, WorldBank, Polymarket, RSS) now set Cache-Control: no-store + CDN-Cache-Control: no-store to prevent edge caching of degraded responses - All no-cache branches get CDN-Cache-Control: no-store - /opensky-reset gets no-store (state-changing endpoint) * fix(sentry): add noise filters for 4 unresolved issues (#479) - Tighten AbortError filter to match "AbortError: The operation was aborted" - Filter "The user aborted a request" (normal navigation cancellation) - Filter UltraViewer service worker injection errors (/uv/service/) - Filter Huawei WebView __isInQueue__ injection * feat: configurable VITE_WS_API_URL + harden POST→GET shim (#480) * fix(gateway): harden POST→GET shim with scalar guard and size limit - Only convert string/number/boolean values to query params (skip objects, nested arrays, __proto__ etc.) to prevent prototype pollution vectors - Skip body parsing for Content-Length > 1MB to avoid memory pressure * feat: make API base URL configurable via VITE_WS_API_URL Replace hardcoded api.worldmonitor.app with VITE_WS_API_URL env var. When empty, installWebApiRedirect() is skipped entirely — relative /api/* calls stay on the same domain (local installs). When set, browser fetch is redirected to that URL. Also adds VITE_WS_API_URL and VITE_WS_RELAY_URL hostnames to APP_HOSTS allowlist dynamically. * fix(analytics): use greedy regex in PostHog ingest rewrites (#481) Vercel's :path* wildcard doesn't match trailing slashes that PostHog SDK appends (e.g. /ingest/s/?compression=...), causing 404s. Switch to :path(.*) which matches all path segments including trailing slashes. Ref: PostHog/posthog#17596 * perf(proxy): increase AIS snapshot edge TTL from 2s to 10s (#482) With 20k requests/30min (60% of proxy traffic) and per-PoP caching, a 2s edge TTL expires before the next request from the same PoP arrives, resulting in near-zero cache hits. 10s allows same-PoP dedup while keeping browser TTL at 2s for fresh vessel positions. * fix(markets): commodities panel showing stocks instead of commodities (#483) The shared circuit breaker (cacheTtlMs: 0) cached the stocks response, then the stale-while-revalidate path returned that cached stocks data for the subsequent commodities fetch. Skip SWR when caching is disabled. * feat(gateway): complete edge cache tier coverage + degraded-response policy (#484) - Add 11 missing GET routes to RPC_CACHE_TIER map (8 slow, 3 medium) - Add response-headers side-channel (WeakMap) so handlers can signal X-No-Cache without codegen changes; wire into military-flights and positive-geo-events handlers on upstream failure - Add env-controlled per-endpoint tier override (CACHE_TIER_OVERRIDE_*) for incident response rollback - Add VITE_WS_API_URL hostname allowlist (*.worldmonitor.app + localhost) - Fix fetch.bind(globalThis) in positive-events-geo.ts (deferred lambda) - Add CI test asserting every generated GET route has an explicit cache tier entry (prevents silent default-tier drift) * chore: bump version to 2.5.20 + changelog Covers PRs #452–#484: Cloudflare edge caching, commodities SWR fix, security advisories panel, settings redesign, 52 POST→GET migrations. * fix(rss): remove stale indianewsnetwork.com from proxy allowlist (#486) Feed has no <pubDate> fields and latest content is from April 2022. Not referenced in any feed config — only in the proxy domain allowlist. * feat(i18n): add Korean (한국어) localization (#487) - Add ko.json with all 1606 translation keys matching en.json structure - Register 'ko' in SUPPORTED_LANGUAGES, LANGUAGES display array, and locale map - Korean appears as 🇰🇷 한국어 in the language dropdown * feat: add Polish tv livestreams (#488) * feat(rss): add Axios (api.axios.com/feed) as US news source (#494) Add api.axios.com to proxy allowlist and CSP connect-src, register Axios feed under US category as Tier 2 mainstream source. * perf: bootstrap endpoint + polling optimization (#495) * perf: bootstrap endpoint + polling optimization (phases 3-4) Replace 15+ individual RPC calls on startup with a single /api/bootstrap batch call that fetches pre-cached data from Redis. Consolidate 6 panel setInterval timers into the central RefreshScheduler for hidden-tab awareness (10x multiplier) and adaptive backoff (up to 4x for unchanged data). Convert IntelligenceGapBadge from 10s polling to event-driven updates with 60s safety fallback. * fix(bootstrap): inline Redis + cache keys in edge function Vercel Edge Functions cannot resolve cross-directory TypeScript imports from server/_shared/. Inline getCachedJsonBatch and BOOTSTRAP_CACHE_KEYS directly in api/bootstrap.js. Add sync test to ensure inlined keys stay in sync with the canonical server/_shared/cache-keys.ts registry. * test: add Edge Function module isolation guard for all api/*.js files Prevents any Edge Function from importing from ../server/ or ../src/ which breaks Vercel builds. Scans all 12 non-helper Edge Functions. * fix(bootstrap): read unprefixed cache keys on all environments Preview deploys set VERCEL_ENV=preview which caused getKeyPrefix() to prefix Redis keys with preview:<sha>:, but handlers only write to unprefixed keys on production. Bootstrap is a read-only consumer of production cache — always read unprefixed keys. * fix(bootstrap): wire sectors hydration + add coverage guard - Wire getHydratedData('sectors') in data-loader to skip Yahoo Finance fetch when bootstrap provides sector data - Add test ensuring every bootstrap key has a getHydratedData consumer — prevents adding keys without wiring them * fix(server): resolve 25 TypeScript errors + add server typecheck to CI - _shared.ts: remove unused `delay` variable - list-etf-flows.ts: add missing `rateLimited` field to 3 return literals - list-market-quotes.ts: add missing `rateLimited` field to 4 return literals - get-cable-health.ts: add non-null assertions for regex groups and array access - list-positive-geo-events.ts: add non-null assertion for array index - get-chokepoint-status.ts: add required fields to request objects - CI: run `typecheck:api` (tsconfig.api.json) alongside `typecheck` to catch server/ TS errors before merge * feat(military): server-side military bases 125K + rate limiting (#496) * feat(military): server-side military bases with 125K entries + rate limiting (#485) Migrate military bases from 224 static client-side entries to 125,380 server-side entries stored in Redis GEO sorted sets, served via bbox-filtered GEOSEARCH endpoint with server-side clustering. Data pipeline: - Pizzint/Polyglobe: 79,156 entries (Supabase extraction) - OpenStreetMap: 45,185 entries - MIRTA: 821 entries - Curated strategic: 218 entries - 277 proximity duplicates removed Server: - ListMilitaryBases RPC with GEOSEARCH + HMGET + tier/filter/clustering - Antimeridian handling (split bbox queries) - Blue-green Redis deployment with atomic version pointer switch - geoSearchByBox() + getHashFieldsBatch() helpers in redis.ts Security: - @upstash/ratelimit: 60 req/min sliding window per IP - IP spoofing fix: prioritize x-real-ip (Vercel-injected) over x-forwarded-for - Require API key for non-browser requests (blocks unauthenticated curl/scripts) - Input validation: allowlisted types/kinds, regex country, clamped bbox/zoom Frontend: - Viewport-driven loading with bbox quantization + debounce - Server-side grid clustering at low zoom levels - Enriched popup with kind, category badges (airforce/naval/nuclear/space) - Static 224 bases kept as search fallback + initial render * fix(military): fallback to production Redis keys in preview deployments Preview deployments prefix Redis keys with `preview:{sha}:` but military bases data is seeded to unprefixed (production) keys. When the prefixed `military:bases:active` key is missing, fall back to the unprefixed key and use raw (unprefixed) keys for geo/meta lookups. * fix: remove unused 'remaining' destructure in rate-limit (TS6133) * ci: add typecheck:api to pre-push hook to catch server-side TS errors * debug(military): add X-Bases-Debug response header for preview diagnostics * fix(bases): trigger initial server fetch on map load fetchServerBases() was only called on moveend — if the user never panned/zoomed, the API was never called and only the 224 static fallback bases showed. * perf(military): debounce base fetches + upgrade edge cache to static tier (#497) - Add 300ms debounce on moveend to prevent rapid pan flooding - Fixes stale-bbox bug where pendingFetch returns old viewport data - Upgrade edge cache tier from medium (5min) to static (1hr) — bases are static infrastructure, aligned with server-side cachedFetchJson TTL - Keep error logging in catch blocks for production diagnostics * fix(cyber): make GeoIP centroid fallback jitter deterministic (#498) Replace Math.random() jitter with DJB2 hash seeded by the threat indicator (IP/URL), so the same threat always maps to the same coordinates across requests while different threats from the same country still spread out. Closes #203 Co-authored-by: Chris Chen <fuleinist@users.noreply.github.com> * fix: use cross-env for Windows-compatible npm scripts (#499) Replace direct `VAR=value command` syntax with cross-env/cross-env-shell so dev, build, test, and desktop scripts work on Windows PowerShell/CMD. Co-authored-by: facusturla <facusturla@users.noreply.github.com> * feat(live-news): add CBC News to optional North America channels (#502) YouTube handle @CBCNews with fallback video ID 5vfaDsMhCF4. * fix(bootstrap): harden hydration cache + polling review fixes (#504) - Filter null/undefined values before storing in hydration cache to prevent future consumers using !== undefined from misinterpreting null as valid data - Debounce wm:intelligence-updated event handler via requestAnimationFrame to coalesce rapid alert generation into a single render pass - Include alert IDs in StrategicRiskPanel change fingerprint so content changes are detected even when alert count stays the same - Replace JSON.stringify change detection in ServiceStatusPanel with lightweight name:status fingerprint - Document max effective refresh interval (40x base) in scheduler * fix(geo): tokenization-based keyword matching to prevent false positives (#503) * fix(geo): tokenization-based keyword matching to prevent false positives Replace String.includes() with tokenization-based Set.has() matching across the geo-tagging pipeline. Prevents false positives like "assad" matching inside "ambassador" and "hts" matching inside "rights". - Add src/utils/keyword-match.ts as single source of truth - Decompose possessives/hyphens ("Assad's" → includes "assad") - Support multi-word phrase matching ("white house" as contiguous) - Remove false-positive-prone DC keywords ('house', 'us ') - Update 9 consumer files across geo-hub, map, CII, and asset systems - Add 44 tests covering false positives, true positives, edge cases Co-authored-by: karim <mirakijka@gmail.com> Fixes #324 * fix(geo): add inflection suffix matching + fix test imports Address code review feedback: P1a: Add suffix-aware matching for plurals and demonyms so existing keyword lists don't regress (houthi→houthis, ukraine→ukrainian, iran→iranian, israel→israeli, russia→russian, taiwan→taiwanese). Uses curated suffix list + e-dropping rule to avoid false positives. P1b: Expand conflictTopics arrays in DeckGLMap and Map with demonym forms so "Iranian senate..." correctly registers as conflict topic. P2: Replace inline test functions with real module import via tsx. Tests now exercise the production keyword-match.ts directly. * fix: wire geo-keyword tests into test:data command The .mts test file wasn't covered by `node --test tests/*.test.mjs`. Add `npx tsx --test tests/*.test.mts` so test:data runs both suites. * fix: cross-platform test:data + pin tsx in devDependencies - Use tsx as test runner for both .mjs and .mts (single invocation) - Removes ; separator which breaks on Windows cmd.exe - Add tsx to devDependencies so it works in offline/CI environments * fix(geo): multi-word demonym matching + short-keyword suffix guard - Add wordMatches() for suffix-aware phrase matching so "South Korean" matches keyword "south korea" and "North Korean" matches "north korea" - Add MIN_SUFFIX_KEYWORD_LEN=4 guard so short keywords like "ai", "us", "hts" only do exact-match (prevents "ais"→"ai", "uses"→"us" false positives) - Add 5 new tests covering both fixes (58 total, all passing) * fix(geo): support plural demonyms in keyword matching Add compound suffixes (ians, eans, ans, ns, is) to handle plural demonym forms like "Iranians"→"iran", "Ukrainians"→"ukraine", "Russians"→"russia", "Israelis"→"israel". Adds 5 new tests (63 total). --------- Co-authored-by: karim <mirakijka@gmail.com> * chore: strip 61 debug console.log calls from 20 service files (#501) * chore: strip 61 debug console.log calls from services Remove development/tracing console.log statements from 20 files. These add noise to production browser consoles and increase bundle size. Preserved: all console.error (error handling) and console.warn (warnings). Preserved: debug-gated logs in runtime.ts (controlled by verbose flag). Removed: debugInjectTestEvents() from geo-convergence.ts (test-only code). Removed: logSummary()/logReport() methods that were pure console.log wrappers. * fix: remove orphaned stubs and remaining debug logs from stripped services - Remove empty logReport() method and unused startTime variable (parallel-analysis.ts) - Remove orphaned console.group/console.groupEnd pair (parallel-analysis.ts) - Remove empty logSignalSummary() export (signal-aggregator.ts) - Remove logSignalSummary import/call and 3 remaining console.logs (InsightsPanel.ts) - Remove no-op logDirectFetchBlockedOnce() and dead infrastructure (prediction/index.ts) * fix: generalize Vercel preview origin regex + include filters in bases cache key (#506) - api/_api-key.js: preview URL pattern was user-specific (-elie-), rejecting other collaborators' Vercel preview deployments. Generalized to match any worldmonitor-*.vercel.app origin. - military-bases.ts: client cache key only checked bbox/zoom, ignoring type/kind/country filters. Switching filters without panning returned stale results. Unified into single cacheKey string. * fix(prediction): filter stale/expired markets from Polymarket panel (#507) Prediction panel was showing expired markets (e.g. "Will US strike Iran on Feb 9" at 0%). Root causes: no active/archived API filters, no end_date_min param, no client-side expiry guard, and sub-market selection picking highest volume before filtering expired ones. - Add active=true, archived=false, end_date_min API params to all 3 Gamma API call sites (events, markets, probe) - Pre-filter sub-markets by closed/expired BEFORE volume selection in both fetchPredictions() and fetchCountryMarkets() - Add defense-in-depth isExpired() client-side filter on final results - Propagate endDate through all market object paths including sebuf fallback - Show expiry date in PredictionPanel UI with new .prediction-meta layout - Add "closes" i18n key to all 18 locale files - Add endDate to server handler GammaMarket/GammaEvent interfaces and map to proto closesAt field * fix(relay): guard proxy handlers against ERR_HTTP_HEADERS_SENT crash (#509) Polymarket and World Bank proxy handlers had unguarded res.writeHead() calls in error/timeout callbacks that race with the response callback. When upstream partially responds then times out, both paths write headers → process crash. Replace 5 raw writeHead+end calls with safeEnd() which checks res.headersSent before writing. * feat(breaking-news): add active alert banner with audio for critical/high RSS items (#508) RSS items classified as critical/high threat now trigger a full-width breaking news banner with audio alert, auto-dismiss (60s/30s by severity), visibility-aware timer pause, dedup, and a toggle in the Intelligence Findings dropdown. * fix(sentry): filter Android OEM WebView bridge injection errors (#510) Add ignoreErrors pattern for LIDNotifyId, onWebViewAppeared, and onGetWiFiBSSID — native bridge functions injected by Lenovo/Huawei device SDKs into Chrome Mobile WebView. No stack frames in our code. * chore: add validated telegram channels list (global + ME + Iran + cyber) (#249) * feat(conflict): add Iran Attacks map layer + strip debug logs (#511) * chore: strip 61 debug console.log calls from services Remove development/tracing console.log statements from 20 files. These add noise to production browser consoles and increase bundle size. Preserved: all console.error (error handling) and console.warn (warnings). Preserved: debug-gated logs in runtime.ts (controlled by verbose flag). Removed: debugInjectTestEvents() from geo-convergence.ts (test-only code). Removed: logSummary()/logReport() methods that were pure console.log wrappers. * fix: remove orphaned stubs and remaining debug logs from stripped services - Remove empty logReport() method and unused startTime variable (parallel-analysis.ts) - Remove orphaned console.group/console.groupEnd pair (parallel-analysis.ts) - Remove empty logSignalSummary() export (signal-aggregator.ts) - Remove logSignalSummary import/call and 3 remaining console.logs (InsightsPanel.ts) - Remove no-op logDirectFetchBlockedOnce() and dead infrastructure (prediction/index.ts) * feat(conflict): add Iran Attacks map layer Adds a new Iran-focused conflict events layer that aggregates real-time events, geocodes via 40-city lookup table, caches 15min in Redis, and renders as a toggleable DeckGL ScatterplotLayer with severity coloring. - New proto + codegen for ListIranEvents RPC - Server handler with HTML parsing, city geocoding, category mapping - Frontend service with circuit breaker - DeckGL ScatterplotLayer with severity-based color/size - MapPopup with sanitized source links - iranAttacks toggle across all variants, harnesses, and URL state * fix: resolve bootstrap 401 and 429 rate limiting on page init (#512) Same-origin browser requests don't send Origin header (per CORS spec), causing validateApiKey to reject them. Extract origin from Referer as fallback. Increase rate limit from 60 to 200 req/min to accommodate the ~50 requests fired during page initialization. * fix(relay): prevent Polymarket OOM via request deduplication (#513) Concurrent Polymarket requests for the same cache key each fired independent https.get() calls. With 12 categories × multiple clients, 740 requests piled up in 10s, all buffering response bodies → 4.1GB heap → OOM crash on Railway. Fix: in-flight promise map deduplicates concurrent requests to the same cache key. 429/error responses are negative-cached for 30s to prevent retry storms. * fix(threat-classifier): add military/conflict keyword gaps and news-to-conflict bridge (#514) Breaking news headlines like "Israel's strike on Iran" were classified as info level because the keyword classifier lacked standalone conflict phrases. Additionally, the conflict instability score depended solely on ACLED data (1-7 day lag) with no bridge from real-time breaking news. - Add 3 critical + 18 high contextual military/conflict keywords - Preserve threat classification on semantically merged clusters - Add news-derived conflict floor when ACLED/HAPI report zero signal - Upsert news events by cluster ID to prevent duplicates - Extract newsEventIndex to module-level Map for serialization safety * fix(breaking-news): let critical alerts bypass global cooldown and replace HIGH alerts (#516) Global cooldown (60s) was blocking critical alerts when a less important HIGH alert fired from an earlier RSS batch. Added priority-aware cooldown so critical alerts always break through. Banner now auto-dismisses HIGH alerts when a CRITICAL arrives. Added Iran/strikes keywords to classifier. * fix(rate-limit): increase sliding window to 300 req/min (#515) App init fires many concurrent classify-event, summarize-article, and record-baseline-snapshot calls, exhausting the 200/min limit and causing 429s. Bump to 300 as a temporary measure while client-side batching is implemented. * fix(breaking-news): fix fake pubDate fallback and filter noisy think-tank alerts (#517) Two bugs causing stale CrisisWatch article to fire as breaking alert: 1. Non-standard pubDate format ("Friday, February 27, 2026 - 12:38") failed to parse → fallback was `new Date()` (NOW) → day-old articles appeared as "just now" and passed recency gate on every fetch 2. Tier 3+ sources (think tanks) firing alerts on keyword-only matches like "War" in policy analysis titles — too noisy for breaking alerts Fix: parsePubDate() handles non-standard formats and falls back to epoch (not now). Tier 3+ sources require LLM classification to fire. * fix: make iran-events handler read-only from Redis (#518) Remove server-side LiveUAMap scraper (blocked by Cloudflare 403 on Vercel IPs). Handler now reads pre-populated Redis cache pushed from local browser scraping. Change cache tier from slow to fast to prevent CDN from serving stale empty responses for 30+ minutes. * fix(relay): Polymarket circuit breaker + concurrency limiter (OOM fix) (#519) * fix(rate-limit): increase sliding window to 300 req/min App init fires many concurrent classify-event, summarize-article, and record-baseline-snapshot calls, exhausting the 200/min limit and causing 429s. Bump to 300 as a temporary measure while client-side batching is implemented. * fix(relay): add Polymarket circuit breaker + concurrency limiter to prevent OOM Railway relay OOM crash: 280 Polymarket 429 errors in 8s, heap hit 3.7GB. Multiple unique cache keys bypassed per-key dedup, flooding upstream. - Circuit breaker: trips after 5 consecutive failures, 60s cooldown - Concurrent upstream limiter: max 3 simultaneous requests - Negative cache TTL: 30s → 60s to reduce retry frequency - Upstream slot freed on response.on('end'), not headers, preventing body buffer accumulation past the concurrency cap * fix(relay): guard against double-finalization on Polymarket timeout request.destroy() in timeout handler also fires request.on('error'), causing double decrement of polymarketActiveUpstream (counter goes negative, disabling concurrency cap) and double circuit breaker trip. Add finalized guard so decrement + failure accounting happens exactly once per request regardless of which error path fires first. * fix(threat-classifier): stagger AI classification requests to avoid Groq 429 (#520) flushBatch() fired up to 20 classifyEvent RPCs simultaneously via Promise.all, instantly hitting Groq's ~30 req/min rate limit. - Sequential execution with 2s min-gap between requests (~28 req/min) - waitForGap() enforces hard floor + jitter across batch boundaries - batchInFlight guard prevents concurrent flush loops - 429/5xx: requeue failed job (with retry cap) + remaining untouched jobs - Queue cap at 100 items with warn on overflow * fix(relay): regenerate package-lock.json with telegram dependency The lockfile was missing resolved entries for the telegram package, causing Railway to skip installation despite it being in package.json. * chore: trigger deploy to flush CDN cache for iran-events endpoint * Revert "fix(relay): regenerate package-lock.json with telegram dependency" This reverts commit a8d5e1dbbd3300708a081d31783d93e793a072c0. * fix(relay): add POLYMARKET_ENABLED env flag kill switch (#523) Set POLYMARKET_ENABLED=false on Railway to disable all Polymarket upstream requests. Returns 503 immediately, preventing OOM crashes. * fix(breaking-news): fill keyword gaps missing real Iran attack headlines (#521) * fix(breaking-news): fill keyword gaps that miss real Iran attack headlines Three root causes for zero alerts during the Iran war: 1. Keyword gaps — top Iran headlines failed classification: - "US and Israel attack Iran" → info (no "attack iran" keyword) - "attacked Iran" → info (only "attacks iran" existed, plural) - "Explosions heard in Tehran" → info (no "explosions" keyword) Added: attack iran, attacked iran, attack on iran, attack against iran, bombing/bombed iran, war against iran (CRITICAL); explosions, launched/launches attacks, retaliatory/preemptive/preventive attack (HIGH) 2. 5-item RSS limit — Al Jazeera's CRITICAL "major combat operations" headline was item #7 and never reached the classifier. Increased per-feed limit from 5 to 10. 3. False positive — "OpenAI strikes deal with Pentagon" matched HIGH keyword "strikes". Added "strikes deal/agreement/partnership" to exclusions. * fix(threat-classifier): prevent Iran compound keyword false positives "attack iran" as plain substring matched "Iran-backed" and "Iranian" in headlines about proxy groups, not direct attacks on Iran. Added TRAILING_BOUNDARY_KEYWORDS set with negative lookahead (?![\w-]) for all Iran compound keywords. This rejects "Iran-backed militias" and "Iranian targets" while still matching "attack Iran:" and "attack Iran" at end of string. Addresses Codex review comment on PR #521. * fix(relay): regenerate package-lock.json with telegram dependency (#522) The lockfile was missing resolved entries for the telegram package, causing Railway to skip installation despite it being in package.json. * fix(iran): bypass stale CDN cache for iran-events endpoint (#524) The CDN cached empty {events:[],scrapedAt:0} from the pre-Redis deployment and Vercel deploy didn't purge all edge nodes. Add ?_v=2 query param to force cache miss until CDN naturally expires the stale slow-tier entry. * fix(focal-points): attribute theater military activity to target nations (#525) The signal aggregator attributed military flights/vessels to the country they're physically over (point-in-polygon). Aircraft attacking Iran from the Persian Gulf got attributed to XX/IQ/SA, not IR — so Iran showed ELEVATED in Focal Points despite being under active attack (CRIT in Strategic Posture). Feed theater-level posture data back into the signal aggregator for target nations (Iran, Taiwan, North Korea, Gaza, Yemen) so they get credited for military activity in their theater bounding box. Includes double-count guard to skip if the nation already has signals. Also fixes stale "sebuf" comment in threat-classifier. * fix(relay): block rsshub.app requests with 410 Gone (#526) Stale clients still send RSS requests to rsshub.app (NHK, MOFCOM, MIIT). These feeds were migrated to Google News RSS but cached PWA clients keep hitting the relay, which forwards to rsshub.app and gets 403. - Add explicit blocklist returning 410 Gone before allowlist check - Remove rsshub.app from all allowlists (relay, edge proxy, vite) - Remove dead AP News dev proxy target * feat(map): prioritize Iran Attacks layer (#527) * feat(map): move Iran Attacks layer to first position and enable by default Move iranAttacks to the top of the layer toggle list in the full (geopolitical) variant so it appears first. Enable it by default on both desktop and mobile during the active conflict. * feat(map): add Iran Attacks layer support to SVG/mobile map - Implement setIranEvents() in SVG Map (was no-op) - Render severity-colored circle markers matching DeckGL layer - Add iranAttacks to mobile layer toggles (first position) - Forward setIranEvents to SVG map in MapContainer - Add IranEventPopupData to PopupData union for click popups - Add .iran-event-marker CSS with pulse animation - Add data-layer-hidden-iranAttacks CSS toggle * fix(geo): expand geo hub index with 60+ missing world locations (#528) The geo hub index only had ~30 entries, missing all Gulf states (UAE, Qatar, Bahrain, Kuwait, Oman), Iraq cities, and many world capitals. News mentioning Abu Dhabi, Dubai, Baghdad, etc. had no lat/lon assigned so they never appeared on the map. Added: Gulf capitals (Abu Dhabi, Dubai, Doha, Manama, Kuwait, Muscat), Iraq (Baghdad, Erbil, Basra), Jordan, Istanbul, Haifa, Dimona, Isfahan, Kabul, Mumbai, Shanghai, Hong Kong, Singapore, Manila, Jakarta, Bangkok, Hanoi, Canberra, all major European capitals (Rome, Madrid, Warsaw, Bucharest, Helsinki, Stockholm, Oslo, Baltics, Athens, Belgrade, Minsk, Tbilisi, Chisinau, Yerevan, Baku), Americas (Ottawa, Mexico City, Brasilia, Buenos Aires, Caracas, Bogota, Havana), Africa (Nairobi, Pretoria, Lagos, Kinshasa, Mogadishu, Tripoli, Tunis, Algiers, Rabat), conflict zones (Iraq, Kashmir, Golan), chokepoints (Malacca, Panama, Gibraltar), and US military bases (Ramstein, Incirlik, Diego Garcia, Guam, Okinawa). * fix(iran): bust CDN cache to serve updated Gulf-geocoded events (#532) CDN edge cache was still serving stale 93-event response without Gulf state coordinates (UAE, Bahrain, Qatar, Kuwait). Bump cache key from ?_v=2 to ?_v=3 so browsers fetch fresh 100-event data. Also gitignore internal/ for private tooling scripts. * fix(alerts): remove SESSION_START gate that blocked pre-existing breaking news (#533) The isRecent() function used Math.max(now - 15min, SESSION_START) as recency cutoff. Since SESSION_START = Date.now() at module load, items published before page load could never trigger alerts — they failed the SESSION_START gate in the first 15 min, then aged past the 15-min window. Now uses only the 15-minute recency window. Spam prevention remains via per-event dedup (30 min), global cooldown (60s), and source tier filter. * fix(relay): Telegram + OOM + memory cleanup (#531) * fix(relay): resolve Telegram missing package, OOM crashes, and memory cleanup - Add `telegram` and `ws` to root dependencies so Railway's `npm install` installs them - Log V8 heap limit at startup to confirm NODE_OPTIONS is active - Make MAX_VESSELS/MAX_VESSEL_HISTORY env-configurable (default 20k, down from 50k) - Add permanent latch to skip Telegram import retries when package is missing - Raise memory cleanup threshold from 450MB to 2GB (env-configurable) - Clear all caches (RSS, Polymarket, WorldBank) during emergency cleanup * fix(relay): treat blank env vars as unset in safeInt Number('') === 0 passes isFinite, silently clamping caps to 1000 instead of using the 20000 default. Guard empty/null before parsing. * fix(live-news): replace 7 stale YouTube fallback video IDs (#535) Validated all 23 YouTube fallbackVideoIds via oEmbed API and all 9 HLS URLs. Found 5 broken IDs (403 embed-restricted or 404 deleted) plus 2 previously identified stale IDs: - Fox News: QaftgYkG-ek → ZvdiJUYGBis - Sky News Arabia: MN50dHFHMKE → U--OjmpjF5o - RTVE 24H: 7_srED6k0bE → -7GEFgUKilA - CNN Brasil: 1kWRw-DA6Ns → 6ZkOlaGfxq4 - C5N: NdQSOItOQ5Y → SF06Qy1Ct6Y - TBS NEWS DIG: ohI356mwBp8 → Anr15FA9OCI - TRT World: CV5Fooi8WDI → ABfFhWzWs0s All 9 HLS URLs validated OK. 16 remaining YouTube IDs validated OK. * fix(relay): fix telegram ESM import path and broaden latch regex - `import('telegram/sessions')` fails with "Directory import is not supported resolving ES modules" — use explicit `telegram/sessions/index.js` - Broaden permanent-disable latch to also catch "Directory import" errors * fix(ui): move download banner to bottom-right (#536) * fix(alerts): remove SESSION_START gate that blocked pre-existing breaking news The isRecent() function used Math.max(now - 15min, SESSION_START) as recency cutoff. Since SESSION_START = Date.now() at module load, items published before page load could never trigger alerts — they failed the SESSION_START gate in the first 15 min, then aged past the 15-min window. Now uses only the 15-minute recency window. Spam prevention remains via per-event dedup (30 min), global cooldown (60s), and source tier filter. * fix(ui): move download banner to bottom-right of screen Repositioned from top-right (overlapping content) to bottom-right. Dismissal already persists via localStorage. Added TODO for header download link. * Revert "fix(relay): fix telegram ESM import path and broaden latch regex" This reverts commit 1f2f0175abdc52d4ee841ac271eb0f48000cbddf. * Revert "Revert "fix(relay): fix telegram ESM import path and broaden latch regex"" (#537) This reverts commit ad41a2e2d245850fd4b699af2adbe53acca80325. * feat: add day/night solar terminator overlay to map (#529) * Trigger redeploy with preview env vars * Trigger deployment * chore: trigger redeploy for PR #41 * chore: trigger Vercel redeploy (edge function transient failure) * chore: retrigger Vercel deploy * feat: add Nigeria feeds and Greek locale feeds (#271) - Add 5 Nigeria news sources to Africa section (Premium Times, Vanguard, Channels TV, Daily Trust, ThisDay) - Add 5 Greek feeds with lang: 'el' for locale-aware filtering (Kathimerini, Naftemporiki, in.gr, iefimerida, Proto Thema) - Add source tiers for all new outlets - Allowlist 8 new domains in RSS proxy * fix: enforce military bbox filtering and add behavioral cache tests (#284) * fix: add request coalescing to Redis cache layer Concurrent cache misses for the same key now share a single upstream fetch instead of each triggering redundant API calls. This eliminates duplicate work within Edge Function invocations under burst traffic. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: reduce AIS polling frequency from 10s to 30s Vessel positions do not change meaningfully in 10 seconds at sea. Reduces Railway relay requests by 66% with negligible UX impact. Stale threshold bumped to 45s to match the new interval. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: quantize military flights bbox cache keys to 1-degree grid Precise bounding box coordinates caused near-zero cache hit rate since every map pan/zoom produced a unique key. Snapping to a 1-degree grid lets nearby viewports share cache entries, dramatically reducing redundant OpenSky API calls. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: parallelize ETF chart fetches instead of sequential await loop The loop awaited each ETF chart fetch individually, blocking on every Yahoo gate delay. Using Promise.allSettled lets all 10 fetches queue concurrently through the Yahoo gate, cutting wall time from ~12s to ~6s. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: add Redis pipeline batch GET to reduce round-trips Add getCachedJsonBatch() using the Upstash pipeline API to fetch multiple keys in a single HTTP call. Refactor aircraft details batch handler from 20 sequential GETs to 1 pipelined request. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * test: add structural tests for Redis caching optimizations 18 tests covering: cachedFetchJson request coalescing (in-flight dedup, cache-before-fetch ordering, cleanup), getCachedJsonBatch pipeline API, aircraft batch handler pipeline usage, bbox grid quantization (1-degree step, expanded fetch bbox), and ETF parallel fetch. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: enforce military bbox contract and add behavioral cache tests --------- Co-authored-by: Elias El Khoury <efk@anghami.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> * fix: add User-Agent and Cloudflare 403 detection to all secret validation probes (#296) Sidecar validation probes were missing User-Agent headers, causing Cloudflare-fronted APIs (e.g. Wingbits) to return 403 which was incorrectly treated as an auth rejection. Added CHROME_UA to all 13 probes and isCloudflare403() helper to soft-pass CDN blocks. * fix: open external links in system browser on Tauri desktop (#297) Tauri WKWebView/WebView2 traps target="_blank" navigation, so news links and other external URLs silently fail to open. Added a global capture-phase click interceptor that routes cross-origin links through the existing open_url Tauri command, falling back to window.open. * fix: add Greek flag mapping to language selector (#307) * fix: add missing country brief i18n keys and export PDF option (#308) - Add levels, trends, fallback keys to top-level countryBrief in en/el/th/vi locales (fixes raw key display in intelligence brief and header badge) - Add Export PDF option to country brief dropdown using scoped print dialog - Add exportPdf i18n key to all 17 locale files * feat: add day/night solar terminator overlay to map Add a real-time day/night overlay layer using deck.gl PolygonLayer that renders the solar terminator (boundary between day and night zones). The overlay uses astronomical formulas (Meeus) to compute the subsolar point and trace the terminator line at 1° resolution. - New toggleable "Day/Night" layer in all 3 variants (full/tech/finance) - Theme-aware styling (lighter fill on light theme, darker on dark) - Auto-refresh every 5 minutes with conditional timer (only runs when layer is enabled, pauses when render is paused) - Cached polygon computation to avoid recomputing on every render - i18n translations for all 17 locales - Updated documentation with new layer entry Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: address review feedback — equinox terminator + locale indentation - Replace safeTanDecl epsilon clamp with proper equinox handling: when |tanDecl| < 1e-6, draw terminator as vertical great circle through the poles (subsolar meridian ±90°) instead of clamping - Fix JSON indentation in all 17 locale files: dayNight and tradeRoutes keys were left-aligned instead of matching 8-space indentation of surrounding keys --------- Co-authored-by: Elie Habib <elie.habib@gmail.com> Co-authored-by: Elias El Khoury <efk@anghami.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> * fix(relay): auto-reconnect on Telegram AUTH_KEY_DUPLICATED and fix IranIntl handle (#539) - On AUTH_KEY_DUPLICATED (406), disconnect client and set to null so next poll cycle reconnects fresh — self-heals after competing client dies - Fix IranIntl → iranintltv (correct Telegram channel handle) * fix(live-news): add fallback video ID for LiveNOW from FOX channel (#538) The livenow-fox channel had no fallbackVideoId, relying solely on YouTube handle lookup which fails intermittently. Added ZvdiJUYGBis (confirmed live stream) as fallback. * fix(iran): bump CDN cache-bust to v4 for fresh event data (#544) 100 new events pushed to Redis covering active Iran-Israel-US conflict theater including Gulf states (UAE, Bahrain, Qatar, Kuwait, Jordan). Bump ?_v=3 to ?_v=4 to bypass stale CDN. * fix(telegram): fix ESM import path in session-auth script (#542) telegram/sessions → telegram/sessions/index.js (same fix as relay) * fix(telegram): latch AUTH_KEY_DUPLICATED to stop retry spam (#543) AUTH_KEY_DUPLICATED is permanent — the session string is invalidated and no amount of retrying will fix it. Previously the relay retried every 60s, spamming logs. Now it logs a clear error message with instructions to regenerate the session and stops retrying. Renamed telegramImportFailed → telegramPermanentlyDisabled to cover both import failures and auth failures under one latch. * fix(live-news): fix broken Europe channel handles + add fallback video IDs (#541) * fix(live-news): fix broken Europe channel handles + add fallback video IDs - Fix France 24 English handle: @FRANCE24English (404) → @France24_en - Fix WELT handle: @WELTNachrichtensender (hijacked to "Movie Glow") → @WELTVideoTV - Add fallbackVideoId for BBC News, France 24 EN, TRT Haber, NTV Turkey, CNN TURK, TVP Info, Telewizja Republika (verified via Playwright) - Update stale fallback IDs for Fox News, RTVE, CNN Brasil, C5N, TBS News, Sky News Arabia, TRT World * fix(live-news): update CBS News fallback video ID * fix(live-news): update Newsmax fallback video ID * fix(live-news): add NBC News fallback video ID * fix(live-news): full channel audit — fix 10 broken handles + update 8 stale fallbacks Broken handles fixed: - Bloomberg: @Bloomberg (404) → @markets - WION: @WIONews (wrong channel "Write It Out") → @WION - CTI News: @CtiTv (404) → @中天新聞CtiNews - VTC NOW: @VTCNOW (404) → @VTCNowOfficial - Record News: @recordnewsoficial (404) → @RecordNews - T13: @T13 (404) → @Teletrece - Channels TV: @channelstv (404) → @ChannelsTelevision - KTN News: @KTNNewsKE (404) → @ktnnews_kenya - eNCA: @enewschannel (404) → @eNCA - SABC News: @SABCNews (404) → @SABCDigitalNews Stale fallback video IDs refreshed: - Sky News, NASA, CBC News, CNN Brasil, C5N, TBS NEWS DIG, Sky News Arabia, TRT World * feat(oref): add OREF sirens panel with Hebrew-to-English translation (#545) Add real-time Israel Home Front Command (OREF) siren alerts panel: - Edge Function proxy at api/oref-alerts.js - OrefSirensPanel component with live/history views - oref-alerts service with 10s polling and update callbacks - Hebrew→English translation via existing translateText() LLM chain with 3-layer caching (in-memory Map → server Redis → circuit breaker) - i18n strings for all 23 locales - Panel registration, data-loader integration, CSS styles * fix(relay): use execFileSync for OREF curl to avoid shell injection (#546) Proxy credentials with special characters (semicolons, dollar signs) were interpolated into a shell command via execSync. Switch to execFileSync which passes args directly without shell parsing. * gave the user freedom to resize panels "fixes issue #426" (#489) * gave the user freedom to resize panles * feat(panels): add horizontal resize with col-span persistence * feat(cii): integrate Iran strike events into CII scoring, country brief & timeline (#547) Iran had ~100 geolocated strike events but the CII was unaware of them: conflict score stuck at 70 (ACLED only), no strike chip in Active Signals, timeline conflict lane empty, intelligence brief silent on strikes. Changes: - Add strikes[] to CountryData and ingestStrikesForCII() with geo-lookup fallback (bounding boxes when GeoJSON not yet loaded) - Boost CII conflict score with 7-day freshness window (min(50, count*3 + highSev*5)) - Cache iranEvents in IntelligenceCache, preserve across refresh cycles - Wire data loading: always load Iran events (not gated by map layer), ingest into CII, trigger panel refresh - Add activeStrikes to CountryBriefSignals with geo-lookup counting - Render strike chip in Active Signals and include in fallback brief - Feed strike events into 7-day timeline (conflict lane) - Add structured strikeCount/highSeverityStrikeCount fields to GeoSignal (replaces fragile regex parsing in focal-point-detector) - Add active_strike signal type to InsightsPanel focal points - Add bounding-box fallback to signal aggregator for conflict events - Add i18n keys for activeStrikes * fix(alerts): add compound escalation for military action + geopolitical target (#548) Keyword matching was too rigid — "attacks on iran" didn't match CRITICAL because only "attack on iran" (singular) existed. Headlines like "strikes by US and Israel on Iran" also missed because words weren't adjacent. Added compound escalation: if a HIGH military/conflict keyword matches AND the headline mentions a critical geopolitical target (Iran, Russia, China, Taiwan, NATO, US forces), escalate to CRITICAL. Also added missing Iran keyword variants (plural forms, "Iran retaliates/strikes"). * feat(conflict): enhance Iran events popup with severity badge and related events (#549) Rewrite the Iran events popup to follow the established popup pattern (conflict/protest) with severity-colored header, badge, close button, stat rows, and source link using CSS classes instead of inline styles. - Add normalizeSeverity helper (clamps unknown values to 'low') - Show related events from same location (normalized matching, max 5) - Add IranEventPopupData to PopupData union (removes unsafe double cast) - Add iranEvent header CSS with severity border-left colors - Add i18n keys for en/ar/fr * feat(telegram): add Telegram Intel panel (#550) * feat(telegram): add Telegram Intel panel consuming relay feed - Service layer: fetchTelegramFeed() with 30s cache, types matching relay shape - Panel component: topic filter tabs, safe DOM rendering via h()+replaceChildren() - DataLoader + RefreshScheduler pattern (60s interval, hidden-tab aware) - Handles enabled=false and empty states from relay - CSS following existing gdelt-intel pattern - Panel title localized across all 18 locales * fix(i18n): add components.telegramIntel translations to 10 remaining locales * feat(live-news): add residential proxy + gzip decompression for YouTube detection (#551) YouTube blocks Vercel datacenter IPs — returns HTML without videoDetails/isLive data. Switch from edge runtime to Node.js serverless to enable HTTP CONNECT tunnel proxy via YOUTUBE_PROXY_URL env var. Add zlib decompression for gzip responses (YouTube returns empty body without Accept-Encoding header). Also adds missing fallback video IDs for WELT, KTN News, CNA NewsAsia, and updates TBS NEWS DIG fallback. * debug(live-news): add debug param to diagnose proxy env var on Vercel * fix(live-news): set explicit runtime: 'nodejs' for proxy support Vercel defaults to edge runtime when not specified. node:http/https/zlib imports are unavailable in edge — causing FUNCTION_INVOCATION_FAILED. Remove debug param added in previous commit. * fix(live-news): lazy-load node modules + proxy fallback to direct fetch Top-level import of node:http/https/zlib crashes if Vercel bundles for edge despite runtime: 'nodejs' config. Use dynamic import() to lazy-load at call time. Also add try/catch around proxy so it falls back to direct fetch if proxy connection fails. * feat(aviation): integrate AviationStack API for non-US airport delays (#552) Replace 100% simulated delay data for international airports with real flight data from AviationStack API. Add 28 Middle East/conflict-zone airports (Iran, Iraq, Lebanon, Syria, Yemen, Pakistan, Libya, Sudan). Key changes: - AviationStack integration with bounded concurrency (5 parallel), rotating batch (20 airports/cycle), and 20s deadline - Redis SETNX lock prevents cross-isolate cache stampede on expiry - Split FAA/intl caches (both 30min TTL) with isolated error handling - Fix severity colors (was checking 'GS'/'GDP', now minor/moderate/major/severe) - Fix tooltip (was obj.airport, now obj.name + obj.iata) - Add FLIGHT_DELAY_TYPE_CLOSURE for airport/airspace closures - Add closure i18n key across all 18 locales - Graceful fallback: no API key → simulation; API failure → simulation * feat(live-news): move YouTube proxy scraping to Railway relay Vercel serverless cannot use node:http/https for HTTP CONNECT proxy tunnels. Move the residential proxy YouTube scraping to the Railway relay (ais-relay.cjs) which has full Node.js access. - Add /youtube-live route to relay with proxy + direct fetch fallback - Add 5-min in-memory cache for channel lookups, 1hr for oembed - Revert Vercel api/youtube/live.js to edge runtime — now proxies to Railway first, falls back to direct scrape * feat(settings): add AVIATIONSTACK_API to desktop settings page (#553) Register the key in Rust keychain (SUPPORTED_SECRET_KEYS), frontend RuntimeSecretKey type, feature toggle, and settings UI under "Tracking & Sensing" category. * fix(live-news): use correct relay auth header for YouTube proxy (#554) Edge function was sending X-Relay-Auth header with RELAY_AUTH_TOKEN env var, but the Railway relay expects x-relay-key header validated against RELAY_SHARED_SECRET. This mismatch caused the relay to reject requests from Vercel, falling back to direct YouTube scrape (which fails from datacenter IPs for many channels). * fix(live-news): align YouTube edge function with relay auth pattern (#555) Use same getRelayBaseUrl/getRelayHeaders as other edge functions: - WS_RELAY_URL env var instead of VITE_WS_API_URL - RELAY_SHARED_SECRET + RELAY_AUTH_HEADER for flexible auth - Dual x-relay-key + Authorization headers * fix(i18n): rename OREF Sirens panel to Israel Sirens (#556) Remove internal implementation references (OREF, proxy relay, oref.org.il) from all user-facing strings across 18 locales and panel config. * fix(live-news): annotate empty catches and sanitize error output (#560) - Add context comments to empty catch blocks for debuggability - Replace error.message leak with generic client-safe message * fix(sentry): add noise filters and fix beforeSend null-filename leak (#561) - Add 8 new ignoreErrors patterns: signal timeout, premium gate, hybridExecute/mag/webkit bridge injections, postMessage null, NotSupportedError, appendChild injection, luma assignment - Fix LIDNotify regex to match both LIDNotify and LIDNotifyId - Fix beforeSend: strip null/anonymous filename frames so deck.gl TypeErrors (28 events, 8 users) are properly suppressed * feat(cii): wire OREF sirens into CII score & country brief (#559) * feat(cii): wire OREF sirens into CII score and country brief Active OREF sirens now boost Israel's CII score through two channels: - Conflict component: +25 base + min(25, alertCount*5) for active sirens - Blended score: +15 for active sirens, +5/+10 for 24h history thresholds Country brief for Israel shows a siren signal chip when alerts are active. * refactor(cii): extract getOrefBlendBoost helper to DRY scoring paths * fix(relay): add graceful shutdown + poll concurrency guard for Telegram (#562) - SIGTERM/SIGINT handler disconnects Telegram client before container dies - telegramPollInFlight guard prevents overlapping poll cycles - Mid-poll AUTH_KEY_DUPLICATED now permanently disables (was reconnect loop) * fix(aviation): query all airports instead of rotating batch (#557) * fix(aviation): query all airports instead of rotating batch of 20 The rotating batch (20 airports/cycle) caused major airports like DXB (52% cancellations) to be missed entirely for multiple cache cycles. With a paid AviationStack plan, query all ~90 non-US airports per refresh with concurrency 10 and 50s deadline (~9 chunks × 5s = 45s). * feat(cii): feed airport disruptions into CII and country brief Major/severe airport delays and closures now boost the CII security score and appear as signal chips in country briefs. Only major+ severity alerts are ingested to avoid noise from minor delays. - Add aviationDisruptions to CountryData and ingestAviationForCII() - Boost security score: closure +20, severe +15, major +10, moderate +5 - Store flight delays in intelligenceCache for country brief access - Add aviation disruptions chip in country brief signals grid * fix(relay): replace smart quotes crashing relay on startup (#563) * fix(relay): replace Unicode smart quotes crashing Node.js CJS parser * fix(relay): await Telegram disconnect + guard startup poll * fix(cii): resolve Gulf country strike misattribution via multi-match bbox disambiguation (#564) Dubai/Doha/Bahrain/Kuwait coordinates matched Iran's bounding box first due to iteration order. Now collects ALL matching bboxes, disambiguates via isCoordinateInCountry() geometry, and falls back to smallest-area bbox. - Add BH, QA, KW, JO, OM to bounds tables (previously missing entirely) - Extract ME_STRIKE_BOUNDS + resolveCountryFromBounds() into country-geometry.ts - All 4 consumer files use shared constant (single source of truth) - Bump CDN cache-bust param for iran-events endpoint * fix(relay): upstreamWs → upstreamSocket in graceful shutdown (#565) * fix(relay): install curl in Railway container for OREF polling (#567) * fix(relay): increase Polymarket cache TTL to 10 minutes (#568) * fix(relay): increase Polymarket cache TTL to 10 minutes All requests were MISS with 2-min TTL under concurrent load. Bump to 10-min cache and 5-min negative cache to reduce upstream pressure. * fix(relay): normalize Polymarket cache key from canonical params Raw url.search as cache key meant ?tag=fed&endpoint=events and ?endpoint=events&tag_slug=fed produced different keys for the same upstream request — defeating both cache and inflight dedup, causing 121 MISS entries in 3 seconds. Build cache key from parsed canonical params (endpoint + sorted query string) so all equivalent requests share one cache entry. * feat(webcams): add Iran tab to live webcams panel (#569) Add dedicated Iran region tab as the first/default tab with 4 feeds: Tehran, Middle East overview, Tehran (alt angle), and Jerusalem. * fix(relay): replace nixpacks.toml with railpack.json for curl (#571) Railway uses Railpack (not Nixpacks). nixpacks.toml in scripts/ was silently skipped. Use railpack.json at repo root with deploy.aptPackages to install curl at runtime for OREF polling. * fix(webcams): replace duplicate Tehran feed with Tel Aviv, rename Iran tab (#572) - Remove duplicate iran-tehran2 feed (same channel/video as iran-tehran) - Remove iran-mideast feed - Add Tel Aviv feed (-VLcYT5QBrY) to Iran Attacks tab - Rename tab label from "IRAN" to "IRAN ATTACKS" across all 18 locales * feat(scripts): add Iran events seed script and latest data (#575) Add seed-iran-events.mjs for importing Iran conflict events into Redis (conflict:iran-events:v1). Includes geocoding by location keywords and category-to-severity mapping. Data file contains 100 events from 2026-02-28. * fix(relay): add timeouts and logging to Telegram poll loop (#578) GramJS getEntity/getMessages have no built-in timeout. When the first channel hangs (FLOOD_WAIT, MTProto stall), telegramPollInFlight stays true forever, blocking all future polls — zero messages collected, zero errors logged, frontend shows "No messages available". - Add 15s per-channel timeout on getEntity + getMessages calls - Add 3-min overall poll cycle timeout - Force-clear stuck in-flight flag after 3.5 minutes - Detect FLOOD_WAIT errors and break loop early - Log per-cycle summary: channels polled, new msgs, errors, duration - Track media-only messages separately (no text → not a bug) - Expose lastError, pollInFlight, pollInFlightSince on /status endpoint * feat(cii): hook security advisories into CII scoring & country briefs (#579) Travel advisories (Do Not Travel, Reconsider, Caution) from US, AU, UK, NZ now act as a floor and boost on CII scores. Do Not Travel guarantees a minimum score of 60 (elevated), Reconsider floors at 50. Multi-source corroboration (3+ govts) adds +5 bonus. Advisory chips appear in country brief signal grid with level-appropriate styling, and advisory context is passed to AI brief generation. - Extract target country from advisory titles via embassy feed tags and country name matching - Add advisoryMaxLevel/advisoryCount/advisorySources to CII CountryData - Wire ingestAdvisoriesForCII into data loader pipeline - Add travelAdvisories/travelAdvisoryMaxLevel to CountryBriefSignals - Render advisory signal chips in CountryBriefPage * fix(sentry): guard setView against invalid preset + filter translateNotifyError (#580) - DeckGLMap.setView(): early-return if VIEW_PRESETS[view] is undefined, preventing TypeError on 'longitude' when select value is invalid - Add ignoreErrors pattern for Google Translate widget crash * feat(relay): bootstrap OREF 24h history on startup (#582) * fix(relay): improve OREF curl error logging with stderr capture -s flag silenced curl errors. Add -S to show errors, capture stderr via stdio pipes, and log curl's actual er…
matthewvecchione1-ops
pushed a commit
to matthewvecchione1-ops/worldmonitor
that referenced
this pull request
Mar 4, 2026
* Add Security Advisories panel with government travel alerts (#460)
* feat: add Security Advisories panel with government travel advisory feeds
Adds a new panel aggregating travel/security advisories from official
government foreign affairs agencies (US State Dept, AU DFAT Smartraveller,
UK FCDO, NZ MFAT). Advisories are categorized by severity level
(Do Not Travel, Reconsider, Caution, Normal) with filter tabs by
source country. Includes summary counts, auto-refresh, and persistent
caching via the existing data-freshness system.
* chore: update package-lock.json
* fix: event delegation, localization, and cleanup for SecurityAdvisories panel
P1 fixes:
- Use event delegation on this.content (bound once in constructor) instead
of direct addEventListener after each innerHTML replacement — prevents
memory leaks and stale listener issues on re-render
- Use setContent() consistently instead of mixing with this.content.innerHTML
- Add securityAdvisories translations to all 16 non-English locale files
(panels name, component strings, common.all key)
- Revert unrelated package-lock.json version bump
P2 fixes:
- Deduplicate loadSecurityAdvisories — loadIntelligenceData now calls the
shared method instead of inlining duplicate fetch+set logic
- Add Accept header to fetch calls for better content negotiation
* feat(advisories): add US embassy alerts, CDC, ECDC, and WHO health feeds
Adds 21 new advisory RSS feeds:
- 13 US Embassy per-country security alerts (TH, AE, DE, UA, MX, IN, PK, CO, PL, BD, IT, DO, MM)
- CDC Travel Notices
- 5 ECDC feeds (epidemiological, threats, risk assessments, avian flu, publications)
- 2 WHO feeds (global news, Africa emergencies)
Panel gains a Health filter tab for CDC/ECDC/WHO sources.
All new domains added to RSS proxy allowlist.
i18n "health" key added across all 17 locales.
* feat(cache): add negative-result caching to cachedFetchJson (#466)
When upstream APIs return errors (HTTP 403, 429, timeout), fetchers
return null. Previously null results were not cached, causing repeated
request storms against broken APIs every refresh cycle.
Now caches a sentinel value ('__WM_NEG__') with a short 2-minute TTL
on null results. Subsequent requests within that window get null
immediately without hitting upstream. Thrown errors (transient) skip
sentinel caching and retry immediately.
Also filters sentinels from getCachedJsonBatch pipeline reads and fixes
theater posture coalescing test (expected 2 OpenSky fetches for 2
theater query regions, not 1).
* feat: convert 52 API endpoints from POST to GET for edge caching (#468)
* feat: convert 52 API endpoints from POST to GET for edge caching
Convert all cacheable sebuf RPC endpoints to HTTP GET with query/path
parameters, enabling CDN edge caching to reduce costs. Flatten nested
request types (TimeRange, PaginationRequest, BoundingBox) into scalar
query params. Add path params for resource lookups (GetFredSeries,
GetHumanitarianSummary, GetCountryStockIndex, GetCountryIntelBrief,
GetAircraftDetails). Rewrite router with hybrid static/dynamic matching
for path param support.
Kept as POST: SummarizeArticle, ClassifyEvent, RecordBaselineSnapshot,
GetAircraftDetailsBatch, RegisterInterest.
Generated with sebuf v0.9.0 (protoc-gen-ts-client, protoc-gen-ts-server).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add rate_limited field to market response protos
The rateLimited field was hand-patched into generated files on main but
never declared in the proto definitions. Regenerating wiped it out,
breaking the build. Now properly defined in both ListEtfFlowsResponse
and ListMarketQuotesResponse protos.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* chore: remove accidentally committed .planning files
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add Cloudflare edge caching infrastructure for api.worldmonitor.app (#471)
Route web production RPC traffic through api.worldmonitor.app via fetch
interceptor (installWebApiRedirect). Add default Cache-Control headers
(s-maxage=300, stale-while-revalidate=60) on GET 200 responses, with
no-store override for real-time endpoints (vessel snapshot). Update CORS
to allow GET method. Skip Vercel bot middleware for API subdomain using
hostname check (non-spoofable, replacing CF-Ray header approach). Update
desktop cloud fallback to route through api.worldmonitor.app.
* fix(beta): eagerly load T5-small model when beta mode is enabled
BETA_MODE now couples the badge AND model loading — the summarization-beta
model starts loading on startup instead of waiting for the first summarization call.
* fix: move 5 path-param endpoints to query params for Vercel routing (#472)
Vercel's `api/[domain]/v1/[rpc].ts` captures one dynamic segment.
Path params like `/get-humanitarian-summary/SA` add an extra segment
that has no matching route file, causing 404 on both OPTIONS preflight
and direct requests. These endpoints were broken in production.
Changes:
- Remove `{param}` from 5 service.proto HTTP paths
- Add `(sebuf.http.query)` annotations to request message fields
- Update generated client/server code to use URLSearchParams
- Update OpenAPI specs (YAML + JSON) to declare query params
- Add early-return guards in 4 handlers for missing required params
- Add happy.worldmonitor.app to runtime.ts redirect hosts
Affected endpoints:
- GET /api/conflict/v1/get-humanitarian-summary?country_code=SA
- GET /api/economic/v1/get-fred-series?series_id=T10Y2Y&limit=120
- GET /api/market/v1/get-country-stock-index?country_code=US
- GET /api/intelligence/v1/get-country-intel-brief?country_code=US
- GET /api/military/v1/get-aircraft-details?icao24=a12345
* fix(security-advisories): route feeds through RSS proxy to avoid CORS blocks (#473)
- Advisory feeds were fetched directly from the browser, hitting CORS
on all 21 feeds (US State Dept, AU Smartraveller, US Embassies, ECDC,
CDC, WHO). Route through /api/rss-proxy on web, keep proxyUrl for desktop.
- Fix double slash in ECDC Avian Influenza URL (323//feed → 323/feed)
- Add feeds.news24.com to RSS proxy allowlist (was returning 403)
* feat(cache): tiered edge Cache-Control aligned to upstream TTLs (#474)
* fix: move 5 path-param endpoints to query params for Vercel routing
Vercel's `api/[domain]/v1/[rpc].ts` captures one dynamic segment.
Path params like `/get-humanitarian-summary/SA` add an extra segment
that has no matching route file, causing 404 on both OPTIONS preflight
and direct requests. These endpoints were broken in production.
Changes:
- Remove `{param}` from 5 service.proto HTTP paths
- Add `(sebuf.http.query)` annotations to request message fields
- Update generated client/server code to use URLSearchParams
- Update OpenAPI specs (YAML + JSON) to declare query params
- Add early-return guards in 4 handlers for missing required params
- Add happy.worldmonitor.app to runtime.ts redirect hosts
Affected endpoints:
- GET /api/conflict/v1/get-humanitarian-summary?country_code=SA
- GET /api/economic/v1/get-fred-series?series_id=T10Y2Y&limit=120
- GET /api/market/v1/get-country-stock-index?country_code=US
- GET /api/intelligence/v1/get-country-intel-brief?country_code=US
- GET /api/military/v1/get-aircraft-details?icao24=a12345
* feat(cache): add tiered edge Cache-Control aligned to upstream TTLs
Replace flat s-maxage=300 with 5 tiers (fast/medium/slow/static/no-store)
mapped per-endpoint to respect upstream Redis TTLs. Adds stale-if-error
resilience headers and X-No-Cache plumbing for future degraded responses.
X-Cache-Tier debug header gated behind ?_debug query param.
* fix(tech): use rss() for CISA feed, drop build from pre-push hook (#475)
- CISA Advisories used dead rss.worldmonitor.app domain (404), switch to rss() helper
- Remove Vite build from pre-push hook (tsc already catches errors)
* fix(desktop): enable click-to-play YouTube embeds + CISA feed fixes (#476)
* fix(tech): use rss() for CISA feed, drop build from pre-push hook
- CISA Advisories used dead rss.worldmonitor.app domain (404), switch to rss() helper
- Remove Vite build from pre-push hook (tsc already catches errors)
* fix(desktop): enable click-to-play for YouTube embeds in WKWebView
WKWebView blocks programmatic autoplay in cross-origin iframes regardless
of allow attributes, Permissions-Policy, mute-first retries, or secure
context. Documented all 10 approaches tested in docs/internal/.
Changes:
- Switch sidecar embed origin from 127.0.0.1 to localhost (secure context)
- Add MutationObserver + retry chain as best-effort autoplay attempts
- Use postMessage('*') to fix tauri://localhost cross-origin messaging
- Make sidecar play overlay non-interactive (pointer-events:none)
- Fix .webcam-iframe pointer-events:none blocking clicks in grid view
- Add expand button to grid cells for switching to single view on desktop
- Add http://localhost:* to CSP frame-src in index.html and tauri.conf.json
* fix(gateway): convert stale POST requests to GET for backwards compat (#477)
Stale cached client bundles still send POST to endpoints converted to
GET in PR #468, causing 404s. The gateway now parses the POST JSON body
into query params and retries the match as GET.
* feat(proxy): add Cloudflare edge caching for proxy.worldmonitor.app (#478)
Add CDN-Cache-Control headers to all proxy endpoints so Cloudflare can
cache responses at the edge independently of browser Cache-Control:
- RSS: 600s edge + stale-while-revalidate=300 (browser: 300s)
- UCDP: 3600s edge (matches browser)
- OpenSky: 15s edge (browser: 30s) for fresher flight data
- WorldBank: 1800s/86400s edge (matches browser)
- Polymarket: 120s edge (matches browser)
- Telegram: 10s edge (matches browser)
- AIS snapshot: 2s edge (matches browser)
Also fixes:
- Vary header merging: sendCompressed/sendPreGzipped now merge existing
Vary: Origin instead of overwriting, preventing cross-origin cache
poisoning at the edge
- Stale fallback responses (OpenSky, WorldBank, Polymarket, RSS) now
set Cache-Control: no-store + CDN-Cache-Control: no-store to prevent
edge caching of degraded responses
- All no-cache branches get CDN-Cache-Control: no-store
- /opensky-reset gets no-store (state-changing endpoint)
* fix(sentry): add noise filters for 4 unresolved issues (#479)
- Tighten AbortError filter to match "AbortError: The operation was aborted"
- Filter "The user aborted a request" (normal navigation cancellation)
- Filter UltraViewer service worker injection errors (/uv/service/)
- Filter Huawei WebView __isInQueue__ injection
* feat: configurable VITE_WS_API_URL + harden POST→GET shim (#480)
* fix(gateway): harden POST→GET shim with scalar guard and size limit
- Only convert string/number/boolean values to query params (skip objects,
nested arrays, __proto__ etc.) to prevent prototype pollution vectors
- Skip body parsing for Content-Length > 1MB to avoid memory pressure
* feat: make API base URL configurable via VITE_WS_API_URL
Replace hardcoded api.worldmonitor.app with VITE_WS_API_URL env var.
When empty, installWebApiRedirect() is skipped entirely — relative
/api/* calls stay on the same domain (local installs). When set,
browser fetch is redirected to that URL.
Also adds VITE_WS_API_URL and VITE_WS_RELAY_URL hostnames to
APP_HOSTS allowlist dynamically.
* fix(analytics): use greedy regex in PostHog ingest rewrites (#481)
Vercel's :path* wildcard doesn't match trailing slashes that
PostHog SDK appends (e.g. /ingest/s/?compression=...), causing 404s.
Switch to :path(.*) which matches all path segments including
trailing slashes. Ref: PostHog/posthog#17596
* perf(proxy): increase AIS snapshot edge TTL from 2s to 10s (#482)
With 20k requests/30min (60% of proxy traffic) and per-PoP caching,
a 2s edge TTL expires before the next request from the same PoP arrives,
resulting in near-zero cache hits. 10s allows same-PoP dedup while
keeping browser TTL at 2s for fresh vessel positions.
* fix(markets): commodities panel showing stocks instead of commodities (#483)
The shared circuit breaker (cacheTtlMs: 0) cached the stocks response,
then the stale-while-revalidate path returned that cached stocks data
for the subsequent commodities fetch. Skip SWR when caching is disabled.
* feat(gateway): complete edge cache tier coverage + degraded-response policy (#484)
- Add 11 missing GET routes to RPC_CACHE_TIER map (8 slow, 3 medium)
- Add response-headers side-channel (WeakMap) so handlers can signal
X-No-Cache without codegen changes; wire into military-flights and
positive-geo-events handlers on upstream failure
- Add env-controlled per-endpoint tier override (CACHE_TIER_OVERRIDE_*)
for incident response rollback
- Add VITE_WS_API_URL hostname allowlist (*.worldmonitor.app + localhost)
- Fix fetch.bind(globalThis) in positive-events-geo.ts (deferred lambda)
- Add CI test asserting every generated GET route has an explicit cache
tier entry (prevents silent default-tier drift)
* chore: bump version to 2.5.20 + changelog
Covers PRs #452–#484: Cloudflare edge caching, commodities SWR fix,
security advisories panel, settings redesign, 52 POST→GET migrations.
* fix(rss): remove stale indianewsnetwork.com from proxy allowlist (#486)
Feed has no <pubDate> fields and latest content is from April 2022.
Not referenced in any feed config — only in the proxy domain allowlist.
* feat(i18n): add Korean (한국어) localization (#487)
- Add ko.json with all 1606 translation keys matching en.json structure
- Register 'ko' in SUPPORTED_LANGUAGES, LANGUAGES display array, and locale map
- Korean appears as 🇰🇷 한국어 in the language dropdown
* feat: add Polish tv livestreams (#488)
* feat(rss): add Axios (api.axios.com/feed) as US news source (#494)
Add api.axios.com to proxy allowlist and CSP connect-src, register
Axios feed under US category as Tier 2 mainstream source.
* perf: bootstrap endpoint + polling optimization (#495)
* perf: bootstrap endpoint + polling optimization (phases 3-4)
Replace 15+ individual RPC calls on startup with a single /api/bootstrap
batch call that fetches pre-cached data from Redis. Consolidate 6 panel
setInterval timers into the central RefreshScheduler for hidden-tab
awareness (10x multiplier) and adaptive backoff (up to 4x for unchanged
data). Convert IntelligenceGapBadge from 10s polling to event-driven
updates with 60s safety fallback.
* fix(bootstrap): inline Redis + cache keys in edge function
Vercel Edge Functions cannot resolve cross-directory TypeScript imports
from server/_shared/. Inline getCachedJsonBatch and BOOTSTRAP_CACHE_KEYS
directly in api/bootstrap.js. Add sync test to ensure inlined keys stay
in sync with the canonical server/_shared/cache-keys.ts registry.
* test: add Edge Function module isolation guard for all api/*.js files
Prevents any Edge Function from importing from ../server/ or ../src/
which breaks Vercel builds. Scans all 12 non-helper Edge Functions.
* fix(bootstrap): read unprefixed cache keys on all environments
Preview deploys set VERCEL_ENV=preview which caused getKeyPrefix() to
prefix Redis keys with preview:<sha>:, but handlers only write to
unprefixed keys on production. Bootstrap is a read-only consumer of
production cache — always read unprefixed keys.
* fix(bootstrap): wire sectors hydration + add coverage guard
- Wire getHydratedData('sectors') in data-loader to skip Yahoo Finance
fetch when bootstrap provides sector data
- Add test ensuring every bootstrap key has a getHydratedData consumer
— prevents adding keys without wiring them
* fix(server): resolve 25 TypeScript errors + add server typecheck to CI
- _shared.ts: remove unused `delay` variable
- list-etf-flows.ts: add missing `rateLimited` field to 3 return literals
- list-market-quotes.ts: add missing `rateLimited` field to 4 return literals
- get-cable-health.ts: add non-null assertions for regex groups and array access
- list-positive-geo-events.ts: add non-null assertion for array index
- get-chokepoint-status.ts: add required fields to request objects
- CI: run `typecheck:api` (tsconfig.api.json) alongside `typecheck` to catch
server/ TS errors before merge
* feat(military): server-side military bases 125K + rate limiting (#496)
* feat(military): server-side military bases with 125K entries + rate limiting (#485)
Migrate military bases from 224 static client-side entries to 125,380
server-side entries stored in Redis GEO sorted sets, served via
bbox-filtered GEOSEARCH endpoint with server-side clustering.
Data pipeline:
- Pizzint/Polyglobe: 79,156 entries (Supabase extraction)
- OpenStreetMap: 45,185 entries
- MIRTA: 821 entries
- Curated strategic: 218 entries
- 277 proximity duplicates removed
Server:
- ListMilitaryBases RPC with GEOSEARCH + HMGET + tier/filter/clustering
- Antimeridian handling (split bbox queries)
- Blue-green Redis deployment with atomic version pointer switch
- geoSearchByBox() + getHashFieldsBatch() helpers in redis.ts
Security:
- @upstash/ratelimit: 60 req/min sliding window per IP
- IP spoofing fix: prioritize x-real-ip (Vercel-injected) over x-forwarded-for
- Require API key for non-browser requests (blocks unauthenticated curl/scripts)
- Input validation: allowlisted types/kinds, regex country, clamped bbox/zoom
Frontend:
- Viewport-driven loading with bbox quantization + debounce
- Server-side grid clustering at low zoom levels
- Enriched popup with kind, category badges (airforce/naval/nuclear/space)
- Static 224 bases kept as search fallback + initial render
* fix(military): fallback to production Redis keys in preview deployments
Preview deployments prefix Redis keys with `preview:{sha}:` but military
bases data is seeded to unprefixed (production) keys. When the prefixed
`military:bases:active` key is missing, fall back to the unprefixed key
and use raw (unprefixed) keys for geo/meta lookups.
* fix: remove unused 'remaining' destructure in rate-limit (TS6133)
* ci: add typecheck:api to pre-push hook to catch server-side TS errors
* debug(military): add X-Bases-Debug response header for preview diagnostics
* fix(bases): trigger initial server fetch on map load
fetchServerBases() was only called on moveend — if the user
never panned/zoomed, the API was never called and only the 224
static fallback bases showed.
* perf(military): debounce base fetches + upgrade edge cache to static tier (#497)
- Add 300ms debounce on moveend to prevent rapid pan flooding
- Fixes stale-bbox bug where pendingFetch returns old viewport data
- Upgrade edge cache tier from medium (5min) to static (1hr) — bases are
static infrastructure, aligned with server-side cachedFetchJson TTL
- Keep error logging in catch blocks for production diagnostics
* fix(cyber): make GeoIP centroid fallback jitter deterministic (#498)
Replace Math.random() jitter with DJB2 hash seeded by the threat
indicator (IP/URL), so the same threat always maps to the same
coordinates across requests while different threats from the same
country still spread out.
Closes #203
Co-authored-by: Chris Chen <fuleinist@users.noreply.github.com>
* fix: use cross-env for Windows-compatible npm scripts (#499)
Replace direct `VAR=value command` syntax with cross-env/cross-env-shell
so dev, build, test, and desktop scripts work on Windows PowerShell/CMD.
Co-authored-by: facusturla <facusturla@users.noreply.github.com>
* feat(live-news): add CBC News to optional North America channels (#502)
YouTube handle @CBCNews with fallback video ID 5vfaDsMhCF4.
* fix(bootstrap): harden hydration cache + polling review fixes (#504)
- Filter null/undefined values before storing in hydration cache to
prevent future consumers using !== undefined from misinterpreting
null as valid data
- Debounce wm:intelligence-updated event handler via requestAnimationFrame
to coalesce rapid alert generation into a single render pass
- Include alert IDs in StrategicRiskPanel change fingerprint so content
changes are detected even when alert count stays the same
- Replace JSON.stringify change detection in ServiceStatusPanel with
lightweight name:status fingerprint
- Document max effective refresh interval (40x base) in scheduler
* fix(geo): tokenization-based keyword matching to prevent false positives (#503)
* fix(geo): tokenization-based keyword matching to prevent false positives
Replace String.includes() with tokenization-based Set.has() matching
across the geo-tagging pipeline. Prevents false positives like "assad"
matching inside "ambassador" and "hts" matching inside "rights".
- Add src/utils/keyword-match.ts as single source of truth
- Decompose possessives/hyphens ("Assad's" → includes "assad")
- Support multi-word phrase matching ("white house" as contiguous)
- Remove false-positive-prone DC keywords ('house', 'us ')
- Update 9 consumer files across geo-hub, map, CII, and asset systems
- Add 44 tests covering false positives, true positives, edge cases
Co-authored-by: karim <mirakijka@gmail.com>
Fixes #324
* fix(geo): add inflection suffix matching + fix test imports
Address code review feedback:
P1a: Add suffix-aware matching for plurals and demonyms so existing
keyword lists don't regress (houthi→houthis, ukraine→ukrainian,
iran→iranian, israel→israeli, russia→russian, taiwan→taiwanese).
Uses curated suffix list + e-dropping rule to avoid false positives.
P1b: Expand conflictTopics arrays in DeckGLMap and Map with demonym
forms so "Iranian senate..." correctly registers as conflict topic.
P2: Replace inline test functions with real module import via tsx.
Tests now exercise the production keyword-match.ts directly.
* fix: wire geo-keyword tests into test:data command
The .mts test file wasn't covered by `node --test tests/*.test.mjs`.
Add `npx tsx --test tests/*.test.mts` so test:data runs both suites.
* fix: cross-platform test:data + pin tsx in devDependencies
- Use tsx as test runner for both .mjs and .mts (single invocation)
- Removes ; separator which breaks on Windows cmd.exe
- Add tsx to devDependencies so it works in offline/CI environments
* fix(geo): multi-word demonym matching + short-keyword suffix guard
- Add wordMatches() for suffix-aware phrase matching so "South Korean"
matches keyword "south korea" and "North Korean" matches "north korea"
- Add MIN_SUFFIX_KEYWORD_LEN=4 guard so short keywords like "ai", "us",
"hts" only do exact-match (prevents "ais"→"ai", "uses"→"us" false positives)
- Add 5 new tests covering both fixes (58 total, all passing)
* fix(geo): support plural demonyms in keyword matching
Add compound suffixes (ians, eans, ans, ns, is) to handle plural
demonym forms like "Iranians"→"iran", "Ukrainians"→"ukraine",
"Russians"→"russia", "Israelis"→"israel". Adds 5 new tests (63 total).
---------
Co-authored-by: karim <mirakijka@gmail.com>
* chore: strip 61 debug console.log calls from 20 service files (#501)
* chore: strip 61 debug console.log calls from services
Remove development/tracing console.log statements from 20 files.
These add noise to production browser consoles and increase bundle size.
Preserved: all console.error (error handling) and console.warn (warnings).
Preserved: debug-gated logs in runtime.ts (controlled by verbose flag).
Removed: debugInjectTestEvents() from geo-convergence.ts (test-only code).
Removed: logSummary()/logReport() methods that were pure console.log wrappers.
* fix: remove orphaned stubs and remaining debug logs from stripped services
- Remove empty logReport() method and unused startTime variable (parallel-analysis.ts)
- Remove orphaned console.group/console.groupEnd pair (parallel-analysis.ts)
- Remove empty logSignalSummary() export (signal-aggregator.ts)
- Remove logSignalSummary import/call and 3 remaining console.logs (InsightsPanel.ts)
- Remove no-op logDirectFetchBlockedOnce() and dead infrastructure (prediction/index.ts)
* fix: generalize Vercel preview origin regex + include filters in bases cache key (#506)
- api/_api-key.js: preview URL pattern was user-specific (-elie-),
rejecting other collaborators' Vercel preview deployments.
Generalized to match any worldmonitor-*.vercel.app origin.
- military-bases.ts: client cache key only checked bbox/zoom, ignoring
type/kind/country filters. Switching filters without panning returned
stale results. Unified into single cacheKey string.
* fix(prediction): filter stale/expired markets from Polymarket panel (#507)
Prediction panel was showing expired markets (e.g. "Will US strike Iran
on Feb 9" at 0%). Root causes: no active/archived API filters, no
end_date_min param, no client-side expiry guard, and sub-market selection
picking highest volume before filtering expired ones.
- Add active=true, archived=false, end_date_min API params to all 3
Gamma API call sites (events, markets, probe)
- Pre-filter sub-markets by closed/expired BEFORE volume selection in
both fetchPredictions() and fetchCountryMarkets()
- Add defense-in-depth isExpired() client-side filter on final results
- Propagate endDate through all market object paths including sebuf
fallback
- Show expiry date in PredictionPanel UI with new .prediction-meta
layout
- Add "closes" i18n key to all 18 locale files
- Add endDate to server handler GammaMarket/GammaEvent interfaces and
map to proto closesAt field
* fix(relay): guard proxy handlers against ERR_HTTP_HEADERS_SENT crash (#509)
Polymarket and World Bank proxy handlers had unguarded res.writeHead()
calls in error/timeout callbacks that race with the response callback.
When upstream partially responds then times out, both paths write
headers → process crash. Replace 5 raw writeHead+end calls with
safeEnd() which checks res.headersSent before writing.
* feat(breaking-news): add active alert banner with audio for critical/high RSS items (#508)
RSS items classified as critical/high threat now trigger a full-width
breaking news banner with audio alert, auto-dismiss (60s/30s by severity),
visibility-aware timer pause, dedup, and a toggle in the Intelligence
Findings dropdown.
* fix(sentry): filter Android OEM WebView bridge injection errors (#510)
Add ignoreErrors pattern for LIDNotifyId, onWebViewAppeared, and
onGetWiFiBSSID — native bridge functions injected by Lenovo/Huawei
device SDKs into Chrome Mobile WebView. No stack frames in our code.
* chore: add validated telegram channels list (global + ME + Iran + cyber) (#249)
* feat(conflict): add Iran Attacks map layer + strip debug logs (#511)
* chore: strip 61 debug console.log calls from services
Remove development/tracing console.log statements from 20 files.
These add noise to production browser consoles and increase bundle size.
Preserved: all console.error (error handling) and console.warn (warnings).
Preserved: debug-gated logs in runtime.ts (controlled by verbose flag).
Removed: debugInjectTestEvents() from geo-convergence.ts (test-only code).
Removed: logSummary()/logReport() methods that were pure console.log wrappers.
* fix: remove orphaned stubs and remaining debug logs from stripped services
- Remove empty logReport() method and unused startTime variable (parallel-analysis.ts)
- Remove orphaned console.group/console.groupEnd pair (parallel-analysis.ts)
- Remove empty logSignalSummary() export (signal-aggregator.ts)
- Remove logSignalSummary import/call and 3 remaining console.logs (InsightsPanel.ts)
- Remove no-op logDirectFetchBlockedOnce() and dead infrastructure (prediction/index.ts)
* feat(conflict): add Iran Attacks map layer
Adds a new Iran-focused conflict events layer that aggregates real-time
events, geocodes via 40-city lookup table, caches 15min in Redis, and
renders as a toggleable DeckGL ScatterplotLayer with severity coloring.
- New proto + codegen for ListIranEvents RPC
- Server handler with HTML parsing, city geocoding, category mapping
- Frontend service with circuit breaker
- DeckGL ScatterplotLayer with severity-based color/size
- MapPopup with sanitized source links
- iranAttacks toggle across all variants, harnesses, and URL state
* fix: resolve bootstrap 401 and 429 rate limiting on page init (#512)
Same-origin browser requests don't send Origin header (per CORS spec),
causing validateApiKey to reject them. Extract origin from Referer as
fallback. Increase rate limit from 60 to 200 req/min to accommodate
the ~50 requests fired during page initialization.
* fix(relay): prevent Polymarket OOM via request deduplication (#513)
Concurrent Polymarket requests for the same cache key each fired
independent https.get() calls. With 12 categories × multiple clients,
740 requests piled up in 10s, all buffering response bodies → 4.1GB
heap → OOM crash on Railway.
Fix: in-flight promise map deduplicates concurrent requests to the
same cache key. 429/error responses are negative-cached for 30s to
prevent retry storms.
* fix(threat-classifier): add military/conflict keyword gaps and news-to-conflict bridge (#514)
Breaking news headlines like "Israel's strike on Iran" were classified as
info level because the keyword classifier lacked standalone conflict phrases.
Additionally, the conflict instability score depended solely on ACLED data
(1-7 day lag) with no bridge from real-time breaking news.
- Add 3 critical + 18 high contextual military/conflict keywords
- Preserve threat classification on semantically merged clusters
- Add news-derived conflict floor when ACLED/HAPI report zero signal
- Upsert news events by cluster ID to prevent duplicates
- Extract newsEventIndex to module-level Map for serialization safety
* fix(breaking-news): let critical alerts bypass global cooldown and replace HIGH alerts (#516)
Global cooldown (60s) was blocking critical alerts when a less important
HIGH alert fired from an earlier RSS batch. Added priority-aware cooldown
so critical alerts always break through. Banner now auto-dismisses HIGH
alerts when a CRITICAL arrives. Added Iran/strikes keywords to classifier.
* fix(rate-limit): increase sliding window to 300 req/min (#515)
App init fires many concurrent classify-event, summarize-article, and
record-baseline-snapshot calls, exhausting the 200/min limit and causing
429s. Bump to 300 as a temporary measure while client-side batching is
implemented.
* fix(breaking-news): fix fake pubDate fallback and filter noisy think-tank alerts (#517)
Two bugs causing stale CrisisWatch article to fire as breaking alert:
1. Non-standard pubDate format ("Friday, February 27, 2026 - 12:38")
failed to parse → fallback was `new Date()` (NOW) → day-old articles
appeared as "just now" and passed recency gate on every fetch
2. Tier 3+ sources (think tanks) firing alerts on keyword-only matches
like "War" in policy analysis titles — too noisy for breaking alerts
Fix: parsePubDate() handles non-standard formats and falls back to
epoch (not now). Tier 3+ sources require LLM classification to fire.
* fix: make iran-events handler read-only from Redis (#518)
Remove server-side LiveUAMap scraper (blocked by Cloudflare 403 on
Vercel IPs). Handler now reads pre-populated Redis cache pushed from
local browser scraping. Change cache tier from slow to fast to prevent
CDN from serving stale empty responses for 30+ minutes.
* fix(relay): Polymarket circuit breaker + concurrency limiter (OOM fix) (#519)
* fix(rate-limit): increase sliding window to 300 req/min
App init fires many concurrent classify-event, summarize-article, and
record-baseline-snapshot calls, exhausting the 200/min limit and causing
429s. Bump to 300 as a temporary measure while client-side batching is
implemented.
* fix(relay): add Polymarket circuit breaker + concurrency limiter to prevent OOM
Railway relay OOM crash: 280 Polymarket 429 errors in 8s, heap hit 3.7GB.
Multiple unique cache keys bypassed per-key dedup, flooding upstream.
- Circuit breaker: trips after 5 consecutive failures, 60s cooldown
- Concurrent upstream limiter: max 3 simultaneous requests
- Negative cache TTL: 30s → 60s to reduce retry frequency
- Upstream slot freed on response.on('end'), not headers, preventing
body buffer accumulation past the concurrency cap
* fix(relay): guard against double-finalization on Polymarket timeout
request.destroy() in timeout handler also fires request.on('error'),
causing double decrement of polymarketActiveUpstream (counter goes
negative, disabling concurrency cap) and double circuit breaker trip.
Add finalized guard so decrement + failure accounting happens exactly
once per request regardless of which error path fires first.
* fix(threat-classifier): stagger AI classification requests to avoid Groq 429 (#520)
flushBatch() fired up to 20 classifyEvent RPCs simultaneously via
Promise.all, instantly hitting Groq's ~30 req/min rate limit.
- Sequential execution with 2s min-gap between requests (~28 req/min)
- waitForGap() enforces hard floor + jitter across batch boundaries
- batchInFlight guard prevents concurrent flush loops
- 429/5xx: requeue failed job (with retry cap) + remaining untouched jobs
- Queue cap at 100 items with warn on overflow
* fix(relay): regenerate package-lock.json with telegram dependency
The lockfile was missing resolved entries for the telegram package,
causing Railway to skip installation despite it being in package.json.
* chore: trigger deploy to flush CDN cache for iran-events endpoint
* Revert "fix(relay): regenerate package-lock.json with telegram dependency"
This reverts commit a8d5e1dbbd3300708a081d31783d93e793a072c0.
* fix(relay): add POLYMARKET_ENABLED env flag kill switch (#523)
Set POLYMARKET_ENABLED=false on Railway to disable all Polymarket
upstream requests. Returns 503 immediately, preventing OOM crashes.
* fix(breaking-news): fill keyword gaps missing real Iran attack headlines (#521)
* fix(breaking-news): fill keyword gaps that miss real Iran attack headlines
Three root causes for zero alerts during the Iran war:
1. Keyword gaps — top Iran headlines failed classification:
- "US and Israel attack Iran" → info (no "attack iran" keyword)
- "attacked Iran" → info (only "attacks iran" existed, plural)
- "Explosions heard in Tehran" → info (no "explosions" keyword)
Added: attack iran, attacked iran, attack on iran, attack against iran,
bombing/bombed iran, war against iran (CRITICAL); explosions,
launched/launches attacks, retaliatory/preemptive/preventive attack (HIGH)
2. 5-item RSS limit — Al Jazeera's CRITICAL "major combat operations"
headline was item #7 and never reached the classifier. Increased
per-feed limit from 5 to 10.
3. False positive — "OpenAI strikes deal with Pentagon" matched HIGH
keyword "strikes". Added "strikes deal/agreement/partnership" to
exclusions.
* fix(threat-classifier): prevent Iran compound keyword false positives
"attack iran" as plain substring matched "Iran-backed" and "Iranian"
in headlines about proxy groups, not direct attacks on Iran.
Added TRAILING_BOUNDARY_KEYWORDS set with negative lookahead (?![\w-])
for all Iran compound keywords. This rejects "Iran-backed militias"
and "Iranian targets" while still matching "attack Iran:" and
"attack Iran" at end of string.
Addresses Codex review comment on PR #521.
* fix(relay): regenerate package-lock.json with telegram dependency (#522)
The lockfile was missing resolved entries for the telegram package,
causing Railway to skip installation despite it being in package.json.
* fix(iran): bypass stale CDN cache for iran-events endpoint (#524)
The CDN cached empty {events:[],scrapedAt:0} from the pre-Redis
deployment and Vercel deploy didn't purge all edge nodes. Add ?_v=2
query param to force cache miss until CDN naturally expires the
stale slow-tier entry.
* fix(focal-points): attribute theater military activity to target nations (#525)
The signal aggregator attributed military flights/vessels to the country
they're physically over (point-in-polygon). Aircraft attacking Iran from
the Persian Gulf got attributed to XX/IQ/SA, not IR — so Iran showed
ELEVATED in Focal Points despite being under active attack (CRIT in
Strategic Posture).
Feed theater-level posture data back into the signal aggregator for
target nations (Iran, Taiwan, North Korea, Gaza, Yemen) so they get
credited for military activity in their theater bounding box. Includes
double-count guard to skip if the nation already has signals.
Also fixes stale "sebuf" comment in threat-classifier.
* fix(relay): block rsshub.app requests with 410 Gone (#526)
Stale clients still send RSS requests to rsshub.app (NHK, MOFCOM, MIIT).
These feeds were migrated to Google News RSS but cached PWA clients keep
hitting the relay, which forwards to rsshub.app and gets 403.
- Add explicit blocklist returning 410 Gone before allowlist check
- Remove rsshub.app from all allowlists (relay, edge proxy, vite)
- Remove dead AP News dev proxy target
* feat(map): prioritize Iran Attacks layer (#527)
* feat(map): move Iran Attacks layer to first position and enable by default
Move iranAttacks to the top of the layer toggle list in the full
(geopolitical) variant so it appears first. Enable it by default on
both desktop and mobile during the active conflict.
* feat(map): add Iran Attacks layer support to SVG/mobile map
- Implement setIranEvents() in SVG Map (was no-op)
- Render severity-colored circle markers matching DeckGL layer
- Add iranAttacks to mobile layer toggles (first position)
- Forward setIranEvents to SVG map in MapContainer
- Add IranEventPopupData to PopupData union for click popups
- Add .iran-event-marker CSS with pulse animation
- Add data-layer-hidden-iranAttacks CSS toggle
* fix(geo): expand geo hub index with 60+ missing world locations (#528)
The geo hub index only had ~30 entries, missing all Gulf states (UAE,
Qatar, Bahrain, Kuwait, Oman), Iraq cities, and many world capitals.
News mentioning Abu Dhabi, Dubai, Baghdad, etc. had no lat/lon assigned
so they never appeared on the map.
Added: Gulf capitals (Abu Dhabi, Dubai, Doha, Manama, Kuwait, Muscat),
Iraq (Baghdad, Erbil, Basra), Jordan, Istanbul, Haifa, Dimona, Isfahan,
Kabul, Mumbai, Shanghai, Hong Kong, Singapore, Manila, Jakarta, Bangkok,
Hanoi, Canberra, all major European capitals (Rome, Madrid, Warsaw,
Bucharest, Helsinki, Stockholm, Oslo, Baltics, Athens, Belgrade, Minsk,
Tbilisi, Chisinau, Yerevan, Baku), Americas (Ottawa, Mexico City,
Brasilia, Buenos Aires, Caracas, Bogota, Havana), Africa (Nairobi,
Pretoria, Lagos, Kinshasa, Mogadishu, Tripoli, Tunis, Algiers, Rabat),
conflict zones (Iraq, Kashmir, Golan), chokepoints (Malacca, Panama,
Gibraltar), and US military bases (Ramstein, Incirlik, Diego Garcia,
Guam, Okinawa).
* fix(iran): bust CDN cache to serve updated Gulf-geocoded events (#532)
CDN edge cache was still serving stale 93-event response without
Gulf state coordinates (UAE, Bahrain, Qatar, Kuwait). Bump cache
key from ?_v=2 to ?_v=3 so browsers fetch fresh 100-event data.
Also gitignore internal/ for private tooling scripts.
* fix(alerts): remove SESSION_START gate that blocked pre-existing breaking news (#533)
The isRecent() function used Math.max(now - 15min, SESSION_START) as
recency cutoff. Since SESSION_START = Date.now() at module load, items
published before page load could never trigger alerts — they failed the
SESSION_START gate in the first 15 min, then aged past the 15-min window.
Now uses only the 15-minute recency window. Spam prevention remains via
per-event dedup (30 min), global cooldown (60s), and source tier filter.
* fix(relay): Telegram + OOM + memory cleanup (#531)
* fix(relay): resolve Telegram missing package, OOM crashes, and memory cleanup
- Add `telegram` and `ws` to root dependencies so Railway's `npm install` installs them
- Log V8 heap limit at startup to confirm NODE_OPTIONS is active
- Make MAX_VESSELS/MAX_VESSEL_HISTORY env-configurable (default 20k, down from 50k)
- Add permanent latch to skip Telegram import retries when package is missing
- Raise memory cleanup threshold from 450MB to 2GB (env-configurable)
- Clear all caches (RSS, Polymarket, WorldBank) during emergency cleanup
* fix(relay): treat blank env vars as unset in safeInt
Number('') === 0 passes isFinite, silently clamping caps to 1000
instead of using the 20000 default. Guard empty/null before parsing.
* fix(live-news): replace 7 stale YouTube fallback video IDs (#535)
Validated all 23 YouTube fallbackVideoIds via oEmbed API and all 9
HLS URLs. Found 5 broken IDs (403 embed-restricted or 404 deleted)
plus 2 previously identified stale IDs:
- Fox News: QaftgYkG-ek → ZvdiJUYGBis
- Sky News Arabia: MN50dHFHMKE → U--OjmpjF5o
- RTVE 24H: 7_srED6k0bE → -7GEFgUKilA
- CNN Brasil: 1kWRw-DA6Ns → 6ZkOlaGfxq4
- C5N: NdQSOItOQ5Y → SF06Qy1Ct6Y
- TBS NEWS DIG: ohI356mwBp8 → Anr15FA9OCI
- TRT World: CV5Fooi8WDI → ABfFhWzWs0s
All 9 HLS URLs validated OK. 16 remaining YouTube IDs validated OK.
* fix(relay): fix telegram ESM import path and broaden latch regex
- `import('telegram/sessions')` fails with "Directory import is not
supported resolving ES modules" — use explicit `telegram/sessions/index.js`
- Broaden permanent-disable latch to also catch "Directory import" errors
* fix(ui): move download banner to bottom-right (#536)
* fix(alerts): remove SESSION_START gate that blocked pre-existing breaking news
The isRecent() function used Math.max(now - 15min, SESSION_START) as
recency cutoff. Since SESSION_START = Date.now() at module load, items
published before page load could never trigger alerts — they failed the
SESSION_START gate in the first 15 min, then aged past the 15-min window.
Now uses only the 15-minute recency window. Spam prevention remains via
per-event dedup (30 min), global cooldown (60s), and source tier filter.
* fix(ui): move download banner to bottom-right of screen
Repositioned from top-right (overlapping content) to bottom-right.
Dismissal already persists via localStorage. Added TODO for header
download link.
* Revert "fix(relay): fix telegram ESM import path and broaden latch regex"
This reverts commit 1f2f0175abdc52d4ee841ac271eb0f48000cbddf.
* Revert "Revert "fix(relay): fix telegram ESM import path and broaden latch regex"" (#537)
This reverts commit ad41a2e2d245850fd4b699af2adbe53acca80325.
* feat: add day/night solar terminator overlay to map (#529)
* Trigger redeploy with preview env vars
* Trigger deployment
* chore: trigger redeploy for PR #41
* chore: trigger Vercel redeploy (edge function transient failure)
* chore: retrigger Vercel deploy
* feat: add Nigeria feeds and Greek locale feeds (#271)
- Add 5 Nigeria news sources to Africa section (Premium Times, Vanguard,
Channels TV, Daily Trust, ThisDay)
- Add 5 Greek feeds with lang: 'el' for locale-aware filtering
(Kathimerini, Naftemporiki, in.gr, iefimerida, Proto Thema)
- Add source tiers for all new outlets
- Allowlist 8 new domains in RSS proxy
* fix: enforce military bbox filtering and add behavioral cache tests (#284)
* fix: add request coalescing to Redis cache layer
Concurrent cache misses for the same key now share a single upstream
fetch instead of each triggering redundant API calls. This eliminates
duplicate work within Edge Function invocations under burst traffic.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: reduce AIS polling frequency from 10s to 30s
Vessel positions do not change meaningfully in 10 seconds at sea.
Reduces Railway relay requests by 66% with negligible UX impact.
Stale threshold bumped to 45s to match the new interval.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: quantize military flights bbox cache keys to 1-degree grid
Precise bounding box coordinates caused near-zero cache hit rate since
every map pan/zoom produced a unique key. Snapping to a 1-degree grid
lets nearby viewports share cache entries, dramatically reducing
redundant OpenSky API calls.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: parallelize ETF chart fetches instead of sequential await loop
The loop awaited each ETF chart fetch individually, blocking on every
Yahoo gate delay. Using Promise.allSettled lets all 10 fetches queue
concurrently through the Yahoo gate, cutting wall time from ~12s to ~6s.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add Redis pipeline batch GET to reduce round-trips
Add getCachedJsonBatch() using the Upstash pipeline API to fetch
multiple keys in a single HTTP call. Refactor aircraft details batch
handler from 20 sequential GETs to 1 pipelined request.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* test: add structural tests for Redis caching optimizations
18 tests covering: cachedFetchJson request coalescing (in-flight dedup,
cache-before-fetch ordering, cleanup), getCachedJsonBatch pipeline API,
aircraft batch handler pipeline usage, bbox grid quantization (1-degree
step, expanded fetch bbox), and ETF parallel fetch.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: enforce military bbox contract and add behavioral cache tests
---------
Co-authored-by: Elias El Khoury <efk@anghami.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add User-Agent and Cloudflare 403 detection to all secret validation probes (#296)
Sidecar validation probes were missing User-Agent headers, causing
Cloudflare-fronted APIs (e.g. Wingbits) to return 403 which was
incorrectly treated as an auth rejection. Added CHROME_UA to all 13
probes and isCloudflare403() helper to soft-pass CDN blocks.
* fix: open external links in system browser on Tauri desktop (#297)
Tauri WKWebView/WebView2 traps target="_blank" navigation, so news
links and other external URLs silently fail to open. Added a global
capture-phase click interceptor that routes cross-origin links through
the existing open_url Tauri command, falling back to window.open.
* fix: add Greek flag mapping to language selector (#307)
* fix: add missing country brief i18n keys and export PDF option (#308)
- Add levels, trends, fallback keys to top-level countryBrief in en/el/th/vi
locales (fixes raw key display in intelligence brief and header badge)
- Add Export PDF option to country brief dropdown using scoped print dialog
- Add exportPdf i18n key to all 17 locale files
* feat: add day/night solar terminator overlay to map
Add a real-time day/night overlay layer using deck.gl PolygonLayer that
renders the solar terminator (boundary between day and night zones).
The overlay uses astronomical formulas (Meeus) to compute the subsolar
point and trace the terminator line at 1° resolution.
- New toggleable "Day/Night" layer in all 3 variants (full/tech/finance)
- Theme-aware styling (lighter fill on light theme, darker on dark)
- Auto-refresh every 5 minutes with conditional timer (only runs when
layer is enabled, pauses when render is paused)
- Cached polygon computation to avoid recomputing on every render
- i18n translations for all 17 locales
- Updated documentation with new layer entry
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address review feedback — equinox terminator + locale indentation
- Replace safeTanDecl epsilon clamp with proper equinox handling:
when |tanDecl| < 1e-6, draw terminator as vertical great circle
through the poles (subsolar meridian ±90°) instead of clamping
- Fix JSON indentation in all 17 locale files: dayNight and
tradeRoutes keys were left-aligned instead of matching 8-space
indentation of surrounding keys
---------
Co-authored-by: Elie Habib <elie.habib@gmail.com>
Co-authored-by: Elias El Khoury <efk@anghami.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* fix(relay): auto-reconnect on Telegram AUTH_KEY_DUPLICATED and fix IranIntl handle (#539)
- On AUTH_KEY_DUPLICATED (406), disconnect client and set to null so
next poll cycle reconnects fresh — self-heals after competing client dies
- Fix IranIntl → iranintltv (correct Telegram channel handle)
* fix(live-news): add fallback video ID for LiveNOW from FOX channel (#538)
The livenow-fox channel had no fallbackVideoId, relying solely on
YouTube handle lookup which fails intermittently. Added ZvdiJUYGBis
(confirmed live stream) as fallback.
* fix(iran): bump CDN cache-bust to v4 for fresh event data (#544)
100 new events pushed to Redis covering active Iran-Israel-US
conflict theater including Gulf states (UAE, Bahrain, Qatar,
Kuwait, Jordan). Bump ?_v=3 to ?_v=4 to bypass stale CDN.
* fix(telegram): fix ESM import path in session-auth script (#542)
telegram/sessions → telegram/sessions/index.js (same fix as relay)
* fix(telegram): latch AUTH_KEY_DUPLICATED to stop retry spam (#543)
AUTH_KEY_DUPLICATED is permanent — the session string is invalidated
and no amount of retrying will fix it. Previously the relay retried
every 60s, spamming logs. Now it logs a clear error message with
instructions to regenerate the session and stops retrying.
Renamed telegramImportFailed → telegramPermanentlyDisabled to cover
both import failures and auth failures under one latch.
* fix(live-news): fix broken Europe channel handles + add fallback video IDs (#541)
* fix(live-news): fix broken Europe channel handles + add fallback video IDs
- Fix France 24 English handle: @FRANCE24English (404) → @France24_en
- Fix WELT handle: @WELTNachrichtensender (hijacked to "Movie Glow") → @WELTVideoTV
- Add fallbackVideoId for BBC News, France 24 EN, TRT Haber, NTV Turkey,
CNN TURK, TVP Info, Telewizja Republika (verified via Playwright)
- Update stale fallback IDs for Fox News, RTVE, CNN Brasil, C5N, TBS News,
Sky News Arabia, TRT World
* fix(live-news): update CBS News fallback video ID
* fix(live-news): update Newsmax fallback video ID
* fix(live-news): add NBC News fallback video ID
* fix(live-news): full channel audit — fix 10 broken handles + update 8 stale fallbacks
Broken handles fixed:
- Bloomberg: @Bloomberg (404) → @markets
- WION: @WIONews (wrong channel "Write It Out") → @WION
- CTI News: @CtiTv (404) → @中天新聞CtiNews
- VTC NOW: @VTCNOW (404) → @VTCNowOfficial
- Record News: @recordnewsoficial (404) → @RecordNews
- T13: @T13 (404) → @Teletrece
- Channels TV: @channelstv (404) → @ChannelsTelevision
- KTN News: @KTNNewsKE (404) → @ktnnews_kenya
- eNCA: @enewschannel (404) → @eNCA
- SABC News: @SABCNews (404) → @SABCDigitalNews
Stale fallback video IDs refreshed:
- Sky News, NASA, CBC News, CNN Brasil, C5N, TBS NEWS DIG,
Sky News Arabia, TRT World
* feat(oref): add OREF sirens panel with Hebrew-to-English translation (#545)
Add real-time Israel Home Front Command (OREF) siren alerts panel:
- Edge Function proxy at api/oref-alerts.js
- OrefSirensPanel component with live/history views
- oref-alerts service with 10s polling and update callbacks
- Hebrew→English translation via existing translateText() LLM chain
with 3-layer caching (in-memory Map → server Redis → circuit breaker)
- i18n strings for all 23 locales
- Panel registration, data-loader integration, CSS styles
* fix(relay): use execFileSync for OREF curl to avoid shell injection (#546)
Proxy credentials with special characters (semicolons, dollar signs)
were interpolated into a shell command via execSync. Switch to
execFileSync which passes args directly without shell parsing.
* gave the user freedom to resize panels "fixes issue #426" (#489)
* gave the user freedom to resize panles
* feat(panels): add horizontal resize with col-span persistence
* feat(cii): integrate Iran strike events into CII scoring, country brief & timeline (#547)
Iran had ~100 geolocated strike events but the CII was unaware of them:
conflict score stuck at 70 (ACLED only), no strike chip in Active Signals,
timeline conflict lane empty, intelligence brief silent on strikes.
Changes:
- Add strikes[] to CountryData and ingestStrikesForCII() with geo-lookup
fallback (bounding boxes when GeoJSON not yet loaded)
- Boost CII conflict score with 7-day freshness window
(min(50, count*3 + highSev*5))
- Cache iranEvents in IntelligenceCache, preserve across refresh cycles
- Wire data loading: always load Iran events (not gated by map layer),
ingest into CII, trigger panel refresh
- Add activeStrikes to CountryBriefSignals with geo-lookup counting
- Render strike chip in Active Signals and include in fallback brief
- Feed strike events into 7-day timeline (conflict lane)
- Add structured strikeCount/highSeverityStrikeCount fields to GeoSignal
(replaces fragile regex parsing in focal-point-detector)
- Add active_strike signal type to InsightsPanel focal points
- Add bounding-box fallback to signal aggregator for conflict events
- Add i18n keys for activeStrikes
* fix(alerts): add compound escalation for military action + geopolitical target (#548)
Keyword matching was too rigid — "attacks on iran" didn't match CRITICAL
because only "attack on iran" (singular) existed. Headlines like
"strikes by US and Israel on Iran" also missed because words weren't
adjacent.
Added compound escalation: if a HIGH military/conflict keyword matches
AND the headline mentions a critical geopolitical target (Iran, Russia,
China, Taiwan, NATO, US forces), escalate to CRITICAL. Also added
missing Iran keyword variants (plural forms, "Iran retaliates/strikes").
* feat(conflict): enhance Iran events popup with severity badge and related events (#549)
Rewrite the Iran events popup to follow the established popup pattern
(conflict/protest) with severity-colored header, badge, close button,
stat rows, and source link using CSS classes instead of inline styles.
- Add normalizeSeverity helper (clamps unknown values to 'low')
- Show related events from same location (normalized matching, max 5)
- Add IranEventPopupData to PopupData union (removes unsafe double cast)
- Add iranEvent header CSS with severity border-left colors
- Add i18n keys for en/ar/fr
* feat(telegram): add Telegram Intel panel (#550)
* feat(telegram): add Telegram Intel panel consuming relay feed
- Service layer: fetchTelegramFeed() with 30s cache, types matching relay shape
- Panel component: topic filter tabs, safe DOM rendering via h()+replaceChildren()
- DataLoader + RefreshScheduler pattern (60s interval, hidden-tab aware)
- Handles enabled=false and empty states from relay
- CSS following existing gdelt-intel pattern
- Panel title localized across all 18 locales
* fix(i18n): add components.telegramIntel translations to 10 remaining locales
* feat(live-news): add residential proxy + gzip decompression for YouTube detection (#551)
YouTube blocks Vercel datacenter IPs — returns HTML without videoDetails/isLive
data. Switch from edge runtime to Node.js serverless to enable HTTP CONNECT
tunnel proxy via YOUTUBE_PROXY_URL env var. Add zlib decompression for gzip
responses (YouTube returns empty body without Accept-Encoding header).
Also adds missing fallback video IDs for WELT, KTN News, CNA NewsAsia,
and updates TBS NEWS DIG fallback.
* debug(live-news): add debug param to diagnose proxy env var on Vercel
* fix(live-news): set explicit runtime: 'nodejs' for proxy support
Vercel defaults to edge runtime when not specified. node:http/https/zlib
imports are unavailable in edge — causing FUNCTION_INVOCATION_FAILED.
Remove debug param added in previous commit.
* fix(live-news): lazy-load node modules + proxy fallback to direct fetch
Top-level import of node:http/https/zlib crashes if Vercel bundles
for edge despite runtime: 'nodejs' config. Use dynamic import() to
lazy-load at call time. Also add try/catch around proxy so it falls
back to direct fetch if proxy connection fails.
* feat(aviation): integrate AviationStack API for non-US airport delays (#552)
Replace 100% simulated delay data for international airports with real
flight data from AviationStack API. Add 28 Middle East/conflict-zone
airports (Iran, Iraq, Lebanon, Syria, Yemen, Pakistan, Libya, Sudan).
Key changes:
- AviationStack integration with bounded concurrency (5 parallel),
rotating batch (20 airports/cycle), and 20s deadline
- Redis SETNX lock prevents cross-isolate cache stampede on expiry
- Split FAA/intl caches (both 30min TTL) with isolated error handling
- Fix severity colors (was checking 'GS'/'GDP', now minor/moderate/major/severe)
- Fix tooltip (was obj.airport, now obj.name + obj.iata)
- Add FLIGHT_DELAY_TYPE_CLOSURE for airport/airspace closures
- Add closure i18n key across all 18 locales
- Graceful fallback: no API key → simulation; API failure → simulation
* feat(live-news): move YouTube proxy scraping to Railway relay
Vercel serverless cannot use node:http/https for HTTP CONNECT proxy
tunnels. Move the residential proxy YouTube scraping to the Railway
relay (ais-relay.cjs) which has full Node.js access.
- Add /youtube-live route to relay with proxy + direct fetch fallback
- Add 5-min in-memory cache for channel lookups, 1hr for oembed
- Revert Vercel api/youtube/live.js to edge runtime — now proxies to
Railway first, falls back to direct scrape
* feat(settings): add AVIATIONSTACK_API to desktop settings page (#553)
Register the key in Rust keychain (SUPPORTED_SECRET_KEYS), frontend
RuntimeSecretKey type, feature toggle, and settings UI under
"Tracking & Sensing" category.
* fix(live-news): use correct relay auth header for YouTube proxy (#554)
Edge function was sending X-Relay-Auth header with RELAY_AUTH_TOKEN env
var, but the Railway relay expects x-relay-key header validated against
RELAY_SHARED_SECRET. This mismatch caused the relay to reject requests
from Vercel, falling back to direct YouTube scrape (which fails from
datacenter IPs for many channels).
* fix(live-news): align YouTube edge function with relay auth pattern (#555)
Use same getRelayBaseUrl/getRelayHeaders as other edge functions:
- WS_RELAY_URL env var instead of VITE_WS_API_URL
- RELAY_SHARED_SECRET + RELAY_AUTH_HEADER for flexible auth
- Dual x-relay-key + Authorization headers
* fix(i18n): rename OREF Sirens panel to Israel Sirens (#556)
Remove internal implementation references (OREF, proxy relay, oref.org.il)
from all user-facing strings across 18 locales and panel config.
* fix(live-news): annotate empty catches and sanitize error output (#560)
- Add context comments to empty catch blocks for debuggability
- Replace error.message leak with generic client-safe message
* fix(sentry): add noise filters and fix beforeSend null-filename leak (#561)
- Add 8 new ignoreErrors patterns: signal timeout, premium gate,
hybridExecute/mag/webkit bridge injections, postMessage null,
NotSupportedError, appendChild injection, luma assignment
- Fix LIDNotify regex to match both LIDNotify and LIDNotifyId
- Fix beforeSend: strip null/anonymous filename frames so deck.gl
TypeErrors (28 events, 8 users) are properly suppressed
* feat(cii): wire OREF sirens into CII score & country brief (#559)
* feat(cii): wire OREF sirens into CII score and country brief
Active OREF sirens now boost Israel's CII score through two channels:
- Conflict component: +25 base + min(25, alertCount*5) for active sirens
- Blended score: +15 for active sirens, +5/+10 for 24h history thresholds
Country brief for Israel shows a siren signal chip when alerts are active.
* refactor(cii): extract getOrefBlendBoost helper to DRY scoring paths
* fix(relay): add graceful shutdown + poll concurrency guard for Telegram (#562)
- SIGTERM/SIGINT handler disconnects Telegram client before container dies
- telegramPollInFlight guard prevents overlapping poll cycles
- Mid-poll AUTH_KEY_DUPLICATED now permanently disables (was reconnect loop)
* fix(aviation): query all airports instead of rotating batch (#557)
* fix(aviation): query all airports instead of rotating batch of 20
The rotating batch (20 airports/cycle) caused major airports like DXB
(52% cancellations) to be missed entirely for multiple cache cycles.
With a paid AviationStack plan, query all ~90 non-US airports per
refresh with concurrency 10 and 50s deadline (~9 chunks × 5s = 45s).
* feat(cii): feed airport disruptions into CII and country brief
Major/severe airport delays and closures now boost the CII security
score and appear as signal chips in country briefs. Only major+
severity alerts are ingested to avoid noise from minor delays.
- Add aviationDisruptions to CountryData and ingestAviationForCII()
- Boost security score: closure +20, severe +15, major +10, moderate +5
- Store flight delays in intelligenceCache for country brief access
- Add aviation disruptions chip in country brief signals grid
* fix(relay): replace smart quotes crashing relay on startup (#563)
* fix(relay): replace Unicode smart quotes crashing Node.js CJS parser
* fix(relay): await Telegram disconnect + guard startup poll
* fix(cii): resolve Gulf country strike misattribution via multi-match bbox disambiguation (#564)
Dubai/Doha/Bahrain/Kuwait coordinates matched Iran's bounding box first
due to iteration order. Now collects ALL matching bboxes, disambiguates
via isCoordinateInCountry() geometry, and falls back to smallest-area bbox.
- Add BH, QA, KW, JO, OM to bounds tables (previously missing entirely)
- Extract ME_STRIKE_BOUNDS + resolveCountryFromBounds() into country-geometry.ts
- All 4 consumer files use shared constant (single source of truth)
- Bump CDN cache-bust param for iran-events endpoint
* fix(relay): upstreamWs → upstreamSocket in graceful shutdown (#565)
* fix(relay): install curl in Railway container for OREF polling (#567)
* fix(relay): increase Polymarket cache TTL to 10 minutes (#568)
* fix(relay): increase Polymarket cache TTL to 10 minutes
All requests were MISS with 2-min TTL under concurrent load.
Bump to 10-min cache and 5-min negative cache to reduce upstream pressure.
* fix(relay): normalize Polymarket cache key from canonical params
Raw url.search as cache key meant ?tag=fed&endpoint=events and
?endpoint=events&tag_slug=fed produced different keys for the same
upstream request — defeating both cache and inflight dedup, causing
121 MISS entries in 3 seconds.
Build cache key from parsed canonical params (endpoint + sorted
query string) so all equivalent requests share one cache entry.
* feat(webcams): add Iran tab to live webcams panel (#569)
Add dedicated Iran region tab as the first/default tab with 4 feeds:
Tehran, Middle East overview, Tehran (alt angle), and Jerusalem.
* fix(relay): replace nixpacks.toml with railpack.json for curl (#571)
Railway uses Railpack (not Nixpacks). nixpacks.toml in scripts/ was
silently skipped. Use railpack.json at repo root with deploy.aptPackages
to install curl at runtime for OREF polling.
* fix(webcams): replace duplicate Tehran feed with Tel Aviv, rename Iran tab (#572)
- Remove duplicate iran-tehran2 feed (same channel/video as iran-tehran)
- Remove iran-mideast feed
- Add Tel Aviv feed (-VLcYT5QBrY) to Iran Attacks tab
- Rename tab label from "IRAN" to "IRAN ATTACKS" across all 18 locales
* feat(scripts): add Iran events seed script and latest data (#575)
Add seed-iran-events.mjs for importing Iran conflict events into Redis
(conflict:iran-events:v1). Includes geocoding by location keywords and
category-to-severity mapping. Data file contains 100 events from
2026-02-28.
* fix(relay): add timeouts and logging to Telegram poll loop (#578)
GramJS getEntity/getMessages have no built-in timeout. When the first
channel hangs (FLOOD_WAIT, MTProto stall), telegramPollInFlight stays
true forever, blocking all future polls — zero messages collected, zero
errors logged, frontend shows "No messages available".
- Add 15s per-channel timeout on getEntity + getMessages calls
- Add 3-min overall poll cycle timeout
- Force-clear stuck in-flight flag after 3.5 minutes
- Detect FLOOD_WAIT errors and break loop early
- Log per-cycle summary: channels polled, new msgs, errors, duration
- Track media-only messages separately (no text → not a bug)
- Expose lastError, pollInFlight, pollInFlightSince on /status endpoint
* feat(cii): hook security advisories into CII scoring & country briefs (#579)
Travel advisories (Do Not Travel, Reconsider, Caution) from US, AU, UK,
NZ now act as a floor and boost on CII scores. Do Not Travel guarantees
a minimum score of 60 (elevated), Reconsider floors at 50. Multi-source
corroboration (3+ govts) adds +5 bonus.
Advisory chips appear in country brief signal grid with level-appropriate
styling, and advisory context is passed to AI brief generation.
- Extract target country from advisory titles via embassy feed tags and
country name matching
- Add advisoryMaxLevel/advisoryCount/advisorySources to CII CountryData
- Wire ingestAdvisoriesForCII into data loader pipeline
- Add travelAdvisories/travelAdvisoryMaxLevel to CountryBriefSignals
- Render advisory signal chips in CountryBriefPage
* fix(sentry): guard setView against invalid preset + filter translateNotifyError (#580)
- DeckGLMap.setView(): early-return if VIEW_PRESETS[view] is undefined,
preventing TypeError on 'longitude' when select value is invalid
- Add ignoreErrors pattern for Google Translate widget crash
* feat(relay): bootstrap OREF 24h history on startup (#582)
* fix(relay): improve OREF curl error logging with stderr capture
-s flag silenced curl errors. Add -S to show errors, capture stderr
via stdio pipes, and log curl's actual error message inst…
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
@tvpinfo) — Poland's public broadcaster news channel@Telewizja_Republika) — Polish conservative news channeleu) region filterChanges
src/components/LiveNewsPanel.ts— Added channel entries toOPTIONAL_LIVE_CHANNELSand their IDs to the Europe region inOPTIONAL_CHANNEL_REGIONSTest plan