Files
meshcore-hub/docs/routes.md
T
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

6.1 KiB
Raw Blame History

Route Health Monitoring

The Routes page (/routes) lets operators define monitored multi-hop mesh routes and track each one's health over time. A background evaluator on the collector matches captured packet paths against each route's configured node sequence within a rolling time window and rolls the result up into a traffic-light status card, with a per-day history strip and a list of recent matching transmissions.

For the environment-variable reference, see configuration.md → Feature Flags (FEATURE_ROUTES) and → Collector (ROUTE_EVALUATOR_INTERVAL_SECONDS). To define routes from YAML, see seeding.md → Routes.

Prerequisite: route matching reads from the packet_path_hops table, which is populated by Raw Packet Capture. Keep FEATURE_PACKETS=true (the default) so packet paths continue to be captured; with capture off, route cards stay at unknown once the existing window of hops ages out.

How health is evaluated

A route is an ordered list of two or more nodes (the configured path). For each captured packet reception, the evaluator walks the reception's path-hash sequence and checks whether the route's nodes appear in order, as a subsequence (intermediate hops are allowed). When reversible is set (the default), the reverse-ordered path is also accepted, so a packet travelling B → ... → A counts toward an A → B route.

Matches are deduplicated by their underlying event identity, not per on-air transmission. The collector denormalizes the structured event's event_hash (the same key used to dedup advertisements, messages, telemetry, and traces at the structured-event layer) onto each captured raw packet at ingest time. The evaluator prefers event_hash when set, so retransmissions or floods of the same underlying advert/message count once toward packet_count_threshold instead of once per on-air copy. Packets captured before this column existed (and any unclassified wire packets) have event_hash IS NULL and fall back to the wire packet_hash, preserving the previous behaviour until they age out of the configured window_hours.

Each route carries these knobs:

Field Default Description
match_width 1 Path-hash prefix width in bytes (1/2/3). Higher widths disambiguate nodes that share a short public-key prefix.
window_hours 24 Rolling lookback window for the live status card.
packet_count_threshold 3 Distinct matching packets at/above which the route is healthy. "Distinct" is per underlying event, not per transmission — see How health is evaluated above.
clear_threshold (2× threshold) Comfort bar for the clear/marginal split. Omit/null to use twice the threshold.
max_hop_span (unlimited) Caps the position gap between the first and last matched node, to reject matches that wander too far.
reversible true Also match the path in reverse direction.
enabled true When false, the route is skipped by the evaluator and reports unknown/no_coverage.

The result is reported on two axes:

  • Statehealthy (≥ threshold distinct matches in the window), unhealthy (some packets were observed but too few matched the configured path), or no_coverage (no in-scope packets at all in the window).
  • Qualityclear (≥ effective clear bar), marginal (healthy but below the comfort bar), failing (unhealthy), or unknown (no coverage / route disabled).

The state/quality split lets the dashboard render a single traffic-light band while keeping the underlying reason visible: a marginal route is technically up but losing margin, and a failing route has traffic in the window but the configured path isn't completing.

Observer scoping

By default every observer's receptions contribute to every route. A route may instead scope itself to an explicit observer allow-list (route_observers); when set, only receptions from those observer nodes are matched. Use this to ignore noisy or off-path observers that would otherwise drown out the signal. Observer nodes that don't yet exist in the database are skipped with a warning rather than failing the seed.

Background evaluator

The collector runs a background thread that re-evaluates every enabled route on a fixed cadence and upserts the result into route_results (one row per route, holding the latest state, quality, matched_count, and the threshold snapshots used). The cadence is controlled by ROUTE_EVALUATOR_INTERVAL_SECONDS (default 60); set it to 0 to disable the evaluator, in which case route cards remain at unknown until it is re-enabled. The per-route history strip and the /api/v1/routes/{id}/history endpoint evaluate on demand over the raw packet_path_hops table and are not dependent on the background evaluator.

Visibility

Routes carry the same role-based visibility levels as channels — community, member, operator, admin. A user only sees routes whose visibility is at or below their role's maximum level. Seeded routes default to community (visible to everyone); set a higher level to restrict a route to operators/admins only. Visibility is enforced on both the list and detail endpoints, so a hidden route's existence is not leaked.

Defining routes

Routes are keyed by their from/to endpoint labels and upserted by that pair. There are two ways to create them:

  • Seed YAML — add a routes.yaml to your SEED_HOME and run the seed process. See seeding.md → Routes for the format and rules (path nodes must already exist in the database; the (from, to) pair must be unique).
  • APIPOST /api/v1/routes (admin only) creates a route, with a /preview endpoint that dry-runs matching against an unsaved configuration so you can tune thresholds before committing. See SCHEMAS.md for the request/response shapes.

The /routes page renders the live status card, the per-day history strip, recent matching transmissions (with observer attribution), and — for admins — inline edit/delete controls.