Reported by @tg12 in the external security/correctness audit.
Before this change, /api/live-data/{fast,slow} accepted s/w/n/e query
params but their Query() descriptions explicitly said "(ignored)". The
endpoints shipped the full in-memory world dataset on every poll:
/api/live-data/fast → 16.88 MB
/api/live-data/slow → 10.12 MB
── 27 MB per poll cycle, regardless of zoom
For a node with N operators each polling at the steady 15s/120s cadence,
this is hundreds of MB/minute of outbound traffic that never gets used —
the GPU just culls everything outside the viewport client-side. On a
Tor-bridged or LTE-backed node, that bandwidth bill is the actual cost.
This change makes the existing s/w/n/e params honored — when all four
bounds are supplied, the backend bbox-filters a curated set of heavy,
density-driven, time-sensitive collections to that viewport (with the
existing 20% padding from _bbox_filter):
/fast: commercial_flights, military_flights, private_flights,
private_jets, tracked_flights, ships, cctv, uavs, liveuamap,
gps_jamming, sigint, trains
/slow: gdelt, firms_fires, kiwisdr, scanners, psk_reporter
Static reference layers (satellites, datacenters, military_bases,
power_plants, satnogs, weather, news, stocks, etc.) deliberately STAY
world-scale so panning never reveals an "empty world" of infrastructure.
That preserves the no-hostile-UX feel of the existing dashboard.
Behavior contract:
* Without bbox params (or with a partial bbox), the response is
byte-for-byte identical to the pre-#288 implementation. No
behavior change for any existing caller that hasn't opted in.
* World-scale bbox (lng_span >= 300 or lat_span >= 120) short-circuits
filtering and shares the global ETag — zoomed-out operators all
hit the same 304 cache exactly like before.
* ETag now mixes a 1°-quantized bbox suffix when filtering engages,
so two viewports never poison each other's 304 cache. Sub-degree
pans land in the same ETag bucket (i.e. don't bust the cache on
every mouse drag).
Polling cadence, rate-limit windows, and the 304 short-circuit are all
unchanged. Only the SIZE of the responses changes, and only when the
caller opts in via bounds.
Frontend wiring: useViewportBounds reuses the same coarsened/
expanded bounds it already computes for the AIS /api/viewport POST and
pushes them into a new module-level liveDataViewport store.
useDataPolling reads from that store via appendLiveDataBoundsParams
when building each live-data URL.
Tests cover: no-bbox → world data; bbox → heavy layers filtered;
bbox → reference layers untouched; world-scale bbox → no filter;
partial bbox → treated as no bbox; ETag changes with bbox; sub-degree
pan → same ETag; 304 path works; antimeridian-crossing bbox handled.
Co-authored-by: BigBodyCobain <moatbc@gmail.com>
Background
==========
PR #285 set up the seed -> cache -> GDELT model for the carrier tracker
to address audit issues #244/#245/#246. The GDELT half of that pipeline
hits api.gdeltproject.org's doc API for headline-region keyword
matching -- low precision (false centroid positions per #245) AND
unreliable (the host times out from some networks, including Docker
Desktop on Windows).
USNI publishes a weekly Fleet & Marine Tracker with explicit prose like:
"The Gerald R. Ford Carrier Strike Group is operating in the Red Sea"
"Aircraft carrier USS George Washington (CVN-73) is in port in
Yokosuka, Japan"
That is a strictly better source for U.S. Navy carrier positions:
authoritative, deterministically parseable, weekly cadence.
What this PR does
=================
New module: backend/services/fetchers/usni_fleet_tracker.py
- Pulls USNI's WordPress RSS feeds (site-wide + category, unioned).
- Picks the most recent fleet-tracker post by parsed pubDate.
- For each carrier in the registry, scans the article body for
"is operating in / is in port in / returned to / transiting" near
the carrier's name, hull code, or "<name> Carrier Strike Group"
variant. Captures the region/port phrase that follows.
- Maps the region phrase to coordinates via the existing
REGION_COORDS table, with a USNI-phrase alias table for the
specific wording USNI uses ("Yokosuka, Japan", "Norfolk, Va.",
"Naval Station San Diego", "5th Fleet AOR", etc.).
- Returns {hull: position_entry} with position_confidence="recent"
and position_source_at = the article's actual publication
timestamp (not now()).
Politeness
----------
Uses outbound_user_agent("usni-fleet-tracker") so USNI sees a
per-install Shadowbroker identifier (Round 7a / PR #292). The
article body pages return 403 to non-browser UAs; the WordPress RSS
feed serves the full <content:encoded> body and is the supported
aggregator path. No browser UA spoofing.
carrier_tracker.update_carrier_positions() now runs three phases:
1. Bootstrap from cache (or seed on first run).
2. USNI fleet tracker -- PRIMARY high-confidence source.
3. GDELT -- SECONDARY backfill; can NOT demote a "recent" USNI
position to an "approximate" GDELT headline match.
Verified live: 6 of 11 carriers picked up real May 18, 2026 positions
on first refresh (Eisenhower, Ford, Bush, Roosevelt, Lincoln,
Washington). The other 5 weren't mentioned in this week's article
(they're in port at homeports with no deployment changes) and kept
their cache entries -- which is the correct seed/cache contract from
PR #285.
Other small fixes bundled in
============================
docker-compose.yml: add the 6 third-party-fetcher opt-in env vars
(PREDICTION_MARKETS_ENABLED, FINANCIAL_ENABLED, FIMI_ENABLED,
NUFORC_ENABLED, NEWS_ENABLED, CROWDTHREAT_ENABLED). They were
documented in .env.example but never wired through compose, so setting
them in .env had no effect.
frontend/src/components/TopRightControls.tsx: fix 6 broken i18n keys
that were showing as raw "terminal.term1" / "terminal.cleanupDetail" /
"node.soloReady" placeholders in the INFONET TERMINAL modal. The
translation files have these strings under different key names; the
component now calls the right ones. Full-file sweep confirmed every
other t('...') key in the whole frontend resolves cleanly.
Wikimedia's User-Agent policy asks API clients to identify themselves
with a stable, contactable identifier so their operators can rate-limit
or coordinate. Before this change, ShadowBroker was sending:
- Backend (region_dossier.py): generic project default UA only; no
Api-User-Agent.
- Frontend (useRegionDossier.ts, WikiImage.tsx, NewsFeed.tsx): zero
identifying header at all; three separate copy-pasted anonymous
fetches with their own module-local caches.
Three separate components doing the same broken thing meant policy
fixes had to happen in three places, with no shared cache or kill
switch.
Fix (no UX change, zero hostility):
== Backend ==
`backend/services/region_dossier.py` now sets explicit `User-Agent` +
`Api-User-Agent` headers on every outbound Wikidata and Wikipedia
request via a new `_WIKIMEDIA_REQUEST_HEADERS` constant. The identifier
includes a contact path (issues page on the public GitHub repo).
== Frontend ==
New shared helper `frontend/src/lib/wikimediaClient.ts`:
- `fetchWikipediaSummary(title)` — single source of truth for Wikipedia
REST summary lookups, with one shared LRU cache (in-flight requests
deduplicated, 512-entry cap), `Api-User-Agent` on every fetch.
- `fetchWikidataSparql(query)` — same shape for Wikidata SPARQL.
- `WIKIMEDIA_API_USER_AGENT` — exported constant; one place to update
if Wikimedia ever asks us to back off.
Refactored three components to use the shared client:
- `frontend/src/hooks/useRegionDossier.ts` — fetchLeader() and
fetchLocalWikiSummary() now route through the shared helpers.
- `frontend/src/components/WikiImage.tsx` — uses fetchWikipediaSummary,
proper React state instead of module-mutation + forceUpdate trick.
- `frontend/src/components/NewsFeed.tsx` — same shape.
UX: byte-for-byte identical. Same thumbnails, same dossier content,
same load behavior. The only observable difference is the outgoing
request header.
Note on #239 (route duplication): an audit-grade inventory shows 166
main.py routes are shadowed by router modules. That cleanup is too
large to land safely in this PR; it will be staged as a separate
ladder of small PRs grouped by router module.
Tests:
- `backend/tests/test_region_dossier_wikimedia_ua.py` — 3 tests
asserting backend headers are present.
- `frontend/src/__tests__/utils/wikimediaClient.test.ts` — 9 tests
covering Api-User-Agent presence, shared cache, concurrent
deduplication, disambiguation/HTTP-error/network-error fallthroughs,
empty-input safety.
Local: backend 76/76 security suite green, frontend 716/716 vitest
suite green.
Credit: tg12 (external security audit).
PR #226 landed the i18n infrastructure and Chinese (zh-CN) translations.
This follow-up adds the safeguards that make accepting community
translations sustainable without exposing the project to subtle
state-aligned framing in future translation PRs.
Changes:
frontend/src/i18n/index.tsx (renamed from .ts)
- Add LOCALES registry: a single source of truth for available
languages and their NATIVE display names ("English", "中文 (简体)").
Adding a new language is now a one-entry change here plus a
JSON file.
- Add isLocale() guard so an unknown value in localStorage falls
through to navigator.language detection instead of corrupting
state.
- File renamed to .tsx because it contains JSX. Next.js tolerated
JSX in .ts but Vite/Oxc (used by vitest) does not.
frontend/src/components/SettingsPanel.tsx
Add a UI language picker to the Settings header — a small <select>
populated from LOCALES. Users no longer need the dev console to
switch languages. Locale change remains 100% client-side
(localStorage), no network call, no telemetry.
CONTRIBUTING.md (new)
Documents the translation-neutrality requirement that applies
symmetrically to all source countries:
- Translations must be technically faithful to the English source.
- Substitutions aligned with state propaganda from ANY country
(PRC, Russia, US, EU, etc.) will be rejected.
- The test is: "would a translator working strictly from the
English source produce this rendering?"
Also explains how translation PRs are reviewed and how to add
a new language.
.github/CODEOWNERS (new)
Auto-requests maintainer review on:
- /frontend/src/i18n/ (translation safety)
- /backend/auth.py, /backend/routers/wormhole.py,
/backend/services/mesh/, /backend/services/fetchers/
(the same paths recent security audits flagged as sensitive)
- /.github/workflows/, /.gitlab-ci.yml, /docker-compose*.yml,
/helm/ (build/deploy)
- /CONTRIBUTING.md, /.github/CODEOWNERS (policy itself)
frontend/src/__tests__/i18n/i18nProvider.test.tsx (new, 8 tests)
Locks in the i18n contract:
- LOCALES has both en and zh-CN with non-empty native labels
- Default English when navigator is English
- Auto-detect zh-CN when navigator language starts with "zh"
- localStorage preference overrides auto-detect
- setLocale persists to localStorage
- Unknown stored locale falls back to auto-detect
- Renders a real zh-CN translation (catches large-scale
translation removal in future PRs)
- Missing key falls back to the key itself
Note: i18n/index.tsx, the language toggle UI, the translation
policy, and the test suite together form a defense-in-depth setup.
The structural safety guarantee (no network calls, static JSON
bundled at build) is intact; this PR makes the social contract
around translations explicit and enforceable via branch
protection on CODEOWNERS-marked paths.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Introduce a lightweight i18n system with auto-detection of browser
language and localStorage persistence. Add complete Chinese translations
for all major UI sections: navigation, controls, update dialogs, node
activation, terminal launcher, data layers, settings, filters, and more.
Technical terms (Wormhole, Infonet, Mesh, Shodan, SAR, etc.) are
intentionally kept in English. Falls back to English when Chinese
translation is not found.
Co-authored-by: wangsudong <wangsudong@kylinos.cn>
Each alert toast had a 5-second auto-dismiss timer that fired even
while the user was reading the card. This adds pause-on-hover: the
dismiss timer stops while the mouse is over a toast and restarts (full
lifetime) on mouse leave. The progress bar animation pauses with it,
so the visual matches the actual remaining time.
All other behavior is preserved: same cyber/mono styling, same spring
slide-in, same risk-color border + glow, same warning icon, same
LVL X/10 readout, same title/source layout, same click-to-fly + dismiss
on body click, same × dismiss button.
Implementation notes:
- Extract a ToastCard sub-component so each card can own its own
paused state (useState can't be array-indexed in the parent).
- Move the auto-dismiss timer out of useAlertToasts.ts and into
ToastCard. The hook previously scheduled the dismiss itself, which
meant the UI couldn't pause it — only the component knows whether
the user is interacting.
- Add tests covering: title/source/severity render, auto-dismiss
fires at 5s, hover pauses indefinitely, mouse-leave restarts the
full lifetime, × dismisses without flying, body-click flies +
dismisses.
This implements the genuine UX improvement that PR #234 was reaching
for, without #234's broken syntax, missing-field bug, duplicate
timer logic, or design regression.
Refs: #234
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Allow local-operator DM invite import without requiring a full admin session.
Prioritize bundled/bootstrap seed peers and shorten stale seed cooldowns for faster Infonet recovery.
Replace raw DM invite dumps with copyable signed-address controls, contact request handling, and safer sealed-send behavior while the private delivery route connects.
Ship the v0.9.79 runtime refresh with transport lane isolation, Infonet secure-message address management, MeshChat MQTT controls, selected asset trail behavior, telemetry panel refinements, onboarding updates, and desktop/package metadata alignment.
Also ignore local graphify work products so analysis folders do not leak into future commits.
Add Tor/onion runtime wiring and faster Infonet node status refresh.
Keep node bootstrap state clearer across Docker and local runtimes.
Use selected aircraft trail history for cumulative tracked-aircraft emissions.
Let fresh Docker and local installs enter OpenSky, AIS, and other provider keys directly in onboarding or Settings without manually creating .env files. Persist keys server-side in the backend data store, keep them write-only from the browser, reload runtime settings, and retain local-operator access controls.
Allow the bundled Docker frontend proxy to reach local-operator endpoints through the private compose bridge without trusting LAN clients. This restores Time Machine, MeshChat key creation, AI pins/layers, and related local controls in Docker installs. Refresh first-run guidance so Docker users know to configure OpenSky and AIS keys through .env.
- Add Server-Sent Events endpoint at GET /api/mesh/gate/stream that
broadcasts ALL gate events to connected frontends (privacy: no
per-gate subscriptions, clients filter locally)
- Hook SSE broadcast into all gate event entry points: local append,
peer push receiver, and pull loop
- Reduce push/pull intervals from 30s to 10s for faster relay sync
- Add useGateSSE hook for frontend EventSource integration
- GateView + MeshChat use SSE for instant refresh, polling demoted
to 30s fallback
Latency: same-node instant, cross-node ~10s avg (was ~34s)
- Add FINNHUB_API_KEY to docker-compose.yml so financial ticker works
in Docker deployments
- Update default layer config: planes/ships ON, satellites only for
space, no fire hotspots, military bases + internet outages for infra,
all SIGINT except HF digital spots
- Add MapLibre native clustering to APRS markers (matches Meshtastic)
with cluster radius 42, breaks apart at zoom 8
- Derive gate envelope AES key from gate ID via HKDF so all nodes
sharing a gate can decrypt each other's messages (was node-local)
- Preserve gate_envelope/reply_to in chain payload normalization
- Bump Wormhole modal text from 9-10px to 12-13px
- Add aircraft icon zoom interpolation (0.8→2.0 across zoom 5-12)
- Reduce Mesh Chat panel text sizes for tighter layout
paho-mqtt was missing from pyproject.toml, causing the Meshtastic MQTT
bridge to silently disable itself in Docker — no live chat messages
could be received. Also improve Infonet node status labels: show
RETRYING when sync fails instead of misleading SYNCING, and WAITING
when node is enabled but no sync has run yet.
- require_local_operator now recognizes Docker bridge network IPs
(172.x, 192.168.x, 10.x) as local, fixing "Forbidden — local operator
access only" when frontend container calls wormhole/mesh endpoints
- Bumped all changelog modal text from 8-9px to 11-13px for readability
The self-updater extracted files inside the container but Docker restarts
from the original image, discarding all changes. Now detects Docker via
/.dockerenv and returns pull commands for the user to run on their host.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Gate messages now propagate via the Infonet hashchain as encrypted blobs — every node syncs them
through normal chain sync while only Gate members with MLS keys can decrypt. Added mesh reputation
system, peer push workers, voluntary Wormhole opt-in for node participation, fork recovery,
killwormhole scripts, obfuscated terminology, and hardened the self-updater to protect encryption
keys and chain state during updates.
New features: Shodan search, train tracking, Sentinel Hub imagery, 8 new intelligence layers,
CCTV expansion to 11,000+ cameras across 6 countries, Mesh Terminal CLI, prediction markets,
desktop-shell scaffold, and comprehensive mesh test suite (215 frontend + backend tests passing).
Community contributors: @wa1id, @AlborzNazari, @adust09, @Xpirix, @imqdcr, @csysp, @suranyami,
@chr0n1x, @johan-martensson, @singularfailure, @smithbh, @OrfeoTerkuci, @deuza, @tm-const,
@Elhard1, @ttulttul
The UV install conditional was never closed, which caused 'unexpected
end of file' from bash -n and broke the macOS/Linux startup path.
Document in ChangelogModal BUG_FIXES (2026-03-26).
Made-with: Cursor
- Add 5 native ingestors to cctv_pipeline.py: DGT (~1,917 cameras),
Madrid (~357), Málaga (~134), Vigo (~59), Vitoria-Gasteiz (~17)
- Fix DGT DATEX2 parser to match actual XML schema (device elements,
not CctvCameraRecord)
- Wire all new ingestors into the scheduler via data_fetcher.py
- Remove standalone spain_cctv.py by Alborz Nazari, replaced by native
pipeline ingestors that integrate with the existing scheduler pattern
- Fix CCTV image loading for servers with Referer-based hotlink
protection (referrerPolicy="no-referrer")
- Replace external via.placeholder.com fallbacks with inline SVG data
URIs to avoid dependency on unreachable third-party service
- Surface source_agency attribution in CCTV panel UI for open data
license compliance (CC BY / Spain Ley 37/2007)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Map ~35,000 power generation facilities from 164 countries using the
WRI Global Power Plant Database (CC BY 4.0). Follows the existing
datacenter layer pattern with clustered icon symbols, amber color
scheme, and click popups showing fuel type, capacity, and operator.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Merge both feature sets: keep JSDF bases (gsdf/msdf/asdf branches) from
PR #77 and East Asia adversary bases (missile/nuclear branches) from main.
Union all branch types in tests and MaplibreViewer labels.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add ASDF (8), MSDF (6), and GSDF (4) bases to military_bases.json.
Colocated bases (Misawa, Yokosuka, Sasebo) have offset coordinates
to avoid overlap with existing US entries. Add branchLabel entries
for GSDF/MSDF/ASDF in MaplibreViewer popup.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add 68 military bases (PLA, Russia, DPRK, ROC, Philippines, Australia)
with data-driven color coding (red/blue/green) on the map
- Add 6 news RSS feeds (Yonhap, Nikkei Asia, Taipei Times, Asia Times,
Defense News, Japan Times) and 15 geocoding keywords for islands,
straits, and disputed areas
- Extend ICAO country ranges for Russia, Australia, Philippines,
Singapore, DPRK and add Russian aircraft classification (fighters,
bombers, cargo, recon)
- Create PLAN/CCG vessel enrichment module (90+ ships) following
yacht_alert pattern for automatic MMSI-based identification
- Update frontend types and popup styling for adversary/allied/ROC
color distinction
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Infer country and military force (PLA, JSDF, ROK, ROC) from ICAO hex
address blocks when the flag field is Unknown. Extract and extend aircraft
model classification to cover East Asian fighters, cargo, recon, and
tanker types with hyphen-normalized matching.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add 18 US military bases (Japan, Guam, South Korea, Hawaii, Diego Garcia)
as a toggleable map layer. Follows the existing data center layer pattern:
static JSON → backend fetcher → slow-tier API → frontend GeoJSON layer.
Includes red circle markers with labels, click popups showing operator
and branch info, and a toggle in the left panel.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Auto-update POST goes through Next.js proxy which dies when extracted
files trigger hot-reload. Network drops now transition to restart polling
instead of showing failure. Also adds admin key header and FastAPI error
field fallback. Gitignore updated to protect internal docs.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Former-commit-id: 03162f8a4b7ad8a0f2983f81361df7dba42a8689
selectedEntity.id was stored as a numeric array index into data.gdelt[]
and data.news[]. After any data refresh those arrays rebuild, so the
stored index pointed to a different item — showing wrong popup content.
GDELT features now use g.properties?.name || String(g.geometry.coordinates)
as a stable id; popups resolve via find(). News popups resolve via find()
matching alertKey. ThreatMarkers emits alertKey string instead of originalIdx.
ThreatMarkerProps updated: id: number → id: string | number.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Former-commit-id: c2bfd0897a9ebd27e7c905ea3ac848a89883f140
interpFlight, interpShip, and interpSat were plain arrow functions
recreated on every render. Because interpTick fires every second,
TrackedFlightLabels received a new function reference every second
(preventing memo bailout) and all downstream useMemos closed over
these functions re-executed unnecessarily.
Wrap all three in useCallback([dtSeconds]) — dtSeconds is their
only reactive closure variable; interpolatePosition is a stable
module-level import and does not need to be listed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Former-commit-id: 84c3c06407afa5c0227ac1b682cca1157498d1a5
Adds subtle amber glow circles behind both cluster and individual
tower markers for a pulsing radar-station effect.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Former-commit-id: bf6cee0f3b468006356fd95dcf83a27d5e62e5f6
Replaced the circle cluster layer with a symbol layer using the same
radio tower icon. Clusters show the tower with a count label below.
No more orange blobs at any zoom level.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Former-commit-id: 0b1cb0d2a082dde4dcefe12518cdfb28b492ab89