Vitest's default per-test timeout is 5s. That's plenty for tests that
exercise pure functions or even simple JSX, but the heavier React
component trees we render under jsdom — MessagesView, GateView,
Wormhole contact flows — consistently measure 6-10s on GitHub Actions'
shared Node workers under load.
Concrete flake history that drove this bump (none were real product
bugs — all were CI load racing the 5s ceiling on findByText /
waitFor against React reconciliation):
PR #226 messagesViewFirstContact > removes approved contact
PR #237 (same)
PR #261 (same)
PR #262 (same) ← worst: fired on post-merge Docker Publish run,
prevented the AIS SPKI security fix's image from
being published to GHCR until PR #263 cumulatively
re-published it. Real security-fix-shipping risk.
PR #264 fixed messagesViewFirstContact specifically with waitFor
PR #265 messagesViewFirstContact > legacy handle-only addresses
AND gateCompatDecryptUx > browser-local gate runtime
AND failed on the rerun too — confirming the flake
class is broader than the one test we deflaked.
The deflake in PR #264 was too surgical — it addressed one specific
test out of a class of similarly-flaky CI-load-sensitive sites. This
PR addresses the root cause at the config layer instead of playing
whack-a-mole.
Why 15s specifically: 3x the default. Headroom for routine CI slowness
without masking real "test never settles" bugs (those would still
time out, just three rounds later). Individual tests can still pin
their own tighter timeout via the third arg to `it()`.
Also bumps hookTimeout to 15s — beforeEach/afterEach setup for the
same heavier component tests has the same CI-load sensitivity.
User-facing impact: zero. This is dev pipeline infrastructure. End
users never see test timeouts. The cost is theoretical: a buggy test
that genuinely never resolves now takes 15s to declare failure
instead of 5s. In practice that's negligible because the suite runs
once per CI invocation and tests don't usually deadlock.
Validation:
Local full vitest run → 707 passed, 72 files, 10.36s wall clock
(same speed as before — we only changed how long we WAIT for slow
tests, not how fast tests actually run)
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This test asserts that clicking "Remove" on a contact:
1. Surfaces a toast "Removed contact: <name>."
2. Drops the contact from the visible list
The Remove handler in MessagesView dispatches a tight cluster of React
state updates in one event tick:
removeContact(peerId)
locallySavedContactIdsRef.current.delete(peerId)
setContacts(...)
setComposeError('')
setComposeStatus(`Removed contact: ${displayNameForPeer(...)}.`)
Locally those updates settle in <100ms and the toast appears under any
findByText default. Under GitHub Actions runner load — especially the
shared Node.js workers on the "CI Gate / Frontend Tests & Build" step
— the reconcile-and-paint cycle has been measured at ~1.4s, which
exceeds the 1s default findByText timeout.
This is a load-sensitive timing flake, not a real bug — the toast
always renders eventually because the state update chain is purely
synchronous and the displayed text comes from the closure's pre-update
contacts (so the "Remove Me" name is always available when the toast
finally renders).
Historical flake hits in CI on this exact assertion:
PR #226 (zh-CN i18n landing, exposed by i18n parse error)
PR #237 (GitLab mirror parity)
PR #261 (post-#227 audit gap closures)
PR #262 (AIS SPKI pinning — failed post-merge Docker Publish,
skipping image publication for commit 729ea78)
The last one is the worst — a post-merge flake that blocked the
Docker image for an actual security fix from being published. The
subsequent merge of #263 cumulatively re-published the image, but
that's by accident, not by design.
Fix: replace the 1-second findByText with waitFor + 5s timeout +
50ms polling. The 5s ceiling still surfaces a real "toast never
renders" regression with a clear error; it just doesn't get racy
under CI load anymore.
Validation:
Local sequential 10x run of just this test → 10 passed, 0 failed
Full vitest suite → 707 passed, 72 files
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
External audit by @tg12 found three coupled vulnerabilities in the
Next.js admin-auth surface that together let any webpage the operator
visits trigger arbitrary privileged backend calls:
#249/#254 — Cross-origin webpages can have process.env.ADMIN_KEY
injected into their forwarded backend requests just by
issuing fetch('http://localhost:3000/api/wormhole/...')
from a browser tab the operator has open. Full
identity-takeover CSRF.
#255 — When ADMIN_KEY is unset on the server (the default in
.env.example), the admin session route fell through to
GET /api/settings/privacy-profile to "verify" the user-
supplied key. That endpoint is public; it always returns
200 for any X-Admin-Key value. So arbitrary attacker
keys minted full admin session cookies on default
installs.
Both fixes preserve every legitimate UX path. Origin-header gating is
transparent to browser tabs on the dashboard's own host, transparent
to Tauri/native shells (no Origin), and transparent to server-to-
server callers (no Origin). Only cross-origin browser fetches with a
foreign Origin lose the injection.
frontend/src/app/api/[...path]/route.ts
Adds isSameOriginOrNonBrowser() — checks the Origin header against
the request's own Host. Allow if no Origin (native/server-to-
server), allow if Origin host == Host host (same-origin), reject
otherwise. The admin-key injection now requires EITHER a valid
session cookie (auth) OR same-origin-or-non-browser (CSRF guard).
frontend/src/app/api/admin/session/route.ts
verifyAdminKey() simplified to local-only string comparison. When
ADMIN_KEY is configured, the supplied key must match exactly.
When ADMIN_KEY is unset, minting is refused entirely with a clear
message pointing the operator at the backend's auto-trust-loopback
behavior (SHADOWBROKER_TRUST_DOCKER_BRIDGE_LOCAL_OPERATOR=1, the
Docker default — local users keep working without a session).
The previous round-trip to /api/settings/privacy-profile was both
the source of the bug AND useless on its own merits (the endpoint
is public). Removing it makes the validation honest about what
it's checking.
Tests:
frontend/src/__tests__/proxy/proxyAuthBypassChain.test.ts (new, 12)
Cross-origin fetch to sensitive route → no admin-key injection
Cross-origin POST to sensitive route → no admin-key injection
Same-origin fetch → admin-key injection works
No-Origin (server-to-server / native) → admin-key injection works
Valid session cookie on cross-origin → cookie auth wins
Malformed Origin → conservative reject
Non-sensitive routes unaffected
Mint with ADMIN_KEY unset → refused (no fetch happens)
Empty key → 400
Mint with matching ADMIN_KEY → success
Mint with mismatched key → 403
Mint never round-trips to the backend (local-only validation)
frontend/src/__tests__/desktop/adminSessionBoundary.test.ts (updated)
Three tests updated to reflect the new local-only validation
contract. The previous tests asserted fetchMock.toHaveBeenCalled
which validated the now-removed (and broken) backend round-trip.
Full frontend suite: 707 passed, 72 files. No regressions.
Credit: @tg12 for the report. The cross-origin CSRF angle was
non-obvious — they specifically called out that the proxy's
admin-key injection was an open door for any page running in the
operator's browser, which is exactly the right framing.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
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.
Reduce cold-start stalls by raising the default backend memory limit, bounding heavy feed concurrency, preserving non-empty startup caches, and refreshing working news feeds. Fix the Next API proxy for Docker control-plane writes by stripping unsupported hop/body headers and forwarding small request bodies safely. Keep the dashboard dynamic so production users do not get stuck on a cached startup shell.
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.
Render the app shell dynamically so Next can attach per-request CSP nonces to its production scripts, preventing Docker from serving a static shell that cannot hydrate. Also gives the first-contact warmup test enough time in CI.
Skip the Secure flag on the session cookie when the request comes from
a loopback address (localhost, 127.0.0.1, ::1). The Docker image sets
NODE_ENV=production which always enabled Secure, but browsers silently
drop Secure cookies on plain HTTP — breaking the admin panel for
self-hosted users accessing http://localhost:3000.
Fixes#129
- Increase gap between alert boxes from 6px to 12px
- Use weighted repulsion so high-risk alerts stay closer to true position
- Reduce grid cell height for better overlap detection (100→80px)
- Double max iterations (30→60) for dense clusters
- Increase max offset from 350→500px for more spread room
- Fix box height estimate to match actual rendered dimensions
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- 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.