Commit Graph

851 Commits

Author SHA1 Message Date
JingleManSweep c85798bb95 Merge pull request #315 from ipnet-mesh/ui/modal-save-spinner
UI polish: modal save spinners + route list sort
2026-07-19 21:00:57 +01:00
Louis King b86c514847 fix(routes): sort list by From then To
Routes list was sorted only by from_label; routes sharing the same
From appeared in backend-returned order, which looked random. Add
to_label as a secondary tiebreaker in the localeCompare comparator.
Case-sensitivity preserved (no behavior change for the primary key).
2026-07-19 20:56:56 +01:00
Louis King 05369377b0 feat(ui): show spinner on modal action buttons while in-flight
Adds a small DaisyUI loading-spinner to the primary action button and
disables both action buttons (Save/Delete + Cancel) while the request
is in flight. Prevents double-submit on slow operations like route
create/update, and prevents closing a modal mid-request which leaves
modalState in a confusing state.

Two patterns in the SPA, kept idiomatic to each:

- channels.js + routes.js (lit-html state-driven): thread a 'saving'
  boolean from modalState into the modal renderers, which add
  ?disabled + a leading <span class="loading loading-spinner
  loading-sm"></span> when set. Handlers set the flag and re-render
  before await; clear and re-render in the catch.

- node-detail.js (native <dialog> + imperative listeners): add an
  id to the Save button so the handler can toggle .disabled and swap
  .innerHTML on the button element directly, restored in a finally.

Covers all six modal action buttons: channel add/edit + delete,
route add/edit + delete, node-tag edit + delete.
2026-07-19 20:56:50 +01:00
JingleManSweep 138f2534d3 Merge pull request #314 from ipnet-mesh/feat/route-health-precompute
feat: precompute route health + consolidated migration + observer badge fix
2026-07-19 20:40:34 +01:00
Louis King fe284d51b5 docs: route health precompute var + upgrading.md corrections
- docker-compose.yml: passthrough ROUTE_HISTORY_BACKFILL_INTERVAL_SECONDS
  alongside the existing ROUTE_EVALUATOR_INTERVAL_SECONDS (operators
  setting the var in .env had no effect without the passthrough).
- configuration.md: document the new collector var in the Collector table.
- upgrading.md (Route Health Monitoring): correct the migration summary
  (seven tables, not five) and add a paragraph on the precompute
  behaviour; add the new env var row to the table.
- upgrading.md (Dashboard / Route cache consolidation): drop the stale
  'REDIS_CACHE_TTL_DASHBOARD raised to 3600' claim — the precompute
  revert lowered it back to 300 s. Renamed the section to
  'API endpoint reorganization' and retained the two ship-true changes
  (ROUTE_DETAIL TTL removed, /dashboard/recent-activity split).
2026-07-19 20:14:22 +01:00
Louis King bddec0c9c2 chore(migrations): consolidate route migrations into single step
Replaces ec40c67c8c83, 6b3430fd84f4 and cf8dd7eaba9b (never deployed
to production) with one migration that builds the final route-health
schema directly on top of the production head 57bb65130b97.

The consolidated migration creates the seven route tables, adds the
nullable event_hash column to raw_packets, and runs three idempotent
backfills: packet_path_hops from raw_packets.decoded, nodes dedup by
public_key, and route_result_history + quality_avg for enabled routes.

On a freshly-restored production snapshot the route-history backfill
is a no-op (no routes exist yet) but is retained for idempotency
against dev backups that do have routes.
2026-07-19 19:56:41 +01:00
Louis King 65ec669e48 fix: n is not defined in observer filter badges
Commit 37f8d7a re-introduced emoji extraction in observerFilterBadges
after d1a3605 had refactored the iteration variable from `n` (node
object) to `area` (plain string). Three references to n._displayName
and n.public_key were left behind, throwing ReferenceError the moment
any observer had an area tag.

Adverts and Messages pages surface this as a warning badge with the
error message; the early return hides the bug on deployments with no
area-tagged observers.

