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
Individual nodes now render as amber radio tower SVGs with signal waves.
Clusters use a subtle amber glow ring with count label instead of solid
orange blobs. Much less visual clutter against the flight/ship markers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Former-commit-id: 96baa3415440118a6084c739d500a1ce5951d27f
- kiwisdr_fetcher.py imported non-existent `smart_request` (renamed to
`fetch_with_curl`), causing silent ImportError → 0 nodes returned
- Replaced KiwiSDR iframe embed with clean "OPEN SDR RECEIVER" button.
The full KiwiSDR web UI (waterfall, frequency controls, callsign
prompt) is unusable at 288px — better opened in a new tab.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Former-commit-id: aa0fcd92b2390d6a8943b68f2f7eb9b900c7bbb7