auto: docs sync 2026-07-08T00:00:06+00:00

Files changed: engine/lint-report.md vault/.obsidian/workspace.json vault/projects/meshai-config-hot-apply.md vault/projects/meshai-region-routing-plan.md
This commit is contained in:
echo6-autocommit 2026-07-08 00:00:06 +00:00
commit 0ddeba92c6
4 changed files with 148 additions and 13 deletions

View file

@ -1,6 +1,6 @@
# Vault Lint Report
Generated: 2026-07-07T12:00:07Z | Docs scanned: 101 | Elapsed: 0.0s
Generated: 2026-07-07T18:00:05Z | Docs scanned: 102 | Elapsed: 0.0s
## Summary
@ -51,6 +51,7 @@ _None. All wikilinks resolve._
- no incoming links: runbooks/lxc-service-migration.md
- no incoming links: runbooks/mailcow-create-mailbox.md
- no incoming links: docs/matrix/matrix_host.md
- no incoming links: runbooks/meshai-prod-compose-override.md
- no incoming links: runbooks/meshmonitor-password-reset.md
- no incoming links: runbooks/meshtasticd-sim-nodes-runbook.md
- no incoming links: runbooks/nordvpn-lxc.md
@ -64,7 +65,6 @@ _None. All wikilinks resolve._
- no incoming links: runbooks/recon-service-integration.md
- no incoming links: session-resume/SESSION-HANDOFF-meshai-test.md
- no incoming links: docs/matrix/synapse_retention_discovery.md
- no incoming links: runbooks/syncthing-add-node.md
## Gaps & suggestions
@ -92,7 +92,7 @@ Matt decides whether to create a real doc — when he does, future sweeps will l
| Term | Docs mentioning it |
|------|--------------------|
| `tailscale` | 39 |
| `docker` | 34 |
| `docker` | 35 |
| `proxmox` | 31 |
| `headscale` | 25 |
| `peertube` | 19 |

View file

