172 Commits

Author SHA1 Message Date
Daniel Pupius
1d5e60f7b9 docs(claude): add go test and gofmt to dev commands
CI enforces gofmt; noting it here prevents the same oversight.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-16 17:59:13 +00:00
Daniel Pupius
274eab71c4 chore: go fmt
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-16 17:57:12 +00:00
Daniel Pupius
468e4a1d9d feat(map): tri-state links button with MQTT uplinks mode
Adds a new "uplinks" mode to the network map links toggle, cycling
through links → uplinks → off. Uplinks draws a purple dotted line
from each gateway to every node it has uploaded packets for, making
the MQTT upload graph visible as a separate view from RF topology.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-16 17:55:40 +00:00
Daniel Pupius
bd74327515 Retain router position packets mroe aggressively 2026-03-16 10:47:50 -07:00
Daniel Pupius
26a2bba441 Fix network map mobile overflow and infer MQTT hops from SNR=0 (#2)
* Fix network map mobile overflow and infer MQTT hops from SNR=0

- Add min-h-0 and flex-shrink-0 to map layout so legend/actions don't
  overflow the viewport on mobile
- Infer viaMqtt=true for traceroute hops with SNR exactly 0
- Change MQTT hop color from purple to orange across map, legend, and
  node detail badges

https://claude.ai/code/session_01Ffqq7YPCJE28uUFR88eK7C

* Revert MQTT color to purple; keep SNR=0 MQTT inference

The color change was unintended — MQTT hops should stay purple. The
SNR=0 inference in traceroute processing correctly marks those hops
as viaMqtt so they render as purple dashed lines.

https://claude.ai/code/session_01Ffqq7YPCJE28uUFR88eK7C

* Fix test setup: mock URL.createObjectURL for maplibre-gl

maplibre-gl calls URL.createObjectURL during import for its worker
setup, which doesn't exist in jsdom. Add the mock to test setup.

https://claude.ai/code/session_01Ffqq7YPCJE28uUFR88eK7C

* Run go fmt on unformatted files

https://claude.ai/code/session_01Ffqq7YPCJE28uUFR88eK7C

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-15 23:06:20 -07:00
Daniel Pupius
ededf5c93d Fix paths for docker deployment 2026-03-15 21:42:01 -07:00
Daniel Pupius
501a7be689 Use setarch --addr-no-randomize to fix esbuild crash in Docker
esbuild's Go binary crashes with lfstack.push on kernels with 5-level
paging or high-entropy ASLR (memory mapped above 47-bit addresses).
Disabling ASLR via the personality syscall for the build process keeps
allocations at low addresses without requiring host changes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-16 03:34:28 +00:00
Daniel Pupius
92ffa9219c Switch web builder to node:20-bookworm-slim
Alpine (musl) + esbuild's Go binary crashes with lfstack.push on
high-entropy ASLR kernels. Debian (glibc) uses a different memory
allocator that avoids the issue.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-16 03:31:10 +00:00
Daniel Pupius
4cba63fb94 Fix esbuild crash in Docker build with MALLOC_ARENA_MAX=2
esbuild ships a pre-compiled Go binary that crashes with
"lfstack.push invalid packing" on Linux kernels with 5-level paging
or high-entropy ASLR, which place memory above 47-bit addresses.
Limiting glibc arena count keeps allocations in the lower address range.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-16 03:30:03 +00:00
Daniel Pupius
be2857727f Remove unused Users icon import
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-16 03:27:19 +00:00
Daniel Pupius
c5a0e41216 Upgrade Go to 1.25.5
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-16 03:24:40 +00:00
Daniel Pupius
9d6e74d940 Pin Go builder image to 1.24.1 to match go.mod
golang:1.24-alpine may resolve to 1.24.0 which triggers Go's toolchain
auto-download when go.mod specifies go 1.24.1, failing in restricted
Docker build environments.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-16 03:23:38 +00:00
Daniel Pupius
c54e4a6a21 Improve map and node detail UI
Map:
- Fix __publicField error by setting esbuild target to esnext
- Replace react-map-gl with direct maplibre-gl imperative API across all map components
- Add dark theme styling for MapLibre popup, attribution, and navigation controls
- Add NavigationControl to NetworkMap and NodeLocationMap
- Fix auto-zoom race condition using mapLoaded state
- Fix node click popup being immediately dismissed
- Add MQTT links as separate dashed purple layer
- Reduce node/dot sizes on NetworkMap
- Switch glyph CDN to fonts.openmaptiles.org
- Extend map legend with SNR line color key and MQTT indicator

Node detail:
- Extract shared ConnectionRow and ConnectionList components
- Compact connections table: single row per entry with color-coded SNR, badges, short time
- Apply same compact style to NeighborInfoPacket neighbor list
- Move Connections section into right column below Last Activity
- Split Gateway Node out of Device Information into its own card
- Fold observed node count into "Recently observed nodes (N)" subtext
- Add formatLastSeenShort for compact time display

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-16 03:20:27 +00:00
Daniel Pupius
aee1c50b67 Lighten map nav controls, soften button separator
Use a frosted-glass white tint instead of near-black so controls
don't disappear against the dark map. Replace the bright button
separator with a subtle rgba white border.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-16 00:10:38 +00:00
Daniel Pupius
1d3500082e Dark theme for MapLibre navigation controls, add to NetworkMap
Style .maplibregl-ctrl-group with dark semi-transparent background
and invert the SVG icons to match the dark map aesthetic. Add
NavigationControl to NetworkMap alongside NodeLocationMap.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-16 00:09:31 +00:00
Daniel Pupius
3ca73ae978 NodeLocationMap: reduce default zoom by 1, add navigation controls
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-16 00:08:11 +00:00
Daniel Pupius
5242622107 Fix __publicField error with esnext esbuild target
maplibre-gl v5 bundles reference __publicField and other esbuild
class-field helpers without defining them — a known upstream bug
(maplibre-gl-js issue #6680). Setting esbuildOptions.target and
build.target to 'esnext' tells esbuild to emit native class fields
instead of helper-based transforms, eliminating the missing symbol.

Remove all previous workaround attempts (define rewrite, globalThis
polyfill in index.html).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-16 00:04:23 +00:00
Daniel Pupius
38798de9ca Fix __publicField with vite define + globalThis polyfill
The previous index.html polyfill set window.__publicField but Vite's
dep optimizer pre-bundles in Node.js where that global is never set,
so the error persisted.

Two-part fix:
- vite.config.ts define: rewrite bare __publicField identifiers to
  globalThis.__publicField (a valid entity name esbuild accepts)
- index.html: set globalThis.__publicField using the exact
  Object.defineProperty signature esbuild expects, before modules run

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 23:57:28 +00:00
Daniel Pupius
a7eb73c558 Replace react-map-gl with direct maplibre-gl imperative API
react-map-gl's Source/Layer components were silently dropping layers
even after onLoad gating — likely a compatibility issue with
maplibre-gl v5. Switch all three map components to the imperative API
(new maplibregl.Map, map.on('load', ...), source.setData()) which is
the approach shown in MapLibre's own docs and has no wrapper layer.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 23:50:56 +00:00
Daniel Pupius
19a81a363e Gate map layers behind onLoad to fix missing markers
Source/Layer components mounted before the map style finishes loading
fail silently. Add mapLoaded state + onLoad callback to LocationMap,
NodeLocationMap, and NetworkMap so GeoJSON sources and layers are only
added after the style is ready.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 23:46:03 +00:00
Daniel Pupius
7e173c1b63 Fix NodeLocationMap height and always show center dot
- Pass fullHeight to NodeLocationMap in NodeDetail so it fills the
  h-[400px] wrapper rather than rendering its own h-[300px] and
  leaving dead space below
- Always show the center dot in NodeLocationMap: the previous threshold
  (accuracyMeters < 100) suppressed the dot for typical Meshtastic
  precision values (10-13 bits → 2-10km accuracy), making the map
  appear empty even when a location was known

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 23:43:03 +00:00
Daniel Pupius
8a521d54f2 Right-align MapLibre attribution text
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 23:39:23 +00:00
Daniel Pupius
c969b7930c Style MapLibre attribution to match dark theme
Default attribution renders as a white pill on dark maps.
Override with dark semi-transparent background and muted text,
consistent across all map components.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 23:38:34 +00:00
Daniel Pupius
cb04054d18 Add compact attribution to NodeLocationMap and NetworkMap
Both were missing attributionControl so they rendered MapLibre's
default (full text). Now consistent with LocationMap.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 23:35:44 +00:00
Daniel Pupius
bd09beff40 Fix __publicField polyfill via index.html instead of vite define
esbuild's define only accepts literals/identifiers, not arrow functions.
Inject the polyfill as a plain script tag before the module entry point
so it is available when any pre-compiled dependency uses it.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 23:33:35 +00:00
Daniel Pupius
602a20cfc1 Fix several UI issues in packet stream and maps
- Fix __publicField runtime error from pre-compiled npm packages by
  defining the helper in vite optimizeDeps esbuildOptions
- Compact attribution control on LocationMap thumbnails so it doesn't
  dominate small maps
- Remove duplicate accuracy overlay in MapReportPacket (caption +
  accuracy div both at bottom-0 overlapping); accuracy still shown
  in the KeyValuePair section below
- Fix NodeLocationMap height: min-h-[300px] doesn't give ReactMap a
  resolved height, changed to h-[300px]
- Add short name lookup to TraceroutePacket hops (same pattern as
  NeighborInfoPacket)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 20:37:50 +00:00
Daniel Pupius
8130158c1e feat(cache): priority-based eviction with age protection and Bélády approximation
Replace the flat circular buffer with NodeAwareCache, a smarter eviction
strategy for historical mesh packet data:

- Packets younger than 1 hour are never evicted (recent traffic preserved)
- Under pressure, evict from the lowest-priority type first (neighbor-info
  outlasts node-info; chat messages outlast everything)
- Within a priority tier, evict from the most recently active source node —
  that node will resend soonest, so its old packet is cheapest to lose
  (Bélády approximation; protects flaky/distant node history)
- Node retention window still applies: silent nodes' packets are excluded
  from GetAll and pruned proactively before priority eviction runs

Also:
- Add --cache-retention flag (default 3h) and raise --cache-size default to 5000
- Fix decoder error strings (replace verbose Go errors with short codes)
- Add HTTP security headers middleware to server
- Fix broker dispatchLoop deadlock on source channel close
- Fix make gen-proto scanning web/node_modules for .proto files
- Fix tools target always reinstalling protoc-gen-go (handles stale arch binary)
- Move server port from 8080 to 5446; update Dockerfile, docker-compose, moat.yaml

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 20:23:50 +00:00
Daniel Pupius
d95a74c1d7 refactor(web): replace Google Maps with MapLibre, clean up map components
- Migrate all three map components (Map, GoogleMap, NetworkMap) to MapLibre GL JS
- Extract shared CARTO_DARK_STYLE constants into lib/mapStyle.ts
- Move buildCircleCoords to lib/mapUtils.ts (was duplicated across components)
- Rename exports: Map → LocationMap, GoogleMap → NodeLocationMap
- Remove dead props (width, height, nightMode) from LocationMap interface
- Lazy-mount GL contexts via IntersectionObserver to prevent WebGL exhaustion
- Fix Math.spread RangeError in NetworkMap bounds calculation
- Remove showLinks conditional render in favour of visibility layout property
- Remove cursor state; set canvas cursor style directly on map interactions
- Remove Google Maps API key env vars from .env.example and .env.local
- Move Vite dev server to port 5747 (avoids cached redirect on 3000)
- Fix CORS/404: set VITE_API_BASE_URL="" so browser uses Vite proxy

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 20:22:12 +00:00
Daniel Pupius
6e650a7c57 chore: mark completed todos as complete 2026-03-15 16:45:23 +00:00
Daniel Pupius
e8e1a112ab feat(topology): track mesh node connections and render as map polylines
Add passive topology tracking from four sources with confidence ranking:
- Zero-hop MQTT observations (continuous, per-gateway, highest frequency)
- Traceroute replies (RouteDiscovery, full path + SNR data)
- NeighborInfo packets (self-reported neighbor SNR)
- relay_node inferred links (1-hop packets with known relay)

Backend: add rx_snr (field 62) and rx_rssi (field 63) to meshstream Data
proto; extract from MeshPacket in decoder.go; regenerate Go bindings.

Frontend:
- topologySlice: LinkObservation model, per-direction confidence merge,
  24h TTL pruning, 2000-edge cap, Redux-pure (timestamp from payload)
- aggregatorSlice: add hopsFromGateway to NodeData
- __root.tsx: dispatch processTopologyPacket after each SSE message
- NetworkMap: render polylines colored by SNR (green/yellow/red/gray),
  dimmed at 0.4 opacity for viaMqtt edges; Links toggle button in legend
- NodeDetail: Connections section showing per-edge SNR, source badge,
  viaMqtt badge, and last-seen time

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-03-15 16:43:48 +00:00
Daniel Pupius
8779da32a8 refactor(map): extract useCallback hooks and fix XSS in NetworkMap
Convert plain function declarations inside component body to useCallback
with correct deps, eliminating the eslint-disable-react-hooks banner.
Extract buildMarkerContent to module level (pure, no state/refs).
Reorder fitMapToBounds before resetAutoZoom to avoid TDZ reference.
Replace innerHTML SVG in marker with DOM-API construction.
Replace showInfoWindow innerHTML template with .textContent DOM APIs
to eliminate XSS risk from node name/status fields.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-03-15 16:35:04 +00:00
Daniel Pupius
1d61a89505 fix: remove dead code, SSE error handling, bounded dedup maps
- Remove unused mustParseDuration from main.go
- Replace http.Error calls after SSE headers with plain returns;
  skip bad packets instead of killing the stream on marshal error
- Change processedPackets/seenPackets from boolean to timestamp values
  and prune entries older than 24h on each packet to prevent unbounded
  memory growth in long-running sessions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 16:27:57 +00:00
Daniel Pupius
9e5fd5bcae Add code review findings as todos
Security and architecture review of current codebase. 11 findings:
- 3 P1 (XSS, hardcoded creds, unbounded memory growth)
- 4 P2 (SSE protocol, broker deadlock, NetworkMap architecture, CORS)
- 4 P3 (security headers, error leakage, dead code, binary payload)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 15:41:24 +00:00
Daniel Pupius
3e24e6aea4 Add node topology tracking design spec
Covers link inference from traceroute, zero-hop MQTT, NeighborInfo,
and relay_node fields. Includes multi-gateway correlation, hop bounding,
SNR-colored polyline rendering on the map, and node detail connections.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 15:31:27 +00:00
Daniel Pupius
e69fad97cb Add moat config 2026-03-14 20:30:11 -07:00
Daniel Pupius
69a31ca406 Split routers and extend mesh traffic retention (#1)
* Split routers on dashboard and extend retention periods

- Add Router node type with 12-hour stale timeout (vs 30 min for regular nodes)
- Create RouterList component to display router nodes separately
- Update NodeList to filter out routers (similar to gateways)
- Add yellow color scheme for router nodes to distinguish from gateways (green) and nodes (blue)
- Extend mesh traffic retention across the board:
  - Client-side packet age filter: 12h → 24h
  - Broker cache size: 50 → 200 packets
  - Messages per channel: 100 → 500

* Update regular node activity threshold to 60 minutes

* Fix TypeScript errors in router filtering logic

* Fix docker-build to use buildx explicitly with --load flag

* Add default empty value for MESHSTREAM_GOOGLE_MAPS_API_KEY in docker-build

* Restructure docker buildx command to fix path argument parsing

* Remove trailing backslash before build context path in docker-build

* Quote build args and separate path argument in docker-build
2026-01-06 12:25:34 -08:00
Daniel Pupius
245911a450 Remove duplicative block 2025-07-03 14:09:20 -07:00
Daniel Pupius
88dd1fc663 Tweak gateway visualization and card headers 2025-07-03 12:38:23 -07:00
Daniel Pupius
f8c0e0d591 Render admin packets differently 2025-07-03 11:08:09 -07:00
Daniel Pupius
dc36070355 Better error cards for private messages, neighbor info rendering 2025-06-23 09:47:14 -07:00
Daniel Pupius
a83f4feddb Longer test timeout for broker test 2025-05-22 14:37:57 -07:00
Daniel Pupius
bc3104f59c go fmt 2025-05-22 14:35:02 -07:00
Daniel Pupius
ee20a1ea46 Better loading state for node page 2025-05-22 14:32:51 -07:00
Daniel Pupius
61a83bafca Better retry and reconnection logic 2025-05-21 14:35:10 -07:00
Daniel Pupius
0feb2591ef Remove stats listener since it is noisy in prod 2025-05-19 11:00:10 -07:00
Daniel Pupius
26ebc2a6f5 Ignore old packets 2025-05-07 11:32:44 -07:00
Daniel Pupius
fc72e9f65a Mobile layout tweaks 2025-05-05 09:42:19 -07:00
Daniel Pupius
15e6047708 Color and layout tweaks 2025-05-05 09:12:51 -07:00
Daniel Pupius
84f8cdfe5c MQTT SetOrderMatters(false) 2025-05-04 16:12:32 -07:00
Daniel Pupius
df7df6dbd6 Unique client id and fix graceful shutdown 2025-05-04 13:26:13 -07:00