- Resolver now picks shortest-hop observation (ORDER BY hop_count ASC) instead of
most recent arrival, matching what the live feed shows for flooding packets
- Red path segments that share an edge with any purple (high-confidence) path are
filtered out so purple always takes visual precedence
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- usePacketPathOverlay: don't cache null beta predictions; re-fetch pinned
packet paths when new observers arrive after pin
- MapView: stable linkedNodesKey fingerprint so linkLines memo only recomputes
when linked node positions/roles change, not on every heartbeat
- health/status + api/routes: exclude test-network-only nodes from stale count
and mqttNodes stat so they don't inflate thresholds
- mosquitto/acl: add meshcore-test/# readwrite for backend user
- useNodes/PacketFeed/App: merge rx_count/tx_count from DB, ALL badge when all
MQTT regions hear a packet, improved summary extraction
- db/index: getRecentPackets now deduplicates by packet_hash with observer
aggregation; getLastNPackets adds rx_count/tx_count and _summary support
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- move beta path resolution onto the backend with packet-level diagnostics and live API access
- add path simulation worker plumbing, health/schema updates, and compose wiring
- update app overlays to fetch server-computed beta paths with animated rendering and request caching
- refine mobile controls so map layers collapse behind a toggle and compact the links legend
- getCandidates: sort all tiers by alignment-toward-src combined with distance
penalty (sortScore = cosine_similarity - dist_km/50) rather than distance alone.
When src is unknown the alignment term is 0 so distance sorting is preserved.
- maxSkips: changed from pathHashes.length (skip everything) to
min(2, floor(N/3)) — prevents degenerate paths on sparse data
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
www.ukmesh.com (website-ukmesh container, port 3004):
- UKLayout, UKHomePage, UKInstallPage, UKMqttPage components
- VITE_SITE=ukmesh build arg routes to UK pages in main.tsx
- Stats fetched via relative /api path (proxied by Nginx)
app.ukmesh.com (app-ukmesh container, port 3003):
- Same dashboard build with VITE_APP_HOSTNAME=app.ukmesh.com
- VITE_NETWORK=ukmesh for future API filtering
MQTT:
- node-ukmesh Mosquitto user for ukmesh observer nodes
- ACL updated: backend reads meshcore/# and ukmesh/#
- ukmesh nodes publish to ukmesh/{IATA}/{PUBKEY}/packets
Cloudflare routes to add in Zero Trust:
www.ukmesh.com -> http://website-ukmesh:80
app.ukmesh.com -> http://app-ukmesh:80
mqtt.ukmesh.com -> http://mosquitto:9001
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Splits the monolithic app container into:
- backend: Node.js handling MQTT, WebSocket, API, DB (no static files)
- app: Nginx serving the React SPA, proxying /api and /ws to backend
This means future dashboards (app.ukmesh.com etc.) can be added as
additional Nginx containers pointing at the same backend with no
duplicate calculations or DB connections.
Also updates nginx.website.conf to proxy to backend instead of app,
and bumps website host port from 3001 to 3002 to avoid conflict.
NOTE: Cloudflare Zero Trust tunnel route for app.teessidemesh.com
must be updated from http://app:3000 to http://app:80.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Lays groundwork for ukmesh.com alongside teessidemesh.com:
- Add network TEXT column to packets (default teesside)
- Parse network from topic prefix: meshcore/* = teesside, ukmesh/* = ukmesh
- Subscribe to both meshcore/# and ukmesh/# on connect
- Pass network through insertPacket so every packet is tagged at source
All existing packets default to teesside. UK-wide nodes publishing
to the ukmesh/ topic prefix will be tagged ukmesh automatically.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Removes the MME-only DISPLAY_IATA gate so packets from any observer
location are stored and emitted to the live dashboard. teessidemesh.com
only has MME observers anyway so behaviour is unchanged there.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Removes the 1-year TimescaleDB retention policy from the packets
hypertable so no data is ever automatically deleted. All tables
(nodes, node_links, packets, etc.) are now kept permanently.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add force_viable column to node_links to permanently force specific
RF links regardless of ITM path loss calculation
- Force Hartlepool-RPT <-> YM-NM-RPT link (147.6 dB, confirmed by 4368
observations but above the 138 dB fade margin threshold)
- Restore FADE_MARGIN_DB=10 in ITM viability check (both code paths)
- Reduce last-resort fallback distance from 500 km to 150 km in the
backtracking DFS path resolver
- getViableLinkPairs and /api/nodes/:id/links now include force_viable=true
- Add useLinks hook for link state management
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Longer history is needed for training path-resolution algorithms (Viterbi
etc.) on real observed relay paths. At current traffic (~10 MB/day) a year
of data is ~3.6 GB — manageable on local storage.
Schema uses a DO block to update an existing retention policy via alter_job
rather than requiring a manual DB migration.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Worker loads confirmed links (itm_viable + observed_count >= 5) when
resolving relay hashes — prefers known neighbours over geographic
proximity, creating a self-improving feedback loop as traffic accumulates
- Visited-node set added to worker resolver (each node relays once per packet)
- Frontend handles live link_update WebSocket messages: linkPairs and
viablePairsArr updated in real-time without page refresh
- MIN_LINK_OBSERVATIONS constant defined in both worker and frontend (5)
- link_update added to WSMessageType union
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Single and double observation links are likely noise from path hash
collisions or one-off propagation events. MIN_LINK_OBSERVATIONS=5
applied to the Links map layer, node popup neighbours, and the
backtracking path resolver link pair set.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- node_links table: stores ITM-viable hop pairs from observed packet paths
with directional counts (count_a_to_b / count_b_to_a) and path loss dB
- Historical backfill: decodes all stored packets on first run to populate links
- Per-node /api/nodes/:id/links endpoint: fetched on popup open (not preloaded)
- Node popup: shows confirmed neighbours with directional arrows and path loss
- Links layer: amber polylines on map showing all ITM-viable link pairs
- Beta path resolver rewritten as backtracking DFS:
- Confirmed neighbours tried first (real observed link data)
- Falls back to closest node within terrain-derived radio range
- Backtracks when a candidate leads to dead ends at subsequent hops
- Skips capped at ceil(hops/3) to prevent degenerate all-skip paths
- Visited set enforces each node appears at most once per path
- hopCount used to slice path_hashes to actual relay count
- Viable link pairs sent in WebSocket initial_state for path resolution
- LINK_BUDGET_DB raised to 148 dB; fade margin removed from viability check
- Viewshed worker: directional link tracking, UK mainland clipping fixes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Filter out nodes with 🚫 in their name from hop candidates in both
resolvePathWaypoints and resolveBetaPath. For beta paths, change
empty-candidates from return null (abort) to continue (skip hop),
so a prohibited relay is simply omitted from the displayed path.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace CSS keyframe animation (unreliable due to Leaflet calling
_updateStyle on every prop change) with a requestAnimationFrame loop
that directly increments stroke-dashoffset on the Leaflet SVG _path
element. Dashes march forward at ~1.2 cycles/s for both regular and
beta paths.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>