# Mesh Map Live: Implementation Notes This document captures the state of the project and the key changes made so far, so a new Codex session can pick up without losing context. Current version: `1.3.0` (see `VERSIONS.md`). ## Overview This project renders live MeshCore traffic on a Leaflet + OpenStreetMap map. A FastAPI backend subscribes to MQTT (WSS/TLS or TCP), decodes MeshCore packets using `@michaelhart/meshcore-decoder`, and broadcasts device updates and routes over WebSockets to the frontend. Core logic is split into config/state/decoder/LOS/history modules so changes are localized. The UI includes heatmap, LOS tools, map mode toggles, and a 24‑hour route history layer. ## Versioning - `VERSION.txt` holds the current version string (`1.3.0`). - `VERSIONS.md` is an append-only changelog by version. ## Key Paths - `backend/app.py`: FastAPI server + MQTT lifecycle and websocket broadcasting. - `backend/config.py`: environment/config constants (shared across backend modules). - `backend/state.py`: shared runtime state (devices/routes/history) + dataclasses. - `backend/decoder.py`: payload parsing, meshcore-decoder integration, route helpers. - `backend/los.py`: LOS math + elevation sampling. - `backend/history.py`: route history persistence + pruning. - `backend/static/index.html`: HTML shell + template placeholders. - `backend/static/styles.css`: UI styles. - `backend/static/app.js`: Leaflet UI, markers, legends, routes, tools. - `backend/static/sw.js`: PWA service worker. - `docker-compose.yaml`: runtime configuration. - `data/state.json`: persisted device/trail/roles/names (loaded at startup). - `data/route_history.jsonl`: rolling 24h route history segments (lines). - `data/neighbor_overrides.json`: optional neighbor override pairs for route disambiguation. - `.env`: dev configuration (mirrors template variables). ## Runtime Commands (Typical Workflow) - `docker compose up -d --build` (run after any file changes). - `docker compose logs -f meshmap-live` (watch MQTT + decode logs). - `curl -s http://localhost:8080/snapshot` (current device map). - `curl -s http://localhost:8080/stats` (counters, route types). - `curl -s http://localhost:8080/debug/last` (recent MQTT decode/debug entries). - `curl -s http://localhost:8080/peers/` (peer counts for a node; uses route history). ## Env Notes (Recent Additions) - `CUSTOM_LINK_URL` adds a HUD link button; blank hides it. - `MQTT_ONLINE_FORCE_NAMES` forces named nodes to show MQTT online and skips them in peers. - `GIT_CHECK_ENABLED`, `GIT_CHECK_FETCH`, `GIT_CHECK_PATH` enable update checks. - `GIT_CHECK_INTERVAL_SECONDS` controls how often the server re-checks for updates. - `ROUTE_MAX_HOP_DISTANCE` prunes hops longer than the configured km distance. - `ROUTE_INFRA_ONLY` limits route lines to repeaters/rooms (companions excluded from routes). - `NEIGHBOR_OVERRIDES_FILE` points at an optional JSON file with neighbor pairs to resolve hash collisions. - Turnstile protection is gated by `PROD_MODE=true` and controlled by: `TURNSTILE_ENABLED`, `TURNSTILE_SITE_KEY`, `TURNSTILE_SECRET_KEY`, `TURNSTILE_API_URL`, and `TURNSTILE_TOKEN_TTL_SECONDS`. - `PROD_MODE`/`PROD_TOKEN` must be passed into the container (compose now forwards them). - Turnstile auth is used for the map page + WebSocket session flow (`meshmap_auth` cookie or `?auth=` on `/ws`), while protected API routes still require `PROD_TOKEN`. - Discord/social embeds can be preserved under Turnstile with `TURNSTILE_BOT_BYPASS` and `TURNSTILE_BOT_ALLOWLIST`. ## MQTT + Decoder - MQTT supports **WebSockets + TLS** or plain TCP. Typical deployments use `MQTT_TRANSPORT=websockets`, `MQTT_TLS=true`, and `MQTT_WS_PATH=/` or `/mqtt`. - Decoder uses Node + `@michaelhart/meshcore-decoder` installed in the container. - `backend/decoder.py` writes a small Node helper and calls it to decode MeshCore packets. ## Frontend UI - Header includes a GitHub link icon and HUD summary (stats, feed note). - Base map toggle: Light/Dark/Topo; persisted to localStorage. - Dark map also darkens node popups for readability. - Node popups do not auto-pan; dragging the map won’t snap back to keep a popup in view. - Legend is collapsible and persisted to localStorage. - HUD is capped to `90vh` and scrolls to avoid running off-screen. - Map start position is configurable with `MAP_START_LAT`, `MAP_START_LON`, `MAP_START_ZOOM`. - Radius filter: `MAP_RADIUS_KM=0` disables filtering; `.env.example` uses `241.4` km (150mi). `MAP_RADIUS_SHOW=true` draws a debug circle. - Default base layer can be set with `MAP_DEFAULT_LAYER` (localStorage overrides). - Units toggle (km/mi) is site-wide; default from `DISTANCE_UNITS` and stored in localStorage. - Node size slider defaults from `NODE_MARKER_RADIUS` and persists in localStorage. - History link size slider defaults from `HISTORY_LINK_SCALE` and persists in localStorage. - Node search (name or key) and a labels toggle (persisted to localStorage). - History tool defaults off and opens a right-side panel with a heat filter slider (visibility is not persisted). - History panel can be dismissed with the X button while keeping history lines visible (toggle History tool to show it again). - History slider modes: 0 = All, 1 = Blue only, 2 = Yellow only, 3 = Yellow + Red, 4 = Red only. - History legend swatch is hidden unless the History tool is active. - Peers tool shows incoming/outgoing neighbors for a selected node, with counts and percentages pulled from route history. - Peers tool skips nodes listed in `MQTT_ONLINE_FORCE_NAMES` (observer listeners). - Peers panel legend clarifies line colors (incoming = blue, outgoing = purple). - Coverage tool only appears when `COVERAGE_API_URL` is set; it fetches tiles on demand. - Trail text in the HUD is only shown when `TRAIL_LEN > 0`; `TRAIL_LEN=0` disables trails entirely. - Hide Nodes toggle hides markers, trails, heat, routes, and history layers. - Heat toggle can hide the heatmap; it defaults on and the button turns green when heat is off. - HUD logo uses `SITE_ICON`; if unset or broken it falls back to a small “Map” badge so the toggle still works. - History line weight was reduced for improved readability. - Propagation overlay keeps heat/routes/trails/markers above it after render; the panel lives on the right and retains the last render until you generate a new one. - Propagation origin markers can be removed individually by clicking them. - Heatmap includes all route payload types (adverts are no longer skipped). - MQTT online status shows as a green marker outline and popup status; it uses `mqtt_seen_ts` from `/status` or `/packets` topics (configurable). - `MQTT_ONLINE_FORCE_NAMES` can force named nodes to show as MQTT online regardless of last seen. - PWA install support is enabled via `/manifest.webmanifest` and a service worker at `/sw.js`. - Preview image (`/preview.png`) renders in-bounds device dots for shared links. - Route lines prefer known neighbor pairs (including overrides) before falling back to closest-hop selection. - Clicking the HUD logo hides/shows the left panel while tool panels stay open. - Share button copies a URL with the current view + toggles (including HUD visibility). - Optional custom HUD link appears when `CUSTOM_LINK_URL` is set. - Update banner shows when `GIT_CHECK_ENABLED=true` and the repo is behind; users can dismiss it per remote SHA. - Update banner dismissal relies on `.hud-update[hidden]` to ensure the banner actually disappears. - URL params override stored settings: `lat`, `lon`/`lng`/`long`, `zoom`, `layer`, `history`, `heat`, `labels`, `nodes`, `legend`, `menu`, `units`, `history_filter`. - Service worker uses `no-store` for navigation requests so env-driven UI toggles (like the radius ring) update without clearing site data. - HUD scrollbars are custom styled in Chromium for a cleaner look. ## LOS (Line of Sight) Tool - LOS elevations are fetched via `/los/elevations` (proxy for `LOS_ELEVATION_URL`) and the LOS/relay/profile math runs client-side for realtime updates (fallbacks still use `/los`). - UI draws an LOS line (green clear / red blocked), renders an elevation profile, and marks peaks. - When blocked, a relay suggestion marker (amber/green) highlights a potential mid-point. - Peak markers show coords + elevation and copy coords on click. - Hovering the profile or the LOS line syncs a cursor tooltip on the profile. - Hovering the LOS profile also tracks a cursor on the map and highlights nearby peaks. - LOS legend items (clear/blocked/peaks/relay) are hidden unless the LOS tool is active. - Shift+click nodes (or long‑press on mobile) or click two points on the map to run LOS. Drag endpoints to update LOS in realtime. - After LOS is locked, click a point marker (A/B) to select it, then click the map to reposition that specific endpoint. ## Device Names + Roles - Names come from advert payloads or status messages when available. - Roles are only accepted from explicit decoder fields: - `deviceRole`/`deviceRoleName` (MeshCore advert flags), or `role` fields in payload. - Name-based role heuristics were removed to avoid mislabels. - Roles are **not** assigned to the receiver of a packet. For decoded packets, the role applies to the **advertised pubkey** (decoded `location.pubkey` or `decoded_pubkey`). - Roles persist to `data/state.json` with `device_role_sources`. Only explicit/override roles are restored on load. - Optional overrides: `data/device_roles.json` can force roles per device_id. ## Routes / Message Paths Routes are drawn when a packet contains a path list (decoder `pathHashes` or `path`). When a hop hash collides, the backend prefers neighbor pairs (or overrides) before falling back to closest-hop selection; oversized path lists are ignored via `ROUTE_PATH_MAX_LEN`. All route hops enforce `ROUTE_MAX_HOP_DISTANCE` to prevent cross‑region jumps. ### 24h History Layer - Every route segment is persisted to `data/route_history.jsonl` and kept for the last `ROUTE_HISTORY_HOURS`. - History lines are color‑coded by volume (blue = low, orange = mid, red = high) and weight scales with counts. - History is hidden by default; the History tool opens a right panel with a slider to filter by heat band. - The History tool also includes a link size slider; it scales line weight without changing counts. - History records route modes from `ROUTE_HISTORY_ALLOWED_MODES` (default: `path`). If routes aren’t visible: - The packet may only include a single hop (`path: ["24"]`). - Other repeaters might not be publishing Path/Trace packets, so a message may not include hops. - Routes and trails drop any `0,0` coordinates (including string values) and will purge bad entries on load. - Route styling uses payload type: 2/5 = Message (blue), 8/9 = Trace (orange), 4 = Advert (green). - If history is empty but routes show, confirm `ROUTE_HISTORY_ALLOWED_MODES` includes the active route mode. ## Frontend Map UI - Legend includes Trace/Message/Advert line styles and Repeater/Companion/Room/Unknown dot colors. - Unknown markers were made more visible (larger, higher contrast gray). - Zoom control moved to bottom-right. - Route lines are thicker/bolder for large screens. - LOS + Propagation panels appear on the right; on mobile they stack to avoid overlap. ## Persistence - Devices, trails, names, and roles are saved to `data/state.json`. - On restart, devices should stay visible if `state.json` exists. - Route history is persisted separately to `data/route_history.jsonl` (rolling window). - If stale/mis-labeled roles appear, delete `data/state.json` or remove role entries. - State load now removes any `0,0` coordinates from devices/trails (including string values). - When `TRAIL_LEN=0`, stored trails are cleared on load and no new trails are written. ## Troubleshooting Notes - If map is empty but MQTT is connected, check `/debug/last` for decoded payloads and `payloadType`. - If markers appear in the wrong place, inspect `decoder_meta` and location fields. - If roles flip incorrectly, verify `role_target_id` in `/debug/last`. - If routes don’t show, verify message hashes appear under multiple receivers in MQTT. - If MQTT online looks wrong, confirm `MQTT_ONLINE_TOPIC_SUFFIXES` in `.env` (default `/status,/packets`). ## Recent Fixes / Changes Summary - Added full WSS support and TLS options. - Integrated meshcore-decoder for advert/location + role parsing. - Added `/stats`, `/snapshot`, `/debug/last`, `/debug/status` endpoints. - Added persistence and state reload logic; safer role restore rules. - Added route drawing for traces/paths/messages with TTL cleanup. - UI: route legend, role legend, and improved marker styles. - Roles now apply to advertised pubkey, not receiver. - Docker restarts are required after file changes (always run `docker compose up -d --build`). - LOS elevations are proxied via `/los/elevations` and LOS/relay computations run client-side (with `/los` fallback). - MQTT online indicator (green outline + legend) and configurable online window. - Filters out `0,0` GPS points from devices, trails, and routes (including string values). - Added 24h route history storage + history toggle with volume-based colors. - Hide nodes now hides heat/routes/history along with markers/trails. - Fixed MQTT disconnect callback signature so broker drops don’t crash the MQTT loop. - Route hash collisions prefer known neighbors (or overrides) before closest-hop selection; long path lists are skipped (`ROUTE_PATH_MAX_LEN`). - First-hop hash collisions now prefer the closest node to the origin to avoid cross-city mis-picks (Issue #11). - Dev-only route debugging: clicking a route line logs hop-by-hop metadata (distances, hashes, origin/receiver) to the browser console when `PROD_MODE=false` (PR #14, credit: https://github.com/sefator). - Trails can be disabled by setting `TRAIL_LEN=0` (HUD trail text is removed). - Node marker size can be tuned via `NODE_MARKER_RADIUS` (users can override locally). - Units toggle defaults from `DISTANCE_UNITS` and persists in localStorage. - Mobile LOS selection supports long-press on nodes. - History tool visibility no longer persists (always off unless `history=on` in the URL). - WebSocket auth accepts `?auth=` for Turnstile-gated sessions. - Protected API routes keep requiring `PROD_TOKEN` even when Turnstile auth is active. - `/api/nodes` now defaults to flat `data` arrays with a top-level `nodes` alias for client compatibility. - `/api/nodes` now applies `updated_since` automatically (use `mode=full` to force full snapshots). - Route IDs are observer-aware (`message_hash:receiver_id`) so multi-observer receptions do not overwrite each other. - `ROUTE_INFRA_ONLY` direct-route checks now allow rendering when at least one endpoint is infrastructure.