- 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>
- Paths (Beta): new map layer with confidence-scored packet path resolution.
Per-hop candidates filtered by 2-char node ID prefix, ranked by proximity
to the interpolated src-to-rx line. Each hop scored by ambiguity factor
(1/n_candidates) and distance factor (reachable within elevation-derived
range). Threshold slider in the filter panel (default 50%). Purple dashed
polyline, separate from the existing packet paths layer.
- Node elevation: viewshed worker now writes terrain elevation (m ASL) to
nodes.elevation_m when computing each viewshed, and backfills existing
nodes from stored radius_m on startup. Elevation shown in node popup.
- Coverage preview: "Preview coverage" button in node popup shows that
node's viewshed at lower opacity (10%) for 20 seconds then auto-dismisses.
- Beta path range uses stored radius_m from node_coverage directly instead
of bounding box estimation.
- Fix homepage Live Map button to link directly to app.teessidemesh.com.
- README Phase 3: clarify nodes must be on MQTT to be claimable.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add Packets page to website explaining all 10 packet types
- Fix handleNodeUpdate to add new nodes instead of silently dropping them
- Fix handleNodeUpsert to filter undefined fields before spreading
- Fix WSMessage type to include node_upsert and coverage_update
- Replace Avg RSSI/SNR stats with Active/Stale repeater counts
- Align stale definition: 7-14 days, inactive >14 days across app and website
- Stale nodes show as red markers on map, hidden at 14 days
- Website stale/active counts include prohibited-emoji repeaters
- Map never shows prohibited-emoji nodes at any point
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>