Commit Graph

13 Commits

Author SHA1 Message Date
Alex Vanderpot c689ca1b3d clickhouse: back map, node search, and chat with live materialized views
Replace the per-request argMax/GROUP-BY views with insert-triggered
(incremental) materialized views so map node positions, node search, and
public-channel chat read pre-aggregated state instead of re-scanning all of
meshcore_packets on every query.

- 005: meshcore_adverts_latest_state (AggregatingMergeTree of argMaxState/
  min/maxState) + incremental MV + backfill; meshcore_adverts_latest becomes a
  -Merge view with the identical column contract. Node search reads it directly;
  map (unified_latest_nodeinfo) is unchanged.
- 006: meshcore_public_channel_messages_raw, a decoded payload_type=5 MergeTree
  keyed (channel_hash, ingest_timestamp); chat dedups by message_id at read time
  over a timestamp-bounded scan. Streaming/pagination push channel+cursor onto
  the primary key.
- Neighbor-edge MVs stay hourly REFRESH (they read the preserved view).

Verified against full prod data (14.5M rows): exact parity (0 mismatches) and
5-9x faster reads with no regressions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 00:13:00 -04:00
Alex Vanderpot c5ee493d8c region groups: data-driven generation, DB-sourced (drop TS/SQL sync)
Replace the hand-curated region groups with data-driven ones and make the
region_groups ClickHouse table the single source of truth.

- scripts/generate-region-groups.ts: offline generator — clusters regions by
  cross-region packet co-occurrence (min-share single-linkage) at two levels
  (broad "region" + tight "metro"), names clusters via `claude -p`, reconciles
  codes by member overlap so permalinks stay stable, and emits the region_groups
  seed. Migration 004 reseeded with the resulting 39 groups.
- Groups are DB-sourced: getRegionGroups() (cached) feeds /api/regions and the
  dropdown/labels; filtering resolves a selector in SQL to a region
  (region = 'X') or a group (region IN / hasAny ... SELECT region_code FROM
  region_groups WHERE group_code = ...). No hardcoded membership in TS;
  resolveSelector removed.
- Drop the TS<->SQL parity script (no membership left to sync); regionSql and
  the migration ALIAS are kept in sync by hand.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 17:40:44 -04:00
Alex Vanderpot a72c64a008 region system: derive IATA region codes from MQTT topics
Replace hardcoded (broker, topic) region slugs with uppercase IATA codes
derived from the meshcore/{IATA} base topic, discovered dynamically from
data (adding a region needs no code change). Adds region groups, Grafana
region/group filtering, and fixes the neighbor graph.

- regions.ts: single source of truth — regionFromTopic / normalizeRegion /
  regionSql / resolveSelector / selectorLabel. Legacy slugs (seattle->SEA)
  and bare meshcore + meshcore/salish -> SEA still resolve.
- regionGroups.ts + seeded region_groups table: PNW/CAL/DEU/POL.
- migration 004: region ALIAS column on meshcore_packets; 001 views expose
  region / regions[]; reworked neighbor MV (region-scoped, no cross-region
  edges, drops implausible >150km and (0,0) edges); scheduled meshcore_regions MV.
- API/streaming/actions resolve selectors; stream routes drop the hardcoded
  region allow-lists; map node query excludes (0,0) sentinel nodes.