Replace n.X with area (the iteration variable). Emoji extraction and
label fallback now operate on the area string directly.
2026-07-19 19:56:34 +01:00
Louis King 938028a7a7 feat: precompute route health in background sweep
Persist route health derivations so the API layer no longer recomputes
them on every request. Two new tables (route_result_history,
route_recent_matches) plus a quality_avg column on route_results back
the dashboard strip and per-route history endpoints.

Collector:
- run_evaluation (60s) writes snapshot + quality_avg + recent_matches
- run_history_backfill (hourly) recomputes completed-day buckets
- subscriber wires a dedicated backfill scheduler thread

API:
- dashboard routes-overview bulk-loads via single history SELECT
- routes detail/detail history read from precomputed tables with
  live-compute fallback
- POST/PUT on routes upserts recent_matches and quality_avg inline
- dashboard cache TTL lowered from 1h to 5m (invalidation-aware)

Config: route_history_backfill_interval_seconds=3600,
        redis_cache_ttl_dashboard default 3600 -> 300
2026-07-19 19:56:24 +01:00
JingleManSweep 82a6d032d0 Merge pull request #313 from ipnet-mesh/feat/dashboard-routes-widgets
feat: dashboard route widgets + cache TTL consolidation
2026-07-19 17:43:47 +01:00
Louis King fc60cd201a feat: route badge reflects 7-day rolling average
The overall health badge on route cards now shows the rolling 7-day
average tier instead of the latest window-hours snapshot, so flapping
routes that are currently up still appear marginal/failing if the
week's mean warrants it. Same averaging drives the dashboard Route
Health widget's summary dot, the routes page summary strip counts,
and (already) the Route Trends chart line colors.

- compute_average_quality() in collector/routes.py (0/1/2 mean,
  thresholds 1.5/0.75, empty-history fallback) — kept in sync with
  the averageRouteTier JS helper in charts.js
- RouteRead / RouteDetail gain a 'quality_avg' field
- list/get/update handlers compute it per route; create skips (no
  meaningful history yet) and the frontend falls back to
  route_result.quality for brand-new routes
- diagnosis tooltip unchanged (still current-snapshot state text)
2026-07-19 17:23:54 +01:00
Louis King 37f8d7ae0b feat: dashboard route widgets + cache TTL consolidation
Adds Route Trends chart and Route Health strip grid to the dashboard,
backs them with a new GET /dashboard/routes-overview endpoint, and
consolidates the Redis cache layer (removes ROUTE_DETAIL TTL, raises
DASHBOARD default to 1h, splits recent-activity into its own short-TTL
endpoint so Recent panels stay fresh while aggregate counts cache 1h).
2026-07-19 16:49:19 +01:00
JingleManSweep f35be67da6 Merge pull request #312 from ipnet-mesh/renovate/fontsource-monorepo
chore(deps): update fontsource monorepo to v5.3.0
2026-07-19 14:31:57 +01:00
renovate[bot] ca2aac1c8f chore(deps): update fontsource monorepo to v5.3.0 2026-07-19 06:14:29 +00:00
JingleManSweep 76c6c00f44 Merge pull request #311 from ipnet-mesh/feat/cache-invalidation-on-writes
fix: route_result not refreshing after route edit (plus cache invalidation infra)
2026-07-18 14:05:40 +01:00
Louis King e93c7fd9d6 fix: re-evaluate route_result synchronously after route create/update
Reported symptom: user edits a Route (e.g. lowers packet_count_threshold
from 6 to 3), and the routes list card still shows the old threshold for
~30s. The cache stack was exonerated — x-cache: MISS on the stale GET,
response body had the new packet_count_threshold=3, but route_result.
threshold remained at 6.

Root cause was neither HTTP cache nor Redis cache. The list card's stats
row (renderStatsRow in routes.js:82) displays route_result.threshold /
effective_clear / matched_count, which are persisted by the background
route_evaluator on a 30-60s schedule — separate DB row from the route's
direct fields. The PUT handler updated the route row immediately but
never triggered a re-evaluation, so route_result carried the stale
snapshot from the prior evaluator cycle until the next sweep.