@ -199,17 +199,19 @@
},
"active": "17bd4a6166f789d0",
"lastOpenFiles": [
"projects/meshai-config-hot-apply.md",
"projects/meshai-config-hot-apply.md.tmp.5281.28f86510539f",
"projects/meshai-region-routing-plan.md.tmp.5281.292f05969651",
"projects/meshai-region-routing-plan.md.tmp.5281.9915ac2ff82e",
"projects/meshai-region-routing-plan.md.tmp.5281.d93b011e3329",
"projects/meshai-region-routing-plan.md.tmp.5281.ce38ed145504",
"projects/meshai-region-routing-plan.md.tmp.5281.c1e4202de220",
"projects/meshai-region-routing-plan.md.tmp.5281.4ae73bc6fce2",
"projects/meshai-region-routing-plan.md",
"projects/meshai-region-routing-plan.md.tmp.5281.a1cf56fc940f",
"runbooks/meshai-prod-compose-override.md",
"runbooks/meshai-prod-compose-override.md.tmp.5281.6ff00a45f3af",
"docs/hardware/environment.md.tmp.246153.593a4dd35d71",
"docs/hardware/environment.md.tmp.246153.61ce5813aae7",
"glossary.md.tmp.246153.a1d75ab07676",
"projects/meshcore-transport.md.tmp.5281.cb5cb7900929",
"projects/meshcore-transport.md.tmp.5281.7a5287ef0bf2",
"projects/meshcore-transport.md.tmp.5281.43dbe09b3b59",
"projects/meshcore-transport.md.tmp.5281.d9ef6ce168ab",
"projects/meshcore-transport.md.tmp.5281.017d34de615d",
"projects/meshcore-transport.md.tmp.5281.1f9a0c39eb6d",
"projects/meshai.md",
"projects/meshcore-transport.md",
"runbooks/pymc-repeater-kiss-tnc-reenumeration.md",
@ -234,8 +236,6 @@
"concepts/lxc-container.md",
"concepts/osint.md",
"concepts/split-dns.md",
"concepts/vector-database.md",
"concepts/meshtastic.md",
"assets/echo6yellow_logo_422x422_square.png",
"assets/echo6yellow_logo_422x81.png",
"assets/echo6_logo.png",

View file

@ -0,0 +1,48 @@
---
title: meshai Config Hot-Apply — Kill the Restart-Required GUI Friction
type: project
tags:
- mesh
related:
- [[meshai]]
- [[meshai-region-routing-plan]]
updated: 2026-07-07
status: proposed
---
# meshai Config Hot-Apply (proposed — backlog)
**Goal (Matt, 2026-07-07):** make GUI **Save just apply** instead of prompting a container restart. The restart-required system is high-friction and Matt loathes it. This doc captures the why + the fix so it can be picked up later. Surfaced while building [[meshai-region-routing-plan]] (coverage/region edits require a restart to take effect).
## Why saves don't apply today
Split by **who reads the config, and when:**
- **Live sections (already hot):** notifications, rules, toggles, timezone, etc. Their consumer reads `config.<section>` **fresh on every use** (e.g. the dispatcher reads `config.notifications` per event), so the save endpoint's `setattr(app.state.config, section, new_value)` (`dashboard/api/config_routes.py:176`) is enough — instant.
- **Restart-required sections snapshot config at BOOT and never re-read it.** `RESTART_REQUIRED_SECTIONS` (`config_routes.py:29-40`) = `connection, llm, mesh_sources, meshmonitor, dashboard, environmental, coverage, generic_sources`. Examples:
- `CoverageFilter` is built ONCE at pipeline start from `areas_from_config(config.coverage)` (`notifications/pipeline/__init__.py:120-126`) — frozen copy of the areas.
- Adapters get their fetch bbox computed ONCE at boot via `enclosing_bbox(coverage.areas)` (`main.py:413`).
- The radio connector opens the link ONCE with `connection` settings.
A naive live mutate would leave these running objects on the OLD config while new data flows under new assumptions → a half-applied, inconsistent "transient AND-mode" (the exact thing the guard prevents). So the flag saves-to-disk-but-diverges-in-memory until restart. Crude but not arbitrary.
## The fix: per-section reload hooks + a hot-apply framework
"Save just works" = on save, **rebuild the component that snapshotted the config, atomically.** Proposed framework: each section registers an optional `apply_live(new_config)` handler; the save endpoint calls it and only falls back to "restart required" when a section has no safe handler.
Tiers:
| Tier | Sections | Effort |
|---|---|---|
| **Easy wins** | `coverage`, `llm`, `environmental` thresholds | Low — rebuild the filter / swap the LLM client / re-read next tick |
| **Moderate** | `generic_sources`, `mesh_sources`, `environmental` adapters | Medium — re-instantiate the adapter with new config |
| **Genuinely disruptive** | `connection`, `meshmonitor`, `dashboard` (port) | Replace full restart with an explicit **"Reconnect" / "Rebind" action** — a live half-swap of an open radio link is unsafe |
**Coverage is Tier-1:** on save, rebuild `CoverageFilter`'s area list + refresh the adapters' bbox → region edits apply fully live (both routing columns AND engine re-scoping). NOTE: the routing-column half is already solved in [[meshai-region-routing-plan]] (regions endpoint reads on-disk coverage, so columns are dynamic without restart); the remaining half is the engine re-scoping, which this work covers.
## Two ways to scope
1. **Narrow:** just make `coverage` hot-apply (rebuild filter + adapter bbox on save) — bounded, testable, closes the region-routing loop end-to-end.
2. **Broad:** design the hot-apply framework and convert the whole GUI so Save just saves everywhere; restart becomes a last-resort scoped reconnect only. (Candidate for a local ultraplan.)
**Payoff:** kills ~90% of restart prompts (everything edited day-to-day) while keeping a safe, scoped reconnect for the handful of things that genuinely disrupt a live connection.

View file

@ -0,0 +1,87 @@
---
title: meshai Region × Family Routing — Implementation Plan
type: project
tags:
- mesh
related:
- [[meshai]]
- [[meshcore-transport]]
- [[meshai-fire-alerting-and-persistence]]
updated: 2026-07-07
status: proposed
---
# meshai — Universal Region × Family Routing: Final Implementation Plan
> Produced by a local multi-agent ultraplan (3 planners → 3 judges → synthesize → 3 red-team critics → finalize), verified against prod code at commit ceb95fb.
>
> **Scope principle (Matt, 2026-07-07): build the PLUMBING, not automation.** meshai exposes the mechanism — region-tagged events + a compact `region_routes` matrix the dispatcher honors + a plain editor to set it. **Matt does all configuration by hand:** defines the region boxes, provisions the radio channels, fills each family×region→channel cell himself. meshai auto-creates nothing — no channels, no regions, no routes, no generated rule objects. "No sprawl" comes from the config being ONE compact object he edits, not from automation.
## 1. Verdict
**GO, phased.** Achievable, mostly config/GUI once the keystone code gap is closed: **nothing currently writes `event.region` / `event.regions`** (`notifications/events.py:55-56` define them, the dispatcher *reads* them, zero writers exist). Close that and the existing region-scope machinery comes alive.
Three non-negotiables baked in:
1. **Matrix routing must resolve ABOVE the toggle `min_severity` floor.** Satpass + most road/511 events are `routine`; the toggle floor defaults to `priority` (`config.py:616`) and runs before delivery-plan build. Left naive, "Magic Valley Ham Radio" and road routing silently do nothing.
2. **Meshtastic has a hard 8-channel ceiling** (index 07); MeshCore routes by unbounded channel *name*. A 3×3 matrix is 9+ cells → the transports are asymmetric. Confront with an explicit MT-coarse / MC-fine policy.
3. **Everything ships behind default-inert flags on the GUARDED toggle path**, never the unguarded rules path.
Effort: **P1 = S (~½ day), P2 = S (~½ day), P3 = M (~23 days).** P1 deploys dark, safe to soak indefinitely.
## 2. Keystone: region tagging (P1)
Region source = `config.coverage.areas[].name` (named bboxes). Reject `mesh_intelligence.regions`/`RegionAnchor` (centroid+radius, wrong for polygon events; its taggers are dead code).
- `config.py:980` add `Coverage.region_tagging: bool = False`. **`coverage` is restart-required** (`config_routes.py:29-40`) — flip needs a container restart.
- `coverage_area.py`: add `MonitoringArea.name: str|None=None`; `areas_from_config` keeps the name; refactor the embedded geometry-extraction (`:193-209`) into a shared `_event_geom_json`; add an **additive** `matching_area_names()` (match-all, no early return — leaves `classify_geom_areas` gate byte-identical) + `event_region_names()`.
- `coverage_filter.py`: add `region_tagging` ctor flag; tag **before** the gate and **only when `not event.regions`** (never clobber satpass); wire in `build_pipeline` (`pipeline/__init__.py:121`).
- **Contract:** `regions` = all intersecting named areas (config order, deduped); `region = regions[0]` (deterministic, keeps cooldown key stable); 0 matches / no geom / parse error ⇒ `None/[]` ⇒ identical to today.
## 3. Routing model: new compact primitive on the guarded toggle path
Add `notifications.region_routes` (a `RegionRouteMatrix`: `enabled: bool=False`, `cells: dict` = family→region→{mt, mc, min_severity}) and resolve it inside `_dispatch_toggles` **above** the severity/region gates.
- **Reject generated per-cell rules:** the rules path is fully unguarded (no cold-start/staleness/dedup; `rule.cooldown_minutes` is dead code) → routing high-frequency families through it floods. It's also the O(R×F) object pile Matt wants gone.
- **Schema must be declared ABOVE `NotificationsConfig`** (no `from __future__ import annotations` → nested-dataclass coercion fires at class-def; else `NameError`). Add an explicit hydration branch mirroring `destinations` (`config.py:1149`).
- **Dispatcher matrix branch** (authoritative-on-match): resolves per event region → cell; per-cell `min_severity` (default `routine`) is the only floor; **bypasses `toggle.regions` and `toggle.min_severity`** (this is what lets routine families route); collapses same-channel overlap to one send but arms cooldown for every contributing region; `_toggle_to_rule` gains optional `broadcast_channel`/`meshcore_channel` overrides. `_audit_route` unchanged (one `mesh_broadcasts_out` row per transport).
- **Cooldown/dedup (matrix branch only; non-matrix path byte-identical):** per-region cooldown key `(toggle, category, region)`; per-`(source, id, transport, channel)` dedup (preserves retry when one region's channel fails + keeps fire growth stages flowing per channel). **Honest caveat:** `toggle.cooldown_seconds` defaults to `0` → out of the box matrix flood protection is **dedup-only**; rollout requires setting non-zero cooldown on high-frequency families (roads/511/wzdx). Keep `fire` cooldown=0 (dedup + growth suffix govern it).
## 4. Manual matrix editor (P3) — plumbing Matt fills in, no generation
**This is a plain editor over the `region_routes` primitive, NOT a generator.** It reads the current matrix, lets Matt assign cells by hand, and saves exactly what he entered. meshai computes/creates nothing — no rules synthesized, no channels/regions invented.
- **Backend** (`dashboard/api/notification_routes.py`): `GET /api/notifications/regions` (lists the region names that exist — coverage area names + satpass observer labels — so the picker offers what Matt already defined; the single routing region source; repoint pickers off the mesh-health `/api/regions`). **MUST read the SAVED (on-disk) coverage, not the in-memory config**`coverage` is restart-required, so the running config lags disk until restart; but adding/removing a *routing column* must NOT require a bot restart. Reading disk decouples the routing-UI region list (dynamic, instant) from the engine's coverage re-scoping (still restart-gated). Confirmed in preview: create/delete a region on the Coverage page → it appears/disappears in the matrix immediately, no restart; `GET /api/notifications/region-routing` (returns the matrix verbatim); `POST /api/notifications/region-routing` (**plain save via explicit server-side read-modify-write** — persists only what Matt entered; RMW because the section save rebuilds all of `NotificationsConfig` and would wipe toggles/rules/destinations otherwise; golden round-trip test is a release gate); optional read-only preview (shows which categories feed each family + the distinct MT-index count vs the 8 ceiling, so Matt sees what he still has to provision).
- **Frontend** (UNVERIFIED — `dashboard-frontend` not in the code checkout; re-verify `ChannelPicker`/`useFamilies`/`Notifications.tsx` first): `RegionRoutingMatrix.tsx` tab — rows=families (from `useFamilies`), cols=regions (from `/api/notifications/regions`), each cell = MT ChannelPicker + MC ChannelPicker + `min_severity` (default routine) + enable; sparse persistence (only cells Matt fills). Optional **manual** convenience fills (apply-down-a-region, copy-a-region) are just clipboard helpers Matt chooses to click — never automatic. Live MT-budget guard (warn at 8 distinct MT indices).
## 5. MC + MT per cell + the Meshtastic 8-channel ceiling
Each cell carries both `mt` (index) and `mc` (name) in one object; dual transport = one cell. **MT has a hard 8-slot firmware ceiling; MC names are unbounded.** Matt's target (Roads/Fires/Ham × SC-ID/SW-ID/East-ID = 9+ cells) can't get distinct MT channels.
**CHOSEN POLICY (Matt, 2026-07-07): MT axis = per-REGION "Alerts" channels; MC axis = fine per-region×family names.** On Meshtastic, one catch-all channel per region — **SWI Alerts, SCI Alerts, EI Alerts** (+ Magic Valley) — each carrying ALL families for that region, families distinguished by the existing emoji/text prefix (🔥/🚧/🛰️). On MeshCore, keep the full region×family split by name. Keeps region separation on MT (what Matt wants), gives up only per-family separation on MT (MeshCore retains it). Budget: 34 region channels + primary + admin, comfortably under 8. **Zero extra code** — it's just how the MT column is filled: set each region's MT index once and use the "apply down the region" fill so every family in that region → that region's Alerts index; the `mc` side stays per-cell.
Channel provisioning is NOT eliminated — meshai only *routes* to channels; every named channel (the 34 MT region Alerts channels + each MC region×family name) must still be hand-created on the Meshtastic device + MeshCore companion and subscribers pointed at it. The matrix kills the *rule-object* sprawl, not radio-side setup; the preview endpoint lists exactly what to provision.
## 6. Satpass special-case
Satpass has no geometry → tag it **upstream** so the never-clobber rule protects it. Refactor `env/satpass.py::to_event` to bind-then-return, set `regions=[entry_observer, exit_observer]` (ordered, deduped; friendly labels already at `satpass.py:164-165`). **Add `"satpass"` to `VALID_TOGGLES` (`categories.py:26`)** — hard prerequisite: `categories_for_toggle("satpass")` returns `[]` today only because satpass isn't in the toggle set. Observer friendly names must exactly match `coverage.areas[].name` to line columns up (e.g. "Magic Valley"); unmatched labels render their own column, never dropped. Routine severity routes fine because the matrix resolves above the floor.
## 7. Migration & back-compat
No migration; three default-inert levers (`MonitoringArea.name=None`, `region_tagging=False`, `RegionRouteMatrix.enabled=False`). **"Byte-identical" holds only while `region_tagging=False`.** Flipping it on activates two behaviors with no code change:
1. **Dormant region-scope activation** — every existing non-empty `toggle.regions` / `rule.region_scope` is dead today and activates. **Rules path is unguarded** → activated rules broadcast every sweep, replay backlog on restart.
2. **Cooldown cardinality shift** — the non-matrix cooldown key goes from global `"*"` to per-region → more broadcasts for multi-region feeds on any toggle with `cooldown_seconds>0`.
**Mandatory pre-flip audit (release gate):** dump prod config; list/disable/re-point every non-empty `rule.region_scope` (unguarded) and review every `toggle.regions`; ensure no matrix-routed family is also routed by an enabled rule (exclusivity); confirm per-region cooldown splits are acceptable. This is why P1 ships dark and soaks.
## 8. Test + safe rollout on CT108
Unit: `matching_area_names` (in-one/overlap-ordered/on-edge/outside/null/unnamed), shared geom helper, `coverage_filter` tag+gate + off-parity + satpass-preserve, `RegionRouteMatrix` round-trip golden, region-routing-save golden (toggles/rules/destinations survive), dispatcher matrix cases (routine routes, overlap one-send-both-cooldowns, per-region cooldown independence, per-channel dedup retry, authoritative-on-match, no-cell fall-through, cold-start/staleness still drop). Note: test harness builds NO `CoverageFilter` — matrix tests set `event.region/regions` manually.
Live (pct exec 108 from root@utility, compose in `/home/zvx/meshai/work/`): (1) deploy P1 dark + restart → zero change; (2) run pre-flip audit; (3) flip `region_tagging=True` + restart, matrix disabled → confirm regions populate, soak; (4) enable matrix, ONE cell (roads × SC-ID → throwaway channel, enabled=false first, cooldown>0) → confirm one landing on both + `cooldown_dropped` increments; (5) point real cells one family at a time. Keep `fire` cooldown=0.
## 9. Phases
| Phase | Effort | Ships |
|---|---|---|
| **P1 keystone tagging (dark)** | S ~½d | events carry region/regions; dormant scopes usable; zero routing change; safe soak |
| **P2 region source + one cell e2e (guarded)** | S ~½d | `/api/notifications/regions` + repoint pickers; satpass in VALID_TOGGLES + observer tagging; prove one family×region via `toggle.destinations` on a throwaway channel |
| **P3 compact primitive + manual matrix editor** | M ~23d | Matt fills the grid himself (dual MT+MC per cell); one compact object, no rule sprawl, nothing auto-created |
## 10. Open decisions for Matt
1. **Meshtastic 8-channel policy** — DECIDED: MT = per-region Alerts channels (SWI/SCI/EI [+ Magic Valley]), each carrying all families for its region; MC = fine per-region×family. Zero extra code (MT-column fill).
2. **Matrix severity semantics** — accept cells route all events ≥ cell `min_severity` (default routine) to both transports, bypassing per-severity `severity_channels` (chattier, but makes routine routing work).
3. **Cooldown posture** — non-zero on roads/511/wzdx before go-live; fire stays 0.
4. **Observer naming** — satpass observers named to match coverage area names.
**Files:** `config.py`, `coverage_area.py`, `notifications/pipeline/coverage_filter.py`, `notifications/pipeline/__init__.py:121`, `notifications/pipeline/dispatcher.py`, `notifications/categories.py:26`, `env/satpass.py`, `dashboard/api/notification_routes.py`, `dashboard/api/mesh_routes.py:254`, `dashboard-frontend/src/pages/RegionRoutingMatrix.tsx` (new, unverified repo).