From acf46342a11b2b897cde7e899d42aa7494dd3a68 Mon Sep 17 00:00:00 2001
From: MrAlders0n <55921894+MrAlders0n@users.noreply.github.com>
Date: Fri, 2 Oct 2026 19:04:46 -0400
Subject: [PATCH] Fix issues from the 2.0.0 release review (#137)
---
.build/Caddyfile | 17 ++-
.build/Dockerfile | 9 --
.build/docker-entrypoint.sh | 47 ++++---
.dockerignore | 1 -
.env.example | 8 +-
.github/workflows/ci.yml | 2 +-
.gitignore | 3 -
CONTRIBUTING.md | 15 ++-
DEEPLINK-HANDOFF.md | 80 ------------
README.md | 45 +++++--
docker/.env.example | 6 +-
index.html | 1 +
logo/icon_mobile_live.html | 2 +-
logo/icon_website_live.html | 2 +-
public/config.js | 2 +
src/App.tsx | 6 +-
src/api/client.ts | 20 +--
src/api/ws-manager.ts | 13 +-
src/components/AppShell.tsx | 2 +-
src/components/CopyButton.tsx | 20 ++-
src/components/CopyLinkButton.tsx | 17 ++-
src/components/ErrorBoundary.tsx | 8 +-
src/components/MultiSelectDropdown.tsx | 4 +-
src/components/badge-utils.ts | 5 +
src/features/channels/ChannelList.tsx | 4 +-
src/features/map/MapView.tsx | 12 +-
src/features/map/map-url.ts | 10 +-
src/features/map/types.ts | 8 +-
src/features/map/useMapBorders.ts | 2 +-
src/features/map/useMapLibre.ts | 20 ++-
src/features/map/useMapNeighbors.ts | 2 +-
src/features/map/useMapNodes.ts | 28 ++---
src/features/map/useMapPacketFlow.ts | 2 +-
src/features/observers/ObserverComparison.tsx | 4 +-
src/features/observers/ObserverSidebar.tsx | 8 +-
src/features/observers/observer-navigation.ts | 4 +-
src/features/packets/ObservationCard.tsx | 18 +--
src/features/packets/PacketExpansion.tsx | 2 +
src/features/packets/PacketInvestigation.tsx | 3 +-
src/features/packets/PacketList.tsx | 3 +-
src/features/packets/PathData.tsx | 33 +++--
src/features/packets/packet-investigation.ts | 8 +-
src/features/packets/packet-structure.tsx | 8 +-
src/features/packets/payload-renderers.tsx | 16 +--
src/features/packets/usePackets.ts | 7 +-
src/features/routes/RouteTable.tsx | 6 +-
src/features/stats/MeshTab.tsx | 19 +--
src/features/stats/ObserverTab.tsx | 6 +-
src/features/stats/ScopesTab.tsx | 13 +-
src/features/stats/TalkersTab.tsx | 12 +-
src/features/stats/chartOptions.ts | 6 +
src/features/stats/scopes.ts | 6 +-
src/features/stats/transforms.ts | 15 ++-
src/features/stats/types.ts | 5 +-
src/features/stats/useStats.ts | 66 +++++-----
src/features/stats/useWindowedStats.ts | 14 +--
src/features/traces/TraceDetailPanel.tsx | 30 ++---
src/hooks/useCopyToClipboard.ts | 25 ++++
src/hooks/useInfinitePages.ts | 10 +-
src/hooks/useRegion.tsx | 8 +-
src/i18n/locales/en.json | 2 +-
src/i18n/locales/fr.json | 2 +-
src/lib/constants.ts | 29 +++--
src/lib/runtime-env.ts | 14 +++
src/types/api.ts | 9 +-
.../App.analyzerObservationCarryOver.test.tsx | 10 +-
tests/App.packetUrlContract.test.tsx | 28 ++---
tests/App.viewOnMapDisabled.test.tsx | 61 +++++++++
tests/api/client.test.ts | 11 +-
tests/api/ws-manager.test.ts | 60 +++++++++
tests/components/CopyButton.test.tsx | 25 +++-
tests/components/CopyLinkButton.test.tsx | 24 +++-
tests/components/ErrorBoundary.test.tsx | 17 +++
tests/components/MultiSelectDropdown.test.tsx | 21 ++++
tests/deploy/docker-entrypoint.test.ts | 49 ++++++++
tests/features/map/map-url.test.ts | 11 +-
tests/features/map/useMapLibre.test.tsx | 117 ++++++++++++++++++
.../map/useMapNodes.layer-order.test.tsx | 70 +++++++++++
.../map/useMapNodes.spiderfy.test.tsx | 53 ++++++++
.../observers/ObserverSidebar.test.tsx | 11 +-
.../observers/observer-navigation.test.ts | 7 ++
.../features/packets/ObservationCard.test.tsx | 27 ++++
.../packets/PacketExpansion.mobile.test.tsx | 15 ++-
tests/features/packets/PacketList.test.tsx | 88 +++++++------
.../packets/packet-investigation.test.ts | 20 ++-
.../features/packets/packet-structure.test.ts | 11 ++
.../packets/payload-renderers.test.tsx | 8 ++
tests/features/packets/usePackets.test.tsx | 63 +++++++++-
tests/features/stats/MeshTab.test.tsx | 16 ++-
tests/features/stats/ObserverTab.test.tsx | 1 +
tests/features/stats/ScopesTab.test.tsx | 11 ++
tests/features/stats/TalkersTab.test.tsx | 17 ++-
tests/features/stats/chart-options.test.ts | 13 +-
tests/features/stats/region-gate.test.tsx | 39 ++++++
tests/features/stats/scope-query.test.tsx | 14 +--
tests/features/stats/scopes.test.ts | 7 ++
tests/features/stats/transforms.test.ts | 8 ++
.../features/traces/TraceDetailPanel.test.tsx | 36 ++++++
tests/hooks/useRegion.test.tsx | 52 ++++++++
tests/lib/runtime-env.test.ts | 29 +++++
100 files changed, 1410 insertions(+), 474 deletions(-)
delete mode 100644 DEEPLINK-HANDOFF.md
create mode 100644 public/config.js
create mode 100644 src/hooks/useCopyToClipboard.ts
create mode 100644 src/lib/runtime-env.ts
create mode 100644 tests/App.viewOnMapDisabled.test.tsx
create mode 100644 tests/components/MultiSelectDropdown.test.tsx
create mode 100644 tests/deploy/docker-entrypoint.test.ts
create mode 100644 tests/features/map/useMapLibre.test.tsx
create mode 100644 tests/features/map/useMapNodes.layer-order.test.tsx
create mode 100644 tests/features/map/useMapNodes.spiderfy.test.tsx
create mode 100644 tests/features/stats/region-gate.test.tsx
create mode 100644 tests/features/traces/TraceDetailPanel.test.tsx
create mode 100644 tests/hooks/useRegion.test.tsx
create mode 100644 tests/lib/runtime-env.test.ts
diff --git a/.build/Caddyfile b/.build/Caddyfile
index 63fb30a..f130845 100644
--- a/.build/Caddyfile
+++ b/.build/Caddyfile
@@ -1,7 +1,5 @@
:80 {
root * /srv
- file_server
- try_files {path} /index.html
encode gzip
header {
@@ -10,6 +8,17 @@
Referrer-Policy strict-origin-when-cross-origin
}
- @hashed path /assets/*
- header @hashed Cache-Control "public, max-age=31536000, immutable"
+ # A missing hashed asset is a 404, never the app shell cached for a year
+ handle /assets/* {
+ @exists file
+ header @exists Cache-Control "public, max-age=31536000, immutable"
+ file_server
+ }
+
+ # Revalidate the shell so a deploy's new asset hashes are picked up
+ handle {
+ header Cache-Control "no-cache"
+ try_files {path} /index.html
+ file_server
+ }
}
diff --git a/.build/Dockerfile b/.build/Dockerfile
index dcc8f87..6b87504 100644
--- a/.build/Dockerfile
+++ b/.build/Dockerfile
@@ -3,15 +3,6 @@ WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
-ENV VITE_API_BASE=__VITE_API_BASE__
-ENV VITE_WS_URL=__VITE_WS_URL__
-ENV VITE_MAP_CENTER=__VITE_MAP_CENTER__
-ENV VITE_MAP_ZOOM=__VITE_MAP_ZOOM__
-ENV VITE_DISABLED_TABS=__VITE_DISABLED_TABS__
-ENV VITE_ENABLED_THEMES=__VITE_ENABLED_THEMES__
-ENV VITE_APP_NAME=__VITE_APP_NAME__
-ENV VITE_SKIP_SPLASH=__VITE_SKIP_SPLASH__
-ENV VITE_BANNER=__VITE_BANNER__
RUN npm run build
FROM caddy:2-alpine
diff --git a/.build/docker-entrypoint.sh b/.build/docker-entrypoint.sh
index 9606ed7..7903324 100644
--- a/.build/docker-entrypoint.sh
+++ b/.build/docker-entrypoint.sh
@@ -1,30 +1,29 @@
#!/bin/sh
set -e
-API_BASE="${VITE_API_BASE:-http://localhost:8080/api/v1}"
-WS_URL="${VITE_WS_URL:-ws://localhost:8080/ws}"
-# Optional map view — leave empty when unset; the app falls back to a world overview.
-MAP_CENTER="${VITE_MAP_CENTER:-}"
-MAP_ZOOM="${VITE_MAP_ZOOM:-}"
-# Optional branding/customization — empty means default behaviour (all tabs, hidden themes hidden).
-DISABLED_TABS="${VITE_DISABLED_TABS:-}"
-ENABLED_THEMES="${VITE_ENABLED_THEMES:-}"
-SKIP_SPLASH="${VITE_SKIP_SPLASH:-}"
-# APP_NAME is substituted verbatim; avoid '&' and '|' (sed replacement metachar and delimiter).
-APP_NAME="${VITE_APP_NAME:-BEACON}"
-# Same caveat as APP_NAME, plus no quotes (it lands inside a JS string).
-BANNER="${VITE_BANNER:-}"
+# Runtime config lives in /config.js (served no-cache), not the hashed bundle, so env changes take
+# effect on any restart and reach returning visitors. Empty values fall back to the app defaults.
+OUT="${BEACON_CONFIG_OUT:-/srv/config.js}"
+KEYS="VITE_API_BASE VITE_WS_URL VITE_MAP_CENTER VITE_MAP_ZOOM VITE_DISABLED_TABS VITE_ENABLED_THEMES VITE_APP_NAME VITE_SKIP_SPLASH VITE_BANNER"
-find /srv -name '*.js' -exec sed -i \
- -e "s|__VITE_API_BASE__|${API_BASE}|g" \
- -e "s|__VITE_WS_URL__|${WS_URL}|g" \
- -e "s|__VITE_MAP_CENTER__|${MAP_CENTER}|g" \
- -e "s|__VITE_MAP_ZOOM__|${MAP_ZOOM}|g" \
- -e "s|__VITE_DISABLED_TABS__|${DISABLED_TABS}|g" \
- -e "s|__VITE_ENABLED_THEMES__|${ENABLED_THEMES}|g" \
- -e "s|__VITE_APP_NAME__|${APP_NAME}|g" \
- -e "s|__VITE_SKIP_SPLASH__|${SKIP_SPLASH}|g" \
- -e "s|__VITE_BANNER__|${BANNER}|g" \
- {} +
+# Values are read from ENVIRON and JSON-escaped, so any character is safe.
+awk -v keys="$KEYS" 'BEGIN {
+ for (i = 1; i < 32; i++) esc[sprintf("%c", i)] = sprintf("\\u%04x", i)
+ esc["\\"] = "\\\\"
+ esc["\""] = "\\\""
+ n = split(keys, k, " ")
+ printf "window.__BEACON_CONFIG__ = {"
+ for (i = 1; i <= n; i++) {
+ v = ENVIRON[k[i]]
+ s = ""
+ for (j = 1; j <= length(v); j++) {
+ c = substr(v, j, 1)
+ s = s ((c in esc) ? esc[c] : c)
+ }
+ printf "%s\n \"%s\": \"%s\"", (i > 1 ? "," : ""), k[i], s
+ }
+ printf "\n};\n"
+}' > "$OUT.tmp"
+mv "$OUT.tmp" "$OUT"
exec "$@"
diff --git a/.dockerignore b/.dockerignore
index 1c277d2..548b0ac 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -12,7 +12,6 @@ colour-palettes
!.env.example
.DS_Store
-.claude
.github
.gitignore
.vscode
diff --git a/.env.example b/.env.example
index 800e481..a7a5e45 100644
--- a/.env.example
+++ b/.env.example
@@ -16,15 +16,15 @@
# Options: Packets,Channels,Map,Nodes,Observers,Routes,Traces,Analytics
# VITE_DISABLED_TABS=Map,Traces
-# Reveal normally-hidden themes in the picker (comma list of theme ids). Unset = none.
-# Options: meshmapper_dark,meshmapper_light
+# Only offer these themes in the picker (comma list of theme ids). Unset = the default set.
+# Listing a hidden theme (meshmapper_dark, meshmapper_light) is the only way to enable it.
# VITE_ENABLED_THEMES=meshmapper_dark,meshmapper_light
-# Custom top-left app name (default: BEACON). Avoid the & and | characters.
+# Custom top-left app name (default: BEACON).
# VITE_APP_NAME=Beacon - MeshMapper
# Skip the once-per-session load splash. Unset = shown.
# VITE_SKIP_SPLASH=true
-# Notice shown above the header on every page; [label](url) and bare URLs become links. Unset = none. Avoid &, | and quotes.
+# Notice shown above the header on every page; [label](url) and bare URLs become links. Unset = none.
# VITE_BANNER=This is the Beacon development instance and may be unstable. For everyday use, visit [live.meshcore.ca](https://live.meshcore.ca).
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index a7335cf..f5355c1 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -15,7 +15,7 @@ jobs:
- uses: actions/setup-node@v4
with:
- node-version: 22
+ node-version: 24
cache: npm
- name: Install
diff --git a/.gitignore b/.gitignore
index dda69a2..782f6e5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -24,9 +24,6 @@ build/
coverage/
.references/
-# Claude
-.claude/
-
# OS
.DS_Store
Thumbs.db
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 8d21955..c8f5c50 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -76,7 +76,6 @@ because the root `tsconfig.json` has `files: []`.
- Keep components small and single-purpose
- Comments state the non-obvious *why* in one terse line; a wrong comment is
worse than none
-- See [.claude/CLAUDE.md](.claude/CLAUDE.md) for the full project conventions
---
@@ -111,7 +110,8 @@ docs: expand CONTRIBUTING.md
```
Scopes are optional but helpful. Common scopes mirror the feature folders:
-`packets`, `nodes`, `observers`, `channels`, `map`, `stats`.
+`packets`, `nodes`, `observers`, `channels`, `map`, `routes`, `traces`, `stats`,
+`i18n`.
---
@@ -121,9 +121,11 @@ Scopes are optional but helpful. Common scopes mirror the feature folders:
src/
api/ — typed REST client and WebSocket manager (singleton)
components/ — shared UI primitives (DataTable, DetailPanel, Badge, …)
- features/ — one folder per domain: packets, nodes, observers, channels,
- map, stats — each with its components, types.ts, and hooks
+ features/ — one folder per domain (packets, nodes, observers, channels,
+ map, routes, traces, stats), each with its components,
+ types.ts, and hooks
hooks/ — shared hooks (useRegion, useTheme, useWsHandlers, …)
+ i18n/ — i18next setup and locale catalogs
lib/ — constants, formatters, theme utilities
types/ — REST shapes (api.ts), enums (enums.ts), WS union (ws.ts)
App.tsx — providers, routing, region watcher, WS init
@@ -137,8 +139,9 @@ Key patterns to understand before contributing:
- **Live + historical merge:** the WebSocket pushes into a capped live buffer;
history comes from cursor-paginated `useInfiniteQuery`. A `useMemo` merges and
dedupes by `packetHash`.
-- **Filters are client-side**, applied in a `useMemo` over the cached region
- dataset — they are not part of the query key, so toggling is instant.
+- **Packet filters:** payload-type, route-type and scope filters go to the
+ server and into the query key, so paging covers the full filtered history.
+ The observer filter and hash/path search stay client-side in a `useMemo`.
- **Payload rendering** switches on `parsedPayload.type` in
`features/packets/payload-renderers.tsx`. When the server adds a payload type,
add a matching renderer.
diff --git a/DEEPLINK-HANDOFF.md b/DEEPLINK-HANDOFF.md
deleted file mode 100644
index e9b304f..0000000
--- a/DEEPLINK-HANDOFF.md
+++ /dev/null
@@ -1,80 +0,0 @@
-# Packet deep link investigation — 2026-08-07
-
-## Optional hardening added
-
-A `hash` selection outside the loaded or filtered results now shows an explicit **Open analyzer** action, using the existing detail query. Loading, request errors, retry and dismissal are visible. Loaded selections continue expanding inline, and `analyze=1` remains the flag that opens the drawer. Filters and region selection are preserved. The historical investigation below explains the original behavior.
-
-## Outcome: no bug in beacon-web
-
-A packet deep link that "doesn't pop up" was traced to a hand-built URL missing the
-`analyze=1` flag. The app behaves as designed and its own generated links are correct.
-
-Reported URL (nothing opens):
-
- https://analyzer.meshmapper.net/?iata=YOW&hash=FBF4D4678D849833&tab=Packets
-
-Same URL with the flag (analyzer opens, fully populated):
-
- https://analyzer.meshmapper.net/?iata=YOW&hash=FBF4D4678D849833&tab=Packets&analyze=1
-
-## Why
-
-`hash` alone is not "open this packet" — it's the *expanded row* selector, and the two
-features are deliberately separate.
-
-- `App.tsx:157` — `analyze=1` gates the analyzer drawer:
- `const analyzerHash = searchParams.get("analyze") === "1" ? searchParams.get("hash") : null;`
-- `PacketList.tsx:80` — `hash` expands a row inline: `const expandedHash = searchParams.get("hash");`
-
-Row expansion only works if that row is in the loaded list. The reported packet was ~7h old
-and the list holds the live window (50 packets, minutes), so there was no row to expand and
-nothing rendered.
-
-Both existing comments already say this (`App.tsx:156`, `PacketList.tsx:79`).
-
-## Links the app generates are fine
-
-`PacketAnalyzerDrawer.tsx:75` sets the flag explicitly:
-
-