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.
6.1 KiB
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_hopstable, which is populated by Raw Packet Capture. KeepFEATURE_PACKETS=true(the default) so packet paths continue to be captured; with capture off, route cards stay atunknownonce 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:
- State —
healthy(≥ threshold distinct matches in the window),unhealthy(some packets were observed but too few matched the configured path), orno_coverage(no in-scope packets at all in the window). - Quality —
clear(≥ effective clear bar),marginal(healthy but below the comfort bar),failing(unhealthy), orunknown(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.yamlto yourSEED_HOMEand 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). - API —
POST /api/v1/routes(admin only) creates a route, with a/previewendpoint that dry-runs matching against an unsaved configuration so you can tune thresholds before committing. SeeSCHEMAS.mdfor 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.