- Dynamic region/group dropdowns (useRegions/RegionSelect); /api/regions.
- Grafana: cascading $region / $region_group template vars + panel filters.
- region-parity.ts (npm run check:regions) guards TS<->SQL drift.
- nix dev shell (flake.nix, Node 24).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 15:20:00 -04:00
Alex Vanderpot 72aa6be3d3 ingest: resubscribe on reconnect + staleness watchdog for zombie MQTT conns (#38)
The letsmesh broker was migrated behind Cloudflare and changed its topic
layout on 2026-06-02, which left prod's MQTT client in a zombie state:
connected per paho's IsConnected() (so the 30s monitor never rebuilt it) but
receiving zero messages, because the subscription was established only once
after the initial connect and never re-applied on paho auto-reconnects. Result:
12 days of silently missing letsmesh ingestion while davekeogh masked the loss.

Make reconnection robust instead of relying on broker-side session persistence:

- Subscribe inside the OnConnect handler so every (re)connect — including paho
  auto-reconnects — restores delivery. Use CleanSession(true)+ResumeSubs(false)
  so we never depend on the broker remembering our session.
- Add a per-broker data-staleness watchdog: a broker that reports connected but
  delivers no messages for MQTT_STALE_AFTER_SECONDS (default 300) is treated as a
  zombie and force-rebuilt (disconnect + fresh connect/subscribe). This catches
  exactly the failure IsConnected() misses.
- Reduce the external monitor to that watchdog role; transient drops are left to
  paho auto-reconnect rather than racing it with a brand-new client.
- Stable per-broker client IDs (by index) and pre-sized MQTTClients slice so
  indices stay aligned when an earlier broker fails; guard BrokerStatus/lastActivity
  with a mutex; promote connect/subscribe logs to Info for visibility.

Adds unit tests for the watchdog and env parsing; documents the new env var.

Co-authored-by: Alex Vanderpot <alex@Alexs-MacBook-Pro-2.local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 11:55:33 -04:00
Alex Vanderpot a0ab900da3 clickhouse: upgrade image 25.6.2.5 -> 26.5.1.882
Fixes a 25.x global memory-tracker drift where the tracker pinned at the
max-memory cap (RSS far below it), causing the OvercommitTracker to kill every
query (map/stats/neighbors all 500ing). Deployed in-place on prod over the
existing data dir after a cold backup.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 10:16:22 -04:00
Alex Vanderpot 02623e5559 neighbors: seattle->letsmesh region + fix path-edge prefix extraction
- Point the seattle region at the letsmesh broker (wss://mqtt-us-v1.letsmesh.net:443,
  topic meshcore/SEA) where Seattle traffic now lives.
- Fix a pre-existing bug in the path-edge extraction: `path` is a hex string of
  1-byte hop prefixes, so use substring(path, 2*i-1, 2) instead of
  hex(substring(path, i, 1)) (which re-hexed a single hex char and never matched
  the 2-char repeater prefixes -> path edges were always empty). Seattle now yields
  path edges again.

Verified on a full prod snapshot: the MV-backed "show all neighbors" query drops
from ~1.6s / 145M rows / 11.8 GiB to ~1ms / 108 rows / 3.8 KiB.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 03:02:09 -04:00
Alex Vanderpot cab5d821cf neighbors: serve from hourly refreshable materialized views
The two slow neighbor queries are converted to read precomputed tables that an
hourly REFRESH EVERY 1 HOUR materialized view maintains, instead of re-aggregating
meshcore_packets per request:

- meshcore_all_neighbor_edges: the global per-region edge graph (direct path_len=0
  adverts + repeater-prefix path edges) with endpoint details. getAllNodeNeighbors
  now filters it by region + bbox + lastSeen + has_location.
- meshcore_node_direct_neighbors: per-node direct adjacency (both directions) with
  neighbor details. getMeshcoreNodeNeighbors now filters it by node_public_key.

Also add the meshcore/SEA topic to the seattle region. Validated on a clean local
stack: migration 001->003 applies, both refreshable MVs create + refresh + populate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 02:33:57 -04:00
Alex Vanderpot 6be73b04a6 clickhouse: lift readonly read-size caps
The readonly profile's max_rows_to_read / max_bytes_to_read (500MB) is exceeded by
the map/stats views, which scan the full (growing) meshcore_packets table -> the web
app failed with TOO_MANY_BYTES. Remove the read-size caps; readonly=1, allow_ddl=0,
max_memory_usage and max_execution_time remain the guardrails.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 01:48:56 -04:00
Alex Vanderpot 7785158934 clickhouse: cap system log growth (TTL + disable profiler)
ClickHouse's internal diagnostics grew unbounded (text_log at Trace level and the
1s query profiler -> trace_log accumulated ~160G over months). Add short TTLs to
all system *_log tables, cap text_log at warning level, and disable the query
profiler in both profiles so trace_log stays empty.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 01:46:04 -04:00
Alex Vanderpot c233952840 pin clickhouse to 25.6.2.5 and grafana to 12.1.1
Pin the bundled service images to known versions for reproducible releases and
safe in-place reuse of an existing data dir (matching the production deployment).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 01:28:01 -04:00
Alex Vanderpot 384f7f8b14 ingest: fix go module path to match repo
Module is now github.com/ajvpot/meshexplorer/ingest (the code lives under
ingest/ in the meshexplorer repo), updated from the old standalone
clickhouse-meshingest path. build/vet/test pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 01:19:42 -04:00
Alex Vanderpot b7bcca6bf3 release-prep: docs, migration dialect fix, drop unused UDF
- Add a deploy-focused root README; update the web app README (meshcore-only,
  point Docker usage at the unified root compose).
- Fix the migration runner: set the goose clickhouse dialect (it defaulted to
  postgres and failed to create its version table). Migrations now apply cleanly.
- Remove the unused meshcore decrypt UDF (meshcore_try_decrypt was never called
  by any view/query/code) and simplify the ClickHouse image to a single stage.

Verified end-to-end: `docker compose up` brings up clickhouse -> migrate ->
meshcoreingest + meshexplorer; live ingestion from the real MQTT brokers lands
packets in ClickHouse and the web API serves decoded meshcore nodes via the
readonly user.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 01:01:33 -04:00
Alex Vanderpot 83978609f0 ingest: meshcore MQTT->ClickHouse stack
Vendor the ingest service under ingest/ and move the web app under meshexplorer/.
The ingest builds the meshcoreingest daemon and the goose migration runner,
applies the meshcore ClickHouse schema (packets, adverts, unified node view),
and loads its MQTT broker list and ClickHouse settings entirely from environment
variables (MQTT_BROKERS as a JSON array, CLICKHOUSE_*). No credentials are baked
into the source.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 01:01:33 -04:00