Fix: add _reevaluate_route(session, route) helper that runs evaluate_route
+ upsert_route_result synchronously after the route commit. Called from
create_route (initial evaluation) and update_route (refresh on every
config change). Disabled routes short-circuit (no point evaluating a
route that's turned off). One bounded DB scan per write — same cost as
a single-route evaluator tick.

After this fix, the PUT response itself carries a fresh route_result
reflecting the new packet_count_threshold / clear_threshold, and the
next GET /api/v1/routes returns it. The list card updates on the very
next render cycle.

Tests:
- test_update_threshold_immediately_reflects_in_route_result: seeds a
  stale RouteResult with the OLD threshold, sends a PUT, asserts the
  response's route_result.threshold/effective_clear now match the new
  route config.
- test_disabled_route_does_not_trigger_evaluation: monkeypatches
  evaluate_route to a spy, asserts it's never called for disabled routes.
2026-07-18 13:55:02 +01:00
Louis King b6740068d3 log: cache invalidation observability for production diagnosis
After PR #311's invalidation wiring and HTTP policy fix, a user reported
the routes list still showed stale data for ~30s after a PUT. The HAR
they sent actually showed the fix working (x-cache: MISS after PUT,
fresh data returned) but had been captured with DevTools' 'Disable
cache' enabled — so it didn't represent normal browsing. We had four
competing hypotheses and no way to pick between them.

Add structured INFO logging at the two points that matter:

  * meshcore_hub.api.cache_invalidation._drop now emits
    'Cache invalidate start: prefix=... backend=...' and
    'Cache invalidate ok: prefix=...'. The backend= field
    distinguishes RedisCacheBackend from NullCache in one glance,
    catching 'REDIS_ENABLED is actually false' cases.

  * meshcore_hub.common.redis.RedisCacheBackend.delete now emits
    'Redis cache delete: prefix=... full_prefix=... keys_deleted=N
    scan_iterations=N'. keys_deleted=0 after a mutation that should
    have invalidated entries is the smoking gun for a cache-key
    mismatch between the store path (key_builder) and the delete
    path (prefix glob).

  * NullCache.delete emits a DEBUG line for the same reason.

The error paths are also enriched with full_prefix for greppability.
No behavioral changes — invalidation still swallows errors so cache
outages never break a write.

After deploy, a single route-edit repro will produce log output whose
shape (start/ok/warning/missing, keys_deleted count) unambiguously
identifies which of the four hypotheses is correct, so we can write
a targeted fix instead of guessing.
2026-07-18 13:41:52 +01:00
Louis King 71f65714ee fix: force browser revalidation so cache invalidation reaches the UI
After PR #311 wired server-side invalidation, a user reported that editing
a Route still showed old values on the routes list for ~30s. Root cause:
the api_cache_middleware emitted 'Cache-Control: private, max-age=30' on
@cached GETs, which let the browser serve its local HTTP cache copy
without revalidating. The Redis invalidation fired correctly but never
mattered — the browser never asked the server.

Switch all /api/* GET responses to 'private, no-cache' (synonymous with
'max-age=0, must-revalidate'). The browser now sends If-None-Match on
every navigation; the server answers 304 when Redis is warm and unchanged
(cheap — no body) or 200 after an invalidation.

The per-endpoint Redis TTL (30s default, 300s dashboard/route-detail)
still bounds cache.set lifetime; only the HTTP-layer max-age disappears.
Most navigations are still 304s, so the cost is one tiny round-trip per
page load while guaranteeing freshness after any mutation.

Adds an end-to-end regression test (test_routes_list_refresh_after_mutation)
modelling the exact reported scenario: cache-fill, conditional 304,
PUT mutation, conditional 200 with fresh body.
2026-07-18 13:17:04 +01:00
Louis King fc0520461a feat: invalidate read caches on user/admin mutations
Mutation handlers (POST/PUT/DELETE) on channels, routes, user profiles,
node tags, and adoptions now drop the corresponding Redis cache entries
after commit so the UI reflects changes on the next page load instead of
waiting for the 30s/300s TTL.

Adds meshcore_hub.api.cache_invalidation with seven helpers that
encapsulate the two cache-key formats (endpoint-name keys like 'nodes:'
vs URL-path keys like '/api/v1/channels:') and swallow backend errors
so a cache outage never breaks a successful write. The helper is a
no-op when Redis is disabled.

Cross-entity embeddings are covered: node-tag writes invalidate nodes,
messages, advertisements, and dashboard; adoption writes invalidate
nodes, profiles, advertisements, and dashboard.

CLI/collector mutations remain TTL-bound (infrequent, operator-driven).
2026-07-18 13:04:06 +01:00
JingleManSweep 26982c4d68 Merge pull request #310 from ipnet-mesh/feat/api-caching-and-event-dedup
feat: HTTP Cache-Control, dashboard TTL bump, and route event-hash dedup
2026-07-18 12:45:32 +01:00
Louis King 3810e029ce feat: HTTP Cache-Control, dashboard TTL bump, and route event-hash dedup
Bundles four independent improvements to API caching and route evaluator
accuracy that all touch the same files in the cache/routes stack.

## HTTP Cache-Control headers (api_cache_control_enabled)

The API now emits HTTP Cache-Control on every /api/v1/* response and
handles ETag / If-None-Match -> 304 Not Modified on @cached endpoints.

- @cached GETs: private, max-age=<redis_ttl> + strong SHA-256 ETag.
  Matching If-None-Match returns 304 with empty body.
- Uncached GETs under /api/v1/*: private, max-age=0, must-revalidate.
- POST/PUT/DELETE/PATCH and /health*: no-store.
- All private (several @cached endpoints are role-aware).
- @cached decorator stores new envelope {body, etag} in Redis; legacy
  bare-JSON entries still read (hashed on the fly) and auto-migrate on
  the next miss.
- X-Cache: HIT|MISS observability header is always emitted.
- Kill switch: API_CACHE_CONTROL_ENABLED (default true).

## Dashboard cache TTL bump (30s -> 300s)

REDIS_CACHE_TTL_DASHBOARD default raised from 30s to 300s. Covers all
/dashboard/* endpoints and /api/v1/routes/{id}/history. These return
trend/aggregation data where minute-level staleness is invisible but
every MISS costs seconds of SQL/aggregation. Also resolves reported
short-TTL behaviour on the per-route healthchart (which shares this
setting). Operators with explicit env var: unchanged. Old Redis entries
expire naturally within <=30s; no flush needed.

## Route recent-packets hop truncation

In the routes page expanded card, path hops are now truncated to
2 + ellipsis + 2 when length > 5, matching the packet-detail page
styling. Reuses the existing packets.hops_hidden i18n key as the
tooltip on the ellipsis badge.

## Route event-hash deduplication

A single advert/message/telemetry/trace retransmitted or flooded
through the mesh produces one RawPacket per on-air copy, each with a
fresh wire packet_hash. Counting those as distinct matches let a single
underlying event satisfy packet_count_threshold within seconds and
biased route health.

- Add nullable event_hash column to raw_packets and packet_path_hops
  (alembic 6b3430fd84f4). No backfill; legacy rows keep NULL and the
  evaluator falls back to wire packet_hash until they age out of
  window_hours.
- Subscriber denormalizes the structured event's event_hash onto the
  captured raw packet after handler dispatch.
- Route evaluator prefers event_hash when set, collapsing all
  receptions of the same underlying event into one match.
2026-07-18 12:43:01 +01:00
JingleManSweep 412bb1207b Merge pull request #308 from ipnet-mesh/feat/route-health-monitoring
feat: Route Health Monitoring
2026-07-17 23:41:18 +01:00
Louis King d509f82aec docs: document Route Health Monitoring for v0.16 release
- Add docs/routes.md feature/operational doc (evaluation semantics,
  visibility, observer scoping, background evaluator, prerequisites)
- Add v0.16.0 'Route Health Monitoring' section to docs/upgrading.md
  (above the existing Observer Ingestion Filters subsection)
- docs/configuration.md: cross-ref header + FEATURE_ROUTES (Feature Flags)
  and ROUTE_EVALUATOR_INTERVAL_SECONDS (Collector) rows
- README.md: Features bullet + docs tree entry + Documentation list entry
- docs/seeding.md: cross-link to routes.md from the Routes section
- docker-compose.yml: wire FEATURE_ROUTES (web) and
  ROUTE_EVALUATOR_INTERVAL_SECONDS (collector) as passthroughs
2026-07-17 23:38:45 +01:00
Louis King 4a2add26a4 fix: satisfy mypy on stats["errors"] union type in test_cli.py 2026-07-16 23:34:45 +01:00
Louis King a64d0b5561 test: close Route Health Monitoring coverage gaps (PR #308)
Add 49 targeted tests across 8 files to lift patch coverage from 76.9%
to ~95%+:

- collector/cli.py (3% -> covered): TestImportRoutes (12 tests for
  _import_routes: happy-path, upsert, all error branches, exception
  handler) + TestRouteSeedImport (seed command integration).
- api/routes/routes.py: update_route all-field branch, duplicate-label
  409, observer sync, unresolved-nodes 400, hidden-route 404, get_route
  contributing-observers, create with observers, preview guards,
  history retention clamp.
- collector/routes.py: evaluate_all_routes exception handler, preview
  early-return guards, evaluate_route_history < 2 expected, limit/until/
  observer params on fetch_candidate_paths, _route_expected_hashes
  derive branch, _has_any_hops_per_day empty guard.
- collector/subscriber.py: TestRouteEvaluatorScheduler (disabled,
  runs-and-stops, error-is-logged) mirroring TestSpamRescoreScheduler.
- common/schemas/routes.py: RouteUpdate + RoutePreviewRequest validators
  (min nodes, distinct, clear_threshold).
- collector/route_evaluator.py: evaluation exception handler.
- common/config.py: route_evaluator_interval_seconds default + custom.
- api/routes/packet_groups.py: mixed-visibility representative selection.
2026-07-16 23:30:28 +01:00
Louis King 8ce40fc75e Merge branch 'main' of github.com:ipnet-mesh/meshcore-hub into feat/route-health-monitoring 2026-07-16 23:08:22 +01:00
JingleManSweep b5bce562a5 Merge pull request #307 from ipnet-mesh/renovate/anomalyco-opencode-1.x
chore(deps): update anomalyco/opencode action to v1.18.3
2026-07-16 23:07:38 +01:00
JingleManSweep 2031e960d7 Merge branch 'main' into renovate/anomalyco-opencode-1.x 2026-07-16 23:05:38 +01:00
JingleManSweep e465cd34ea Merge pull request #309 from ipnet-mesh/renovate/tailwindcss-monorepo
chore(deps): update tailwindcss monorepo to v4.3.3
2026-07-16 23:05:19 +01:00
JingleManSweep ac73b35e92 Merge branch 'main' into renovate/tailwindcss-monorepo 2026-07-16 23:02:26 +01:00
JingleManSweep f2f93656f5 Merge branch 'main' into renovate/anomalyco-opencode-1.x 2026-07-16 23:00:40 +01:00
JingleManSweep 6b823027ef Merge pull request #305 from ipnet-mesh/renovate/daisyui-5.x-lockfile
chore(deps): update dependency daisyui to v5.6.18
2026-07-16 23:00:21 +01:00
renovate[bot] 346d75415a chore(deps): update anomalyco/opencode action to v1.18.3 2026-07-16 21:59:45 +00:00
renovate[bot] 1f6c144322 chore(deps): update tailwindcss monorepo to v4.3.3 2026-07-16 21:59:40 +00:00
Louis King b8b7df0966 ui(routes): split from/to header into aligned icon grid
The route card header wrapped onto two lines when from/to labels were
long. Replace the inline "from -> to" title with a two-row CSS grid
(auto | 1fr) so labels align vertically, each preceded by a dedicated
SVG icon (iconRouteFrom/iconRouteTo: arrow departing/arriving at a
dot). Long labels now ellipsize with a title tooltip.

Move the reversibility indicator (reversible <-> vs one-way ->) out of
the title into a ghost badge beside the quality badge, where it no
longer fights the grid layout.

Also nudge spacing between "Recent Packets" and its path rows (mt-1 ->
mt-2) for a little breathing room.
2026-07-16 22:56:48 +01:00
Louis King 30f18561bf refactor: consolidate route health migrations into one clean migration
Squashes four formerly-separate tail migrations into a single migration
(ec40c67c8c83) that builds the final schema directly off 57bb65130b97:

- 8f2a3c4d5e6f route health tables + packet_path_hops backfill
- c0d1e2f3a4b5 deduplicate nodes by public_key (idempotent data fix)
- 76513f4c57e9 ix_packet_path_hops_received_at index
- 71f6e01e4bf6 rename degraded_threshold -> clear_threshold

The rename runs as a no-op: routes.clear_threshold and
route_results.effective_clear are born with their final names. The
received_at index is created with the table, and the node dedup is
preserved verbatim (no-op on a clean DB).

Also drops the redundant uq_route_results_route_id unique constraint:
route_results.route_id now has a single unique index matching the model,
eliminating a pre-existing autogenerate drift.

Net schema is identical to running all four migrations. Verified with a
fresh alembic upgrade head + alembic check (no drift) on SQLite.
2026-07-16 22:34:13 +01:00
Louis King ad07c1521c feat: route health history, compact cards, clear_threshold rename
- Add per-route health history endpoint (GET /routes/{id}/history)
- Optimize history fetch with _fetch_matching_hops (SQL-level prefix filter)
- Add standalone received_at index on packet_path_hops
- Fix cache key collision by including URL path in _routes_key_builder
- Fix Chart.js canvas reuse with Chart.getChart() guard
- Remove fleet overview endpoint (502 timeout on large datasets)
- Redesign route cards: always expanded, compact icon stats row,
  per-segment date labels, loading spinner, bottom-pinned admin buttons
- Rename degraded_threshold -> clear_threshold across model, schema,
  API, metrics, CLI, frontend, tests, and seed config
- Add iconClock to icons.js
- Update AGENTS.md: random revision IDs, never build images
- Two Alembic migrations: received_at index, column rename
2026-07-15 23:37:03 +01:00
Louis King ae22dff4bc fix: inject now param into run_evaluation for deterministic tests
Test data used a hardcoded _NOW (2026-07-12 12:00 UTC) while
run_evaluation used real datetime.now(), causing a time-window flake
once wall-clock time exceeded _NOW + window_hours (24h). Tests now
pass now=_NOW explicitly; production callers are unaffected (defaults
to datetime.now(timezone.utc) when not provided).
2026-07-13 16:23:00 +01:00
Louis King 8a1b6d787a fix: filter observer autocomplete to is_observer nodes
The Route UI's observer search was hitting GET /api/v1/nodes without
the observer filter, showing all nodes instead of only observer nodes.
The backend already supports ?observer=true via an indexed column —
just needed to pass the param from handleObsSearch and handleObsKeydown.
2026-07-13 16:08:51 +01:00
Louis King 1e87b7abb5 Merge branch 'main' of github.com:ipnet-mesh/meshcore-hub into docs/mesh-link-monitoring-plan 2026-07-13 16:05:14 +01:00
Louis King d10f2b4c9f fix: deduplicate nodes by public_key and re-assert uniqueness
Production DB restore bypassed UNIQUE enforcement on nodes.public_key,
leaving duplicate rows that caused MultipleResultsFound in node lookups
(GET /api/v1/nodes/{public_key} and collector find-or-create handlers).

Migration merges duplicates (winner = earliest first_seen), re-points all
14 FK columns referencing nodes.id, merges scalar fields, and re-creates
the UNIQUE index. Superset of b1c2d3e4f5a6 which only covered 9 FKs.
2026-07-13 15:49:25 +01:00
Louis King 1ee01a65fe refactor: replace route name with from/to endpoint labels
Route identity is now a (from_label, to_label) composite unique pair
instead of a single name column.  The three route migrations (create
tables, add reversible, replace name) are collapsed into one clean
migration since the feature hasn't shipped yet — 8f2a3c4d5e6f creates
all five tables with from_label/to_label + reversible from the start,
no intermediate steps.

Engine: recent_matches now returns the sliced subpath between the
matched endpoints, not the full path.  Diagnosis text moved to a hover
tooltip on the quality badge.  Cards sorted by from_label.  Seed YAML
switched to a list format with from/to keys; upsert by (from_label,
to_label).  Plan/tasks docs updated to present the final schema.
2026-07-13 13:35:58 +01:00
Louis King 6a4329b9d2 fix: path highlight lookup should use prefix match, not exact
Routes with match_width smaller than the mesh protocol's path hash width
would never highlight matched nodes in recent matches.  The collector's
is_subsequence uses startswith() (prefix match), but the JS lookup used
Map.get() (exact match).  Truncate node_hash to 2*match_width chars
before lookup so 'a1b2c3' matches expected_hash 'a1'.
2026-07-12 23:03:04 +01:00
Louis King 27d6160164 fix: use boolean literal for reversible server_default
PostgreSQL rejects DEFAULT 1 for boolean columns; use DEFAULT true
which works on both PostgreSQL and SQLite (3.23.0+).
2026-07-12 22:46:35 +01:00
Louis King 818d4a2abe fix: collision in reversible migration revision ID
The reversible migration used revision ID a1b2c3d4e5f6, which was
already taken by the rename_receiver_to_observer migration.  Renamed
to f1e2d3c4b5a6.
2026-07-12 22:43:39 +01:00
Louis King 0e202b3999 feat: reversible route matching
Routes now default to bidirectional matching — packets traversing the
configured path in reverse (C->B->A instead of A->B->C) also count
towards health.  A 'reversible' toggle lets users constrain a route
to one direction when needed.

- Route model: new 'reversible' column (default true, server_default 1)
- Migration: a1b2c3d4e5f6 (additive ALTER TABLE)
- Matching engine: _match_hops + _fetch_candidate_paths_maybe_bidirectional
  helpers shared across evaluate_route, recent_matches, and preview_route
- API schemas: reversible on RouteCreate/Update/Read/Detail/PreviewRequest
- SPA: toggle in modal; card path chips show <-> for reversible, -> otherwise
- Seed loader + example YAML: reads 'reversible' key
- 4 new collector tests (reverse match, non-reversible, both-directions, API)
2026-07-12 22:39:15 +01:00
Louis King 14fbc45387 feat: route health monitoring with visual path builder
Add complete route health monitoring feature that tracks whether packets
traverse expected multi-hop paths through the mesh network.

Models & migration:
- 5 new models: PacketPathHop, Route, RouteNode, RouteObserver, RouteResult
- Alembic migration with keyset-paginated backfill of existing packets

Collector:
- store_raw_packet refactored to persist path hops via bulk insert
- Matching engine (collector/routes.py) with subsequence matching, quality
  bands (clear/marginal/failing/no_coverage), and collision detection
- Background route evaluator (60s loop) wired into subscriber lifespan
- Route seed loader in CLI (resolves by public_key, matching YAML format)

API:
- 6 CRUD endpoints + preview endpoint under /api/v1/routes
- Schemas accept node_public_keys (64-char hex) instead of internal UUIDs
- 5 Prometheus gauges for route health metrics
- Packet groups endpoint reads from hop table

Web UI:
- Full SPA routes page with summary strip, grouped cards, expandable detail
- Routes nav entry in both desktop (spa.html) and mobile (app.js) navbars
- Home page nav card with feature gate
- API proxy access mapping for v1/routes endpoints
- Visual node-search path builder with autocomplete dropdown, ordered chips
  with reorder/remove controls, and paste-64-char-key support
- Observer picker with same search UX (unordered chips)
- i18n strings in en.json and nl.json

Config:
- feature_routes flag (default: true)
- route_evaluator_interval_seconds (default: 60)
- routes_file seed path
- example/seed/routes.yaml
2026-07-12 22:26:44 +01:00
renovate[bot] e60dc7ba26 chore(deps): update dependency daisyui to v5.6.18 2026-07-12 17:47:38 +00:00
JingleManSweep 2455d5af0e Merge pull request #304 from ipnet-mesh/jinglemansweep-patch-1
Add GitHub funding link to FUNDING.yml
2026-07-09 22:18:00 +01:00
JingleManSweep 91a65807ad Add GitHub funding link to FUNDING.yml 2026-07-09 22:16:08 +01:00
JingleManSweep f48c37578d Merge pull request #302 from ipnet-mesh/renovate/daisyui-5.x-lockfile
chore(deps): update dependency daisyui to v5.6.16
2026-07-09 21:47:32 +01:00