- Load terrain source and hillshade dynamically (only when 3D mode is on)
to prevent tile loading at idle, fixing low-zoom WebGL crash
- Restore terrain correctly on page reload by applying inside map load handler
- Clean up source, hillshade, and sky layers fully when terrain is toggled off
- Raise minZoom to 6 to prevent crash at extreme zoom-out
- Reduce terrain exaggeration to 3x as a stable middle ground
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add SRTM raster-dem terrain source (AWS Terrarium tiles) to map style
- Add hillshade layer always visible for terrain relief in 2D and 3D
- Add sky layer for atmosphere effect in 3D mode
- Replace Links filter row with 3D Terrain toggle in the Layers panel
- Lock map pitch to 0 by default; terrain toggle unlocks and tilts to 45°
- Wire showTerrain prop through Filters → App → MapLibreMap via useEffect
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Lazy path resolver: geographic hop reconstruction from path_hashes without
pre-training data; fixes observer self-hash trimming bug
- ACL watcher worker: auto-provisions Mosquitto ACL entries when PUBLISH is
denied for a known user; eliminates the recurring gnomemoe ACL removal issue
- connectionMonitor: increase historical scan to 50 MB; fix OOM by streaming
lines instead of buffering; fixes missed backfill on restart
- Feed page: replace path-beta with lazy paths; add clickable node markers
showing name and public key; observer column nowrap; summary text unclipped
- PacketFeed: GRP-only message store protected from ADV eviction; scroll to
bottom on new message; 24-hour message backfill on initial load
- getRecentMessages: separate DB query for last 50 GRP messages last 24h
- getPacketDetail: new endpoint for full packet detail including observations
- Grafana/Loki/Promtail observability stack with Docker log collection
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New aclManager.ts rewrites a user's ACL block (all linked nodes) and
sends SIGHUP to the Mosquitto container whenever autoLinkOwnerNodeIds
runs at owner login, eliminating the need to manually edit the ACL
file when a user connects a second node.
- requireOwnerSession now resolves node IDs from owner_account_nodes
on every request instead of using the frozen session cookie list,
so manually- or auto-linked nodes are visible without re-login.
- docker-compose: mount mosquitto config dir (rw) and Docker socket
into backend so it can update the ACL file and signal Mosquitto.
- Add dockerode dependency for container SIGHUP signalling.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- limit owner last-hop data to selected-node cached queries
- replace broad owner-node prewarm with rolling per-node cache updates
- incrementally refresh the latest cached hour and drop data outside the 7-day window
- narrow owner last-hop telemetry to the selected MQTT node and cache it per node with hourly background refresh
- tighten repeater-only last-hop classification and clean up the owner chart selector/filtering
- fix owner direct-sender map basemap, static behavior, and viewport fitting/centering
- allow bare ukmesh.com and teessidemesh.com host handling in backend/frontend config
- split backend API into route/service/repository boundaries across stats, owner, and pathing\n- extract shared API bootstrap and helper modules\n- reduce MapLibreMap into smaller frontend map modules with extracted builders, config, popup UI, and types\n- continue worker modularization by extracting RF terrain/tile helpers alongside RF config and loss helpers\n- add repo-local architecture, DB lifecycle, link model, pathing, frontend map, and contributing docs\n\nThis is the verified refactor checkpoint: backend and frontend builds passed, worker syntax checks passed, affected containers were rebuilt, and /healthz is OK.
Split the monolithic backend router into domain route modules for health, radio, node status, coverage, nodes, owner, telemetry, stats, pathing, and misc endpoints.
Introduce the first service layer extractions for stats and owner so route files are now thin wrappers around business logic instead of owning heavy query orchestration and cache policy directly.
Harden DB startup by separating schema/migration bootstrap from the normal request pool timeout path. Startup schema initialization and migrations now run through a dedicated no-timeout startup pool, while runtime query timeouts remain in place.
This refactor also replaces the old monolithic startup schema asset flow with base schema + migration support, adds DB/config asset resolution cleanup, and substantially reduces backend/src/api/routes.ts from its previous multi-thousand-line state to a smaller registration/helper module.
Current roadmap status:
- route decomposition is effectively complete enough to stop splitting route files further
- stats service extraction is complete
- owner service extraction is complete
- next planned slice is pathing service extraction, followed by repository extraction under stats, owner, and pathing
Previously the /owner/session endpoint used node IDs baked into the
encrypted cookie at login time. If a node's public key changed, the
stale IDs would persist until the user manually logged out and back in.
Now re-fetches fresh node IDs from owner_account_nodes using the stored
MQTT username, falling back to cookie IDs only if the DB returns nothing.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The badge tracked Leaflet HTTP tile-load events, not actual server-side
rendering work (tiles are pre-rendered into Redis by the tile worker).
Counted normal browser tile fetches on pan/zoom, confusing users into
thinking the server was busy rendering. Removed badge, state, and CSS.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
DeckGLOverlay no longer has a ScatterplotLayer. Node dots are rendered
by server-side PNG tiles (/api/tiles/nodes). Update comments to reflect
the current architecture.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Owner portal fixes:
- Fix session node ID case mismatch: decryptOwnerSession was lowercasing
node IDs but /owner/live uppercased the ?nodeId= param, so the find()
never matched — users always got "Node is not owned by this session"
- Revert sameSite strict → lax (broke sessions after March 18 deploy)
- Remove LOWER() wrappers from all owner live SQL (node IDs are uppercase)
- Add Fireball's second node (TS17 Yagi North) to owner_account_nodes
Tile worker:
- Server-side PNG tile rendering via @napi-rs/canvas
- Tile snapshot cache, Redis-backed tile cache, tile worker service
- /api/tiles/nodes/:z/:x/:y.png endpoint with rate limiter
- UK tile TTL logic
Cross-network connectivity:
- Add 7-day hourly history series to /cross-network-connectivity response
- Cache result for 60s to avoid repeated join-heavy query
- Fix query to use literal interval instead of interpolated variable
Other:
- Remove LOWER() from packet/node SQL throughout (IDs stored uppercase)
- Various performance and cleanup improvements
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Wrap MapView + DeckGLOverlay in .map-layer container so DeckGL canvas
anchors to the map area (not app-shell), eliminating the 24px topbar
offset that caused packet history lines to misalign with node dots
- Add EXPENSIVE_LIMITER (12 req/min) to /observer-activity and
/cross-network-connectivity endpoints
- Tighten owner login cookie to sameSite: strict
- Add Content-Security-Policy and Permissions-Policy response headers
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The teesside directory was gitignored in 3c148b6 but the static import
and routing branch in main.tsx were left behind, breaking tsc for fresh
clones. Remove both the import and the teesside routing branch; the
site now falls through to the default Layout, matching the intent of
the original commit.
Fixesgadgethd/ukmesh#1
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Pre-compute /api/stats/charts every 30 min with inflight dedup; cache served instantly to all visitors
- Fix CPU usage stat: systemStats() was called twice per health request, giving near-zero second reading
- De-duplicate observer region packet counts by packet_hash (was counting per observer row)
- Show repeater names on feed instead of rx counter / "heard by N"
- Observer names span full row width on /feed packet list
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Backend:
- getViableLinks: replace correlated EXISTS subquery (2m 49s!) with CTE-based
join on pre-computed node set (39ms) — eliminates WS initial state timeouts
- WS initial state: switch from getLastNPackets (1,872ms, 24h correlated
subqueries) to getRecentPackets (16ms, 5-min CTE) — cold connect now 30ms
- Debounce emitNode/emitNodeUpsert with 500ms window to coalesce rapid advert
bursts before publishing to Redis (10-100x fewer publishes during active periods)
- Pre-normalise node IDs to lowercase at broadcast time; remove per-client
toLowerCase() allocations in shouldSendMessage (~200 allocs/sec eliminated)
- Cache /api/path-beta/history (60s TTL) and /api/coverage (30s TTL)
- Add 10-min TTL to resolveCache to prevent multi-day unbounded growth
- Fix LOWER() on node_status_samples queries suppressing index use
- Add Express compression() middleware; add gzip to nginx.website.conf
- Increase viableLinksCache TTL 30s→5min, INITIAL_STATE_TTL 30s→60s
- Pre-warm initial state cache at startup for teesside+ukmesh
- Add compression package dependency
Frontend:
- Lazy-load coverage (26MB GeoJSON) — only fetch when coverage toggle is on,
not on every page mount regardless of filter state
- Remove standalone 30s setInterval from useDashboardStats; feed stats from
App.tsx consolidated poll instead (-1 HTTP request per 30s)
- Pre-compute DeckGL packet-history segment colour+width inside useMemo;
getColor/getWidth become pure property lookups (~6000 log() calls/sec removed)
- DeckGLOverlay.tsx: add HistorySegmentWithColor type for pre-computed values
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
RADIO_BOT_URL is a private integration — strip the default service name
from docker-compose and remove it from the README env table.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Teesside dashboard, CSS theme, and favicon are now gitignored —
they live on the VM but are not part of the ukmesh.com project.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New www.teessidemesh.com single-page dashboard (TeessieDashboard.tsx)
with amber/charcoal theme, connectivity status, repeater cards with
7-day battery sparklines, MQTT observer grid, and decoded packet feed
- Backend: cross-network connectivity endpoint (hop-count packet matching),
radio-stats and radio-history proxy endpoints, observer-activity endpoint,
nodesDay stat field
- docker-compose: website-teesside service on port 3002
- Path-beta: worker pool, resolve cache, and pool refactor for performance
- Connection monitor, WS server and MQTT client improvements
- Mosquitto config, viewshed worker, and frontend UI polish
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New /repeater route with search and detailed repeater cards
- UK geographic bounds filter for viewshed calculations
- Advert packets API endpoint (GET /api/nodes/:id/adverts)
- Battery calculation fix (3.0V-4.2V range)
- NodeMarker optimizations (CircleMarker for repeaters)
- Map z-index adjustments for link lines
- Database performance indexes
- Nginx gzip compression
- Exclude mosquitto/acl and multipath.md from git
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Drop duplicate MQTT packets when the same packet hash arrives at the
same observer with the same hop count within 120s. Increase TimescaleDB
max_connections to 300 to prevent connection exhaustion on startup.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add server-side multi-observer resolver that solves a shared backbone
once (anchor observer), then constrains other observers to reuse the
same nodes for shared path hash prefixes, branching only at divergence
- New GET /api/path-beta/resolve-multi endpoint returns consistent
per-observer results in a single request
- Relax corridor filtering for multi-observer packets: candidates
accepted if within ANY observer's corridor, not just the primary
- Strengthen observer hop hint weights (0.09→0.18 confirmed,
0.11→0.22 reachable, 0.10→0.20 fallback) for better cross-observer
candidate steering
- Enumerate permutations from fork point for divergent suffixes
- Frontend prefers multi endpoint when 2+ observers, with cache
keyed by sorted observer IDs for automatic invalidation on new
observations
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Extract and persist routeType from wire header byte (was always undefined)
- Log warnings on truncated path data and reserved hash-size modes
- Guard buildCompatRawHex against pathByteLength > 63 overflow
- Extract transport codes (4 bytes) when present in FLOOD/DIRECT+codes packets
- Validate path hash hex lengths against wire-format hash size in resolver
- Add routeType to LivePacket type for downstream consumers
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- getOwnerNodeIdsForUsername/getMappedOwnerNodeIds: move dedup and format
validation into SQL (SELECT DISTINCT + regex filter) instead of JS post-processing
- viableLinksCache: add setInterval to evict stale entries every 30s so unused
network/observer combos don't persist in memory indefinitely
- /owner/live: add 5s in-memory cache per nodeId to avoid 6 heavy parallel
DB queries on every poll cycle
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add X-Content-Type-Options, X-Frame-Options, and Referrer-Policy headers to all responses
- Cap express.json() body to 50kb to prevent memory exhaustion via large payloads
- Redact credentials from MQTT broker URL before logging (ws://user:pass@host pattern)
- Whitelist telemetry event kind field instead of accepting arbitrary strings
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Enforce OWNER_COOKIE_SECRET env var — throw on startup if unset rather than
falling back to a weak derived secret
- Fix HTTPS detection to use exact string match instead of .includes()
- Cap and validate MQTT login credentials (32 char max, username format regex)
- Validate packetHash query param as hex before hitting the path resolver
- Validate node ID path params as 64-char hex in /nodes/:id/links and /nodes/:id/history
- Bound viableLinksCache to 50 entries with oldest-first eviction
- Bound countedAdvertHashes to 10,000 entries with oldest-first eviction
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>