- Fix URL restore: lat/lon now override view center when explicitly provided
- Fix touch scroll: 8px threshold before drag activation, preventDefault once active
- Add location bootstrap: timezone-first detection, optional geolocation upgrade
- Enable DeckGL on mobile with deviceMemory capability guard
- Add DeckGL state sync on moveend/zoomend for URL param updates
- Fix breakpoint off-by-one: JS now uses <= to match CSS max-width: 768px
- Add country-click on SVG fallback with CSS transform inversion
- Add fitCountry() to both map engines (DeckGL uses fitBounds, SVG uses projection)
- Add SVG inertial touch animation with exponential velocity decay
- Add mobile map e2e tests for timezone, URL restore, touch, and breakpoint
- Fetch and display alert history waves in OrefSirensPanel (cap 50 most recent)
- Last-hour waves highlighted with amber border and RECENT badge
- Translate Hebrew history alerts via existing translateAlerts pipeline
- Guard formatAlertTime/formatWaveTime against NaN from unparseable OREF dates
- Cap relay history bootstrap to 500 records
- Add 3-minute TTL to prevent re-fetching history on every 10s poll
- Remove dead .oref-footer/.oref-history CSS; add i18n key for history summary
- Remove useFallbackOnly from RT channel — RT is HLS-only (banned from
YouTube), so the flag was causing undefined videoId on HLS failure
instead of graceful offline state
- Add response-headers shim to redis-caching test so military flights
bbox tests can import list-military-flights.ts
- Restore LiveNOW from FOX fallbackVideoId (removed in channel audit)
* fix(relay): increase OREF curl maxBuffer to 10MB to prevent ENOBUFS
AlertsHistory.json response exceeds execFileSync default 1MB buffer,
causing spawnSync ENOBUFS on Railway container at startup.
* fix(relay): use curl -o tmpfile for OREF history instead of stdout buffer
Large AlertsHistory.json overflows execFileSync stdout buffer (ENOBUFS).
Now writes to temp file via curl -o, reads with fs.readFileSync, cleans up.
Live alerts (tiny payload) still use stdout path.
Bump circuit breaker name from 'FAA Flight Delays' to 'Flight Delays v2'
to force all clients to discard stale IndexedDB entries that predate
PR #603 (which added normal-ops airport fill). Also downgrade CDN cache
tier from 'slow' (15 min) to 'medium' (5 min) since airport status
changes more frequently than other slow-tier endpoints.
- eNCA: fix YouTube handle from @eNCA to @encanews
- VTC NOW: remove — VTC shut down Jan 2025 (Vietnam govt restructuring)
- CTI News: remove stale fallbackVideoId and useFallbackOnly, let auto-detect work
* fix(aviation): always show MENA airports on map regardless of delay status
MENA airports only appeared when they had active delays/closures.
GCC airports like DOH, AUH, RUH were invisible during normal operations.
Now fills in "normal operations" entries for all MENA airports without
alerts so they always render as gray dots on the flight delays layer.
* fix(aviation): show all monitored airports globally, not just MENA
Extend normal-operations fill to all 128 monitored airports worldwide,
not just the 35 MENA airports. Any airport without active delays now
appears as a gray dot on the flight delays map layer.
- Guard loadVideoById/cueVideoById with typeof check in LiveNewsPanel
(race condition: YT.Player object exists before onReady fires)
- Add ignoreErrors for GM_getValue (Greasemonkey extension) and
InvalidStateError (IndexedDB/DOM state errors from browser internals)
Root cause: ICAO NOTAM API times out from Vercel edge (>10s).
AviationStack alerts indistinguishable from simulation in logs.
Changes:
- Add /notam proxy endpoint to Railway relay (25s timeout, 30min cache)
- Route fetchNotamClosures through relay when WS_RELAY_URL is set
- Fall back to direct ICAO call (20s timeout) when no relay
- Log cache hits with real vs simulated alert counts
- Send all MENA airports in single NOTAM request (was batched by 20)
Requires: ICAO_API_KEY env var on Railway relay
- Add POLYMARKET_MAX_QUEUED=20 cap to prevent unbounded queue growth
under sustained load (rejects with negative cache when full)
- Use requestedLimit to slice cached responses — callers requesting
limit=20 now get 20 items instead of the full 50-item upstream payload
- Hoist PROXY_STRIP_KEYS Set to module level (avoids per-call allocation)
Three issues caused continuous MISS every 5 seconds:
1. Concurrent limit rejection poisoned cache: 11 tags fire via Promise.all
but POLYMARKET_MAX_CONCURRENT=3, so 8 tags got negative-cached with
empty [] (5 min TTL). Those 8 tags NEVER got positive cache because
they were always throttled. Fix: replace reject-with-negative-cache
with a proper queue — excess requests wait for a slot instead of
being silently rejected.
2. Cache key fragmentation: fetchPredictions(limit=20) and
fetchCountryMarkets(limit=30) created separate cache entries for the
same tag. Fix: normalize to canonical limit=50 upstream, cache key
is shared regardless of caller's requested limit.
3. CDN bypass: end_date_min timestamp in query string made every URL
unique, preventing Vercel CDN caching entirely. Fix: strip
end_date_min, active, archived from proxy params (relay ignores them
anyway).
The Redis SETNX lock for AviationStack had a 3s wait but the API takes
~25s for 50 airports. Every lock-loser fell back to simulation — meaning
AviationStack data was never served despite the API key being configured.
Changes:
- Remove fetchIntlWithLock/tryAcquireLock, use getCachedJson/setCachedJson
with conditional TTL (30min real data, 5min simulation fallback)
- Add cancellation severity tiers: ≥20% moderate, ≥10% minor (DXB at 32%
cancelled was previously dropped as null)
- Bump cache key to v2 to invalidate stale simulation data
- Add HTML content-type detection for NOTAM API (ICAO CloudFront returns
HTML challenge pages from Vercel datacenter IPs)
The edge function was the only relay proxy missing RELAY_SHARED_SECRET
auth headers. The relay returns 401 for all non-public routes, so the
panel always received an error response → "No messages available".
- Add RT (rt.com) as optional channel in Europe region
- HLS stream: rt-glb.rttv.com/dvr/rtnews/playlist.m3u8 (CORS: *)
- Enable native <video> HLS playback on web (was desktop-only)
- Channels in DIRECT_HLS_MAP with CORS headers now work everywhere
Railway zero-downtime deploys start the new container before the old one
receives SIGTERM. Both containers connect with the same session string
simultaneously, triggering Telegram's AUTH_KEY_DUPLICATED which permanently
invalidates the session. A 60s startup delay gives the old container time
to disconnect gracefully. Configurable via TELEGRAM_STARTUP_DELAY_MS env.
The composite score ignored theater military buildup and breaking news
alerts, causing misleadingly low scores during active military events.
- Add theater boost from raw asset counts + strike capability (capped +25)
- Add breaking news severity boost (critical=15, high=8, capped +15)
- Listen for wm:breaking-news events with 30-min TTL and auto-expiry
- Read cached theater postures via getCachedPosture() with stale discount
- Lower trend threshold from ±5 to ±3 for faster escalation detection
- Cleanup listeners and timers in destroy()
* feat(aviation): add NOTAM closure detection via ICAO API
Adds international airport closure detection via ICAO NOTAMs:
- New fetchNotamClosures() queries ICAO realtime-notams endpoint
- Detects closures via Q-codes (FA/AH/AL/AW/AC/AM) and text patterns
- Batches airports in groups of 20 per API call
- 4-hour cache TTL via cachedFetchJson (stampede-safe)
- NOTAM closures override existing AviationStack alerts for same airport
- Graceful: no ICAO_API_KEY env var = silently skipped
To activate: set ICAO_API_KEY env var (register at dataservices.icao.int)
* feat(settings): add ICAO_API_KEY to desktop app settings
Adds ICAO NOTAM API key to the desktop settings UI:
- Rust: SUPPORTED_SECRET_KEYS [23→24]
- TypeScript: RuntimeSecretKey + RuntimeFeatureId unions
- Feature definition: 'icaoNotams' in Tracking & Sensing category
- Settings UI: label, signup URL, analytics name
* feat(aviation): limit NOTAM queries to MENA airports only
Per user request, ICAO NOTAM closure detection is scoped to
Middle East airports only (region='mena', ~35 airports).
This reduces API calls (2 batches vs 5) and focuses on the
region where closures are most relevant.
* fix(aviation): align NOTAM cache TTL to 30 min (matching FAA/intl)
* feat: add GPS/GNSS jamming data ingestion from gpsjam.org
- scripts/fetch-gpsjam.mjs: standalone fetcher that downloads daily H3
hex data, filters medium/high interference, converts to lat/lon via
h3-js, and writes JSON. Can be run on cron.
- api/gpsjam.js: Vercel Edge Function that proxies gpsjam.org data with
1hr cache, returns medium/high hexes for frontend consumption.
- src/services/gps-interference.ts: frontend service that fetches from
the Edge API, converts H3→lat/lon, and classifies by conflict region.
- h3-js added as dependency for hex→coordinate conversion.
* feat: add GPS jamming map layer, CII integration, and country brief signals
Wire gpsjam.org data into map visualization, instability scoring, and
country intelligence. ScatterplotLayer renders high (red) and medium
(orange) interference hexes. CII security score incorporates jamming
counts per country via h3→country geocoding with cache. Country briefs
show jamming zone chip. Full i18n across 18 locales including popup
labels. Data loads with intelligence signals cycle (15min), gated by
1hr client-side cache.
* 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 instead of
generic "Command failed" from execFileSync.
* feat(relay): bootstrap OREF 24h history on startup and add missing headers
- Fetch AlertsHistory.json once on startup to populate orefState.history
immediately instead of starting empty
- Add X-Requested-With: XMLHttpRequest header required by Akamai WAF
- Add IST→UTC date converter handling DST ambiguity
- Redact proxy credentials from error logs and client responses
- Fix historyCount24h to count individual alert records, not snapshots
- Guard historyCount24h reducer for both array and string data shapes
- Add input validation to orefDateToUTC for malformed dates
- 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
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
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
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.
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(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.
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(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
* 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
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