The MeshCore routing cell's channel-vs-room toggle derived its mode
solely from whether the value started with `room:`, and the Room button
merely cleared the value -- so room mode was never entered, the room
<select> (gated on that) never rendered, and a room could never be
selected (chicken-and-egg: the select was the only writer of the `room:`
value). Add explicit per-cell mode state that the toggle sets directly
(falling back to value-derived on first render, so existing room: cells
load in room mode); Home now enters room mode and renders the dropdown
even with an empty value, Hash renders the channel input.
Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extends MeshCore routing so a region_routes `mc` cell can target a room
server, not just a `#`-channel. A room = a contact with type==3; a cell
value of `room:<pubkey>` routes to it via the existing addressed DM path
(send_msg to the room's pubkey), with an optional login for
password-protected rooms; a bare cell value stays a channel broadcast
(unchanged). Adds transport get_rooms()/login_to_room()/send_to_room_async,
a GET /api/meshcore/rooms endpoint, per-room password storage in
secrets_store (env MESHCORE_ROOM_<prefix>_PWD), and a routing-GUI
channel-vs-room picker (rooms shown by name with a path indicator).
Open rooms work end-to-end. Password-protected rooms need a follow-up:
a backend endpoint to SET the per-room password from the GUI (the generic
secrets API is allowlist-gated); the storage + login already exist.
Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two fire-scope cleanups:
1. Remove the fire digest feature entirely -- scheduler
(notifications/scheduled/fire_digest.py), pipeline wiring, the
fires.digest_* adapter_config key registrations, and the Fire Digest
dashboard UI (ScheduledBroadcasts / Environment / Reference /
AdapterConfig / ActivityLog). The unrelated generic per-rule
notification digest is kept. Orphaned fires.digest_* config rows and the
fire_digest_broadcasts table are left as inert data (v16 migration
untouched).
2. Add a coverage-scope gate at fire ingest: _ingest_fires now skips any
fire whose coordinates fall outside all configured coverage areas (same
areas_from_config + classify_geom_areas membership the dispatch-level
CoverageFilter uses), so out-of-coverage fires are never stored, tracked,
alerted, reminded, or re-ingested. Fails open when coverage is disabled,
has no areas, or excludes the fires adapter.
Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Meshtastic connection page listed channels twice (the pre-existing
Observe-Channels selector plus a redundant read-only Index/Name/Role
table); remove the redundant table and its now-unused getChannels() /
MeshtasticChannel API helpers. On the MeshCore companion page, move the
Channels (name/hash/key) card up to sit right after the Status card,
above the Advertising settings, so it is no longer buried at the bottom.
No behavior change; layout/UX only.
Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Remove the standalone Channels page and its nav entry (avoids menu bloat)
and relocate the channel listings onto the existing connection pages:
MeshCore channels (name, on-air hash, and the channel key/PSK with a copy
button) on the MeshCore companion page, and Meshtastic channels (index,
name, role) on the Meshtastic connection page. The MeshCore transport now
retains the channel_secret it already fetches at enumeration and
/api/meshcore/channels/detail returns it as `key` (hex PSK) so operators
can provision companion radios. known_channels() and /api/meshcore/channels
are unchanged.
Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a read-only Channels page listing both transports' channels with the
correct identifier per transport: Meshtastic by channel index (index,
name, role) from /api/channels, and MeshCore by channel name plus the
on-air hash from a new /api/meshcore/channels/detail endpoint. The
MeshCore transport now retains the channel_hash it already fetches at
enumeration (previously discarded); known_channels() and the existing
/api/meshcore/channels endpoint are unchanged. Adds a Channels nav entry
and /channels route.
Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Split the single region_routes.enabled master switch into per-transport
mt_enabled (Meshtastic) and mc_enabled (MeshCore) flags so the two
transports can be region-routed independently. Previously the shared
switch forced MeshCore into the region matrix; with all mc cells null it
routed MeshCore nowhere instead of falling through to the toggle-level
meshcore_channel. The dispatcher is now authoritative per-transport: a
disabled transport falls through to its toggle path, and matched-but-
inactive cells still suppress the toggle for enabled transports. The
destinations delivery branch also honors matrix-handled suppression to
prevent double-broadcast. Loader maps legacy enabled:true to
mt_enabled:true, mc_enabled:false. Each GUI routing page gains its own
master enable toggle.
Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extends TEXT_HINTS with three missing legacy prefixes so NULL-source audit rows
render the correct family label in the Activity Log UI:
- ⏳ (U+23F3) → Weather (legacy composer weather-watch: "⏳ WX: …")
- 🌡️ → Weather (NWS heat-alert format added in PR #96)
- ⚠️ Road Incident (U+26A0+FE0F) → Traffic (legacy incident_handler rows)
Placed more-specific prefix before shorter variants to avoid false matches.
DB backfill: 0 residual NULL rows found (all rows already tagged after PR #99);
no audit-table writes performed. No mesh transmission.
Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Backend: add _SOURCE_TO_TABLE class constant in Dispatcher mapping event.source
("nws", "nifc", "wzdx", "traffic", "511") to canonical audit table names.
_post_broadcast_commit now falls back to this map when _broadcast_audit is
absent/None, so native env adapter sends (nws.py, fires.py, wzdx.py,
roads511.py, traffic.py) write a non-NULL source_event_table instead of NULL.
Existing _broadcast_audit paths (Central handlers, scheduled broadcasts) are
unchanged.
Frontend: replace naive familyLabel() string transform with explicit
TABLE_LABELS lookup (10 known tables → friendly names) plus a TEXT_HINTS
emoji-prefix heuristic for legacy NULL-source rows, so historical orphan rows
still display a meaningful label before/after backfill.
Tests: three new unit tests in test_dispatcher_persistence.py covering the
fallback path (nws→nws_alerts), the full _SOURCE_TO_TABLE map, and that
explicit _broadcast_audit is never overridden by the fallback.
Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(meshcore): opt-in telemetry auto-poll on selected contacts
req_telemetry + a poller for selected contacts (meshcore_telemetry_contacts,
interval with a min floor, availability detection). Contacts page gains
per-node auto-poll toggles + battery/sensor readouts + Poll-now, and maps
numeric contact type codes to Chat/Repeater/Room/Sensor badges.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(meshcore-telemetry): reconcile with current transport/meshcore-lib API
- Add EventType.ACK + NEW_CONTACT to the telemetry test's fake module;
_setup_subscriptions() subscribes to both (added in main before rebase)
and the stale stub caused all three TestPollerScheduler tests to abort
with AttributeError on connect().
- Same NEW_CONTACT gap fixed in test_meshcore_conn_type.py and
test_meshcore_dm_delivery.py — these ran first (alphabetically) via
setdefault, contaminating the shared sys.modules["meshcore"] stub for
all downstream test files and causing 9 extra connect()-path failures
suite-wide (TestPeriodicAdvertScheduler, TestAdvertOnConnect, etc.).
- req_telemetry_sync(contact, min_timeout=5) matches the installed lib
(meshcore-2.3.7 binary.py) exactly — no production-code change needed.
- All 30 test_meshcore_telemetry tests pass; full-suite failures drop
16 → 7 (remaining 7 are pre-existing, unrelated to telemetry).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(region-routing): P1 tagging + region_routes primitive + read/write API + preview launcher
- config.py: add Coverage.region_tagging (bool=False); add RegionRouteMatrix
dataclass (enabled, cells) above NotificationsConfig; add region_routes field
to NotificationsConfig; add explicit hydration branch for region_routes in
_dict_to_dataclass mirroring destinations pattern.
- coverage_area.py: add MonitoringArea.name (str|None=None, frozen); update
areas_from_config to preserve name; refactor inline geom extraction from
classify_event_areas into shared _event_geom_json helper; add
matching_area_names(geom_json, areas)->list[str] (additive, all named
matches, config-order, deduped; gate unchanged); add event_region_names
convenience wrapper.
- coverage_filter.py: add region_tagging ctor kwarg; stamp event.region/
regions before the gate when region_tagging=True and areas non-empty and
not event.regions (never clobbers satpass preset).
- pipeline/__init__.py: wire region_tagging into CoverageFilter construction.
- notification_routes.py: add GET /notifications/regions (named coverage area
names, config-order, deduped); GET /notifications/region-routing (matrix as
JSON); POST /notifications/region-routing (explicit RMW — only region_routes
changes, toggles/rules/destinations survive).
- scripts/preview_dashboard.py: mesh-free launcher — dashboard API only, no
mesh connector, no broadcast loop; vite runs separately.
All 87 coverage tests pass; 300 total pass; 6 pre-existing failures unchanged
(adapter config count mismatch + MeshCore EventType.NEW_CONTACT).
* feat(region-routing): manual region x family matrix editor page
Adds RegionRoutingMatrix.tsx — a plain editor over the region_routes
config primitive. Rows = families (via useFamilies()), cols = regions
(from GET /api/notifications/regions). Each cell exposes MT channel
(ChannelPicker single + includeDisabled), MC channel name (text input),
min_severity select (routine/priority/critical/immediate), and an enabled
checkbox. Only cells where MT or MC is set are included in the sparse
POST payload. Master enable toggle maps to top-level enabled. MT budget
guard warns when more than 7 distinct MT indices are in use. Sticky
family column; horizontal scroll for wide region sets.
Registers route /region-routing in App.tsx and adds "Region Routing"
nav entry (Map icon) under the Meshtastic section in Layout.tsx,
immediately after Routing.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(region-routing): regions endpoint reads saved (disk) coverage so routing columns are dynamic without a bot restart; preview reloads config after writes
* feat(routing): unify MT/MC routing into per-family cards; region routing as an in-card expand; remove rules/destinations UI + standalone page
* refactor(routing): move Meshtastic Routing from /notifications to /meshtastic/routing (mirror /meshcore/routing); redirect legacy path
* feat(region-routing): dispatcher honors region_routes matrix (authoritative-on-match, per-region cooldown, per-channel dedup); non-matrix path unchanged
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(region-routing): matrix dedup key must match boot-restore 2-tuple form (prevents restart re-broadcast flood); regression test
---------
Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
The MeshAI/1.0 UA intermittently trips WAFs (Idaho Power's Azure Front Door
403s it ~2/30; a browser UA gets 200 every time). Default the adapter +
preview to a browser User-Agent, retry once on 403/429, and add optional
per-source custom headers (UA/auth) editable in the GUI. Makes WAF'd and
keyed feeds pollable.
Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a Destinations manager to the Routing page — define each delivery target
(mesh channel / email / webhook / digest) once — and a destination picker on
each family and rule. The duplicated per-family inline email/webhook editors
move under an Advanced/legacy disclosure. De-fragments delivery config: no
more configuring the same email in two places.
Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Move the generic-source editor into a reusable GenericSourcesEditor and render
it as a "Custom Sources" section on the Data Feeds (Environment) page, so a
custom source sits with the built-in feeds instead of an orphan page. Remove
the standalone /data-sources route + nav. Custom sources are now managed as
first-class data feeds; enable their family in Notifications to route them.
Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add GET /api/notifications/families and make the Routing UI merge the static
built-in families with registered dynamic families, so a generic source's
family appears as a toggle the operator can enable and assign a delivery
destination (mesh/meshcore/email/webhook). Closes "there's nowhere to
broadcast it" — a custom source is now routable from the GUI. Data Feeds fold
+ orphan-page retirement follow in B2.
Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Whitelist generic_sources as a config section and add a Data Sources page:
add/edit/delete sources with the full field-mapping UI (items/id/lat/lon/
geometry/title paths + field_mappings list + summary template) and a
server-side URL Preview that shows the endpoint's JSON so operators can map
fields without knowing the structure ahead of time. Makes the generic
adapter truly no-code — point it at any public REST/GeoJSON feed from the GUI.
Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Activity Log endpoint wasn't reading mesh_broadcasts_out, so it only
surfaced a partial set (MT band-propagation + satpass) and missed the
event-driven weather broadcasts and the entire MeshCore side. Query
mesh_broadcasts_out for all broadcasts across both transports and all
categories, newest-first with pagination, so the feed reflects everything
that actually went to the mesh.
Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Upgrades the Coverage page from one bbox to a list of named areas, matching
the backend coverage.areas / Shapely set-union gate. Draw multiple boxes,
name/edit/delete each, all rendered on the map; saves config.coverage.areas
(clears legacy bbox). Coords rounded to 6dp. Enabled + per-adapter override
toggles unchanged.
Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds a GENERAL > Coverage page: a Leaflet map to draw/set the universal
coverage bbox (with numeric W/S/E/N inputs), an enabled toggle, and per-
adapter override toggles (excluded_adapters). Environment's per-adapter
geographic-scope fields now show only when that adapter is overridden;
otherwise they point to the Coverage map. Saves via PUT /api/config/coverage.
Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Enable firmware auto-add (set_autoadd_config CMD 58) at connect when
connection.meshcore_auto_add_contacts is set (default on), and refresh
the contact roster on NEW_CONTACT so replies resolve immediately. GUI
toggle on the MeshCore Connection page. So the USB AIDA companion adds
every node it hears an advert from and can send/decrypt DMs without
manual contact exchange.
Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The banner said 'Container restart required' and a code comment claimed 'the
container will tear down' — both wrong. /api/restart touches /tmp/meshai_restart,
which the entrypoint watches to restart the BOT PROCESS in place (~seconds); the
container is not recreated. Fix the copy + point users at the existing 'Restart
now' button. Also mention transport connections in the restart-required list.
Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
MeshCore can now connect over USB serial (and BLE) directly, not just TCP to
the pyMC companion. The meshcore lib already supported create_serial/create_ble;
we just wire it up. Plus a USB auto-detect scanner that resolves stable device
paths to fix ttyACM enumeration hopping across replug/reboot.
Backend:
- ConnectionConfig: meshcore_conn_type (tcp|serial|ble, default tcp),
meshcore_serial_port, meshcore_baud=115200, meshcore_ble_address (validated)
- meshcore_transport._do_connect dispatches per mode: serial ->
MeshCore.create_serial(port, baudrate, auto_reconnect, max_reconnect_attempts),
ble -> create_ble(address or None), tcp -> create_tcp (unchanged). Mode-aware
logging/reconnect. Transport otherwise unchanged (mode-agnostic once _mc exists).
- factory.meshcore_enabled(config): active when the selected mode is configured
(serial port / ble address / tcp host); back-compat — meshcore_host + default
tcp still activates exactly as before.
- serial_ports.list_serial_ports(): pyserial comports + stable_path resolution
by-id -> by-path -> raw (by-id keyed on USB serial = stable across replug),
likely_radio flag by VID (RAK/nRF/CP210x/CH340), excludes legacy ttyS*, never
raises. GET /api/serial-ports (+ container by-id passthrough hint).
Frontend:
- SerialPortPicker component: "Detect USB devices" -> lists ports (likely-radio
badge, shows stable_path) -> onChange sets the stable by-id path; manual text
fallback; empty/error/note states.
- MeshCore Connection: type selector TCP/Serial/BLE + per-mode fields (serial
picker + baud; ble address). Meshtastic serial branch now uses the picker too.
Code-ready; not activated (defaults keep TCP). 35 new tests; suite at 10-failure
baseline. Container needs /dev/serial passed through for by-id paths.
Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The banner used a static META.hasKey flag (false for firms+roads511), so
those always showed 'API key not configured' even when keys were set —
contradicting the ManagedSecret SET badge. Now fetch /api/secrets and show
the banner only when a keyed adapter's secret is genuinely unset; copy
softened to 'API key required — set it in the field below'. wzdx kept keyless
(no banner). Keyless adapters never banner.
Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The 100%-coverage pass added timezone to FullConfig, but it's a top-level
scalar (own PUT /api/config/timezone), not a section tab — so SectionKey =
keyof FullConfig broke SECTION_DESCRIPTIONS: Record<SectionKey,string>.
Exclude 'timezone' from SectionKey. Docker frontend build failed on this;
live container was untouched.
Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add bound GUI controls for every remaining user-facing + internal config
setting so the dashboard is the complete config surface (secrets stay in
.env via the ManagedSecret widgets). Per the exhaustive per-key audit.
19 GAP controls (real settings with no prior control):
- Config: global timezone (+ backend: timezone in VALID_SECTIONS + scalar
save_section branch), commands.custom_commands (kv editor), knowledge
sparse_host/port, alert_rules.high_util_hours
- Data Feeds: nws.areas, usgs.flood_thresholds (json), usgs_quake
feed_url/min_magnitude(native floor)/bbox, wzdx.registry_ttl,
satpass.broadcast_lead_seconds, central.connect_timeout,
toggles.<family>.regions (un-hidden + save-merge fix)
- Notifications: band_conditions_tz, digest.schedule, digest.include
- Danger Zones: snow.enabled + buffer_mi (unlocked)
18 INTERNAL knobs (Advanced subsections):
- connection reconnect/timing (7) split across MT/MeshCore Connection
- environmental.geocoder url/timeout/radius/limit (4)
- identity.contact_email (+ LOCAL_FIELDS mirror -> local.yaml; still feeds
NWS User-Agent); mesh_sources url + regions lat/lon already editable
- toggles.<family>.name/webhook_headers, danger_zones.webhook_headers
(new reusable KeyValueInput component)
alert_node_ids intentionally not surfaced (synthetic local default with no
dataclass home; overlaps the editable per-rule node_ids). No dead controls.
Frontend validated at Docker build. Backend py_compile OK.
Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
API keys/secrets now live in /data/secrets/.env (gitignored, never in config
YAML), while remaining fully editable from the dashboard. Config YAML holds
only ${VAR} references.
Backend:
- meshai/secrets_store.py: get_status (SET/NOT-SET, never values), set_secret,
delete_secret over /data/secrets/.env (resolved like load_config); authoritative
SECRET_FIELD_TO_ENV map (traffic→TOMTOM_API_KEY, firms→FIRMS_MAP_KEY,
roads511→ROADS511_API_KEY, wzdx→WZDX_API_KEY, smtp→SMTP_PASSWORD,
mesh_sources→MESHMONITOR_API_TOKEN) + backend-dependent llm_env_var
- dashboard/api/secrets_routes.py: GET /api/secrets (status only), PUT/DELETE
/api/secrets/{env_var} (validated, restart_required); registered in server.py
- config_loader: save_section preserves ${VAR} secret refs on section save
(never rejects them); EXPECTED_SECRETS += ROADS511_API_KEY, WZDX_API_KEY
- config.example.yaml + docker-entrypoint default config use ${VAR} refs;
first-run bootstraps /data/secrets/.env; .gitignore covers it
Frontend:
- components/ManagedSecret.tsx: masked, Set/Not-set badge, reveal, Save->PUT,
"restart required"; carries no config value so secrets never enter a section
save payload
- wired into Environment (tomtom/roads511/wzdx/firms), Config LLM tab
(env var by backend), Notifications (smtp)
Restart required after a secret change (env read at config-load). 11 store
tests; suite at 10-failure baseline (1714 passed).
Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Audit confirmed the Rich TUI (`meshai --config`) is a strict subset of the
dashboard GUI (covers FEWER sections) and nothing depends on it for bootstrap.
Strip it so the dashboard is the single config surface.
- delete meshai/cli/configurator.py (~1435 lines); cli/__init__ minimal
- main.py: drop --config + run_configurator dispatch (keep --config-file);
bootstrap log now points to config.example.yaml / the dashboard
- docker-entrypoint.sh: remove the ttyd `meshai --config` block on :7682
(default-config heredoc + bot exec loop untouched)
- docker-compose.yml + Dockerfile: drop the 7682 port + ttyd install (EXPOSE 8080)
- README / config.py header / config.example.yaml: de-reference the TUI
- drop `rich` from pyproject + requirements (grep-verified unused)
Close the only GUI config gap the audit found:
- Config.tsx weather tab: add openmeteo.url + wttr.url inputs (backend already
round-trips the nested WeatherConfig dataclasses)
Bootstrap intact: config.example.yaml copy, entrypoint default-config write,
and defaulted Config() when no file exists all remain. `python3 -m meshai`
and `--config-file` unchanged. TS/import validated at Docker build.
Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Close the GUI gaps so a standalone (all-native) deployment needs no
hand-editing of /data/config.yaml:
- Environment.tsx: native satpass (SGP4) section — observers editor
({slug,name,lat,lon,alt_m} list, mirrors traffic corridors), tle_groups,
norad_ids, min_elevation_deg, window_hours, tle_refresh_seconds (all
environmental.satpass YAML). Central adapter_config satpass panel unchanged.
- Environment.tsx: wzdx states + registry_url added (were file-only).
- BUG FIX: the satpass enable toggle wrote adapter_config.satpass.enabled
(Central), but env/store.py gates the native adapter on
environmental.satpass.enabled — toggle now writes the native/YAML layer.
Backend round-trips automatically (SatpassConfig/WZDxConfig already declare
all fields; _dict_to_dataclass whitelists by field). Frontend-only change.
NOTE: TS build (tsc && vite build) not runnable offline (node_modules absent);
validated at Docker build / deploy time.
Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Config → Settings → Context: relabel the raw "Max Age (sec)" field to
"Chat context retention (days)" (days<->seconds conversion, min 1,
default 14). Governs the shared per-mesh chat memory window.
- Make PUT /api/config/context apply LIVE: MeshContext.update_settings()
updates max_age/observe_channels/ignore_nodes in place; config_routes
refreshes the running MeshContext via app.state.mesh_context (mirrors
the existing _refresh_toggle_filter pattern) so retention changes take
effect without a restart.
Tests: +tests/test_context_hot_reload.py (10); 0 new failures.
Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- router.should_respond branches on message.transport: MeshCore DMs are
governed solely by meshcore_context.respond_to_dms (enforced at the
transport); Meshtastic solely by bot.respond_to_dms. No global gate.
- MeshObservation tagged per-transport; the LLM "recent traffic" block is
scoped to the originating mesh (keyword override for the other mesh),
labeled by mesh so the model knows which it is describing.
- MeshCore observe_channels is now opt-in (empty = observe none).
- Chat-context retention 30d -> 14d (both meshes).
- Meshtastic integer channel-index filter no longer misapplied to MeshCore
observations (their channel is a companion slot index).
- Frontend: relabel DM toggles per-mesh ("Answer direct messages",
Meshtastic-only / MeshCore-only), remove the false channel-mention
tooltips, opt-in wording for MeshCore observe-channels.
Tests: +tests/test_llm_scoping.py (10), context-filter updated for opt-in;
0 new failures (34 pre-existing).
Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(dashboard): MeshCore Contacts roster + Companion status (read-only)
Expose the live companion's contact roster (get_contacts) and self/channel
status via /api/meshcore/contacts + /api/meshcore/self. Fill the Contacts
(roster table) and Companion (status + channels) pages. Telemetry auto-poll
comes next.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(meshcore): self-advertisement (send-advert + advert-on-connect + periodic)
AIDA now announces itself: send_advert(flood=True) on every connect, an
optional periodic auto-advert (meshcore_advert_interval_seconds), and a
manual "Send Advert" button + POST /api/meshcore/advert. Makes the
companion discoverable/DM-able on the mesh.
---------
Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(dashboard): Phase A — 4-section nav; move Scheduled Broadcasts + Danger Zones off Routing
Regroup nav into GENERAL/MESHTASTIC/MESHCORE/DOCUMENTATION (<=5 pages each,
MT & MC mirror). Consolidate via tabs (Places, Nodes & Health, Contacts &
Companion) reusing existing components. Move Band Conditions, cold-start,
and fire digest to per-mesh Scheduled Broadcasts pages; move Danger Zones
to its own page. Routing keeps its sending rules unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(dashboard): Phase B — clean identical Routing grids; relocate per-family gating to Data Feeds
Meshtastic Routing becomes an always-visible pure-delivery grid matching
MeshCore (no master-toggle expand/collapse). Per-family gating (enable/
severity/freshness/cooldown) moves to a Family Settings section on Data
Feeds. Sending rules + Notification Rules unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(dashboard): Phase C — MeshCore bot-behavior parity (observe channels / ignore contacts / DMs)
Add meshcore context (observe channels by name, ignore contacts, DM policy)
and wire the MeshCore inbound path to honor it, mirroring Meshtastic's
observe/ignore filtering. Symmetric "Bot behavior" sections on both
Connection pages. Meshtastic path unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(dashboard): Phase D — dedupe Environment/Adapter Config into one Data Feeds surface
Curated family panels are the single home for the shared adapter keys;
Adapter Config becomes an Advanced/raw escape hatch (owned keys no longer
double-editable). Surface include_in_llm_context per adapter. Fix the
adapter-config array-vs-object parsing (fire digest values now load).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(dashboard): Phase E — Activity Log (per-mesh broadcast feed); remove subscription backend
Replace Alerts with an Activity Log fed by per-mesh broadcast logging
(transport+channel+success on mesh_broadcasts_out, additive migration).
Remove the entire subscription backend (commands, DM dispatch, storage,
API) and its UI.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add POST /api/mesh/test-send (fire a labeled test broadcast on a chosen
mesh+channel via the live transport) and GET /api/meshcore/channels
(surface the companion's enumerated channel names). "Send test message"
cards on both Connection pages, with the MeshCore one listing real
channels. Lets the operator confirm a mesh's send path on demand.
Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(transport): derive active transports from config, drop transport setting
A mesh is active when its connection is configured: Meshtastic is the
always-on base; MeshCore runs whenever meshcore_host is set (blank = off);
both configured = both. Removes the transport mode field/toggle entirely
so there's no separate flag to miss.
* docs: fix stale transport comment after field removal
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(routing): MeshCore as first-class delivery types (meshcore_broadcast/dm)
Replace the composite auto-fan with explicit per-mesh delivery types so
each family independently controls broadcast/DM per severity on Meshtastic
AND MeshCore. mesh_broadcast->Meshtastic only, meshcore_broadcast->MeshCore
(by channel name), mesh_dm/meshcore_dm likewise; routing via the existing
transport hint. Adds meshcore_dm_contacts. Meshtastic-only configs
unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(routing): deliver meshcore_broadcast via meshcore_channel through CompositeTransport
The hinted _broadcast path passed the channel NAME on the `channel` kwarg,
which MeshCoreTransport ignores (it reads meshcore_channel), so
meshcore_broadcast silently no-op'd on transport=both configs. Route the
meshcore child via meshcore_channel and the meshtastic child via channel.
Fix the test that asserted the broken kwarg layout. Add the new delivery
types to the remaining enumeration/validation sites (channel-test endpoint,
scheduler digest chunking, danger-zone valid set).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(dashboard): split Notifications into Meshtastic and MeshCore sections
Delineate per-mesh routing: each family configures Meshtastic delivery
(mesh_broadcast/mesh_dm, channel index, node IDs) and MeshCore delivery
(meshcore_broadcast/meshcore_dm, channel name, contacts) in separate
sections; shared settings (enable/severity/regions/email/webhook) once.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(dashboard): first-class MeshCore nav section + dedicated pages
Group the sidebar into Meshtastic and MeshCore sections. Promote MeshCore
routing and connection to their own pages; move MeshCore routing out of
Notifications (which stays Meshtastic + shared family settings). Add
placeholder Contacts and Companion pages for the follow-on companion data
API. No backend change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(dashboard): parallel MT/MC nav order + symmetric connection links
Order both nav groups Connection/Routing/Mesh(Contacts)/Sources(Companion).
Replace the prominent MeshCore block on the Meshtastic Connection page with
a single subtle cross-link, mirrored on the MeshCore Connection page.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(dashboard): focused Meshtastic Connection/Sources pages for MT/MC parity
Meshtastic Connection and Sources are now their own focused pages
(mirroring MeshCore), instead of deep-linking into the full Config page.
Global settings move to a restored top-level Config item. No duplicate
editors; connection cross-links are mirror-image between the two pages.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(dashboard): MeshCore transport + per-family routing GUI controls
Add Transport mode selector (Meshtastic/MeshCore/Both) and MeshCore
host/port fields to the Config Connection section, and an independent
per-family "MeshCore channel" number input in Notifications (blank = not
broadcast on MeshCore, sends null). Extends the ConnectionConfig and
per-family toggle TS types.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(routing): MeshCore routing by channel name, not index
MeshCore channels are {name,PSK} (up to 40+ slots, not Meshtastic's 0-7).
The send index is a fragile slot position, so store the channel NAME per
family and resolve name->slot against the companion's live channel table
at send time; never blind-send to an unresolved slot. GUI field becomes a
channel-name text box. meshtastic path unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(routing): thread per-family meshcore_channel through the broadcast send path
MeshBroadcastChannel now carries the rule's meshcore_channel name and
passes it to send_message, so per-family MeshCore routing actually fires
end-to-end (dispatcher -> channel -> composite -> MeshCoreTransport).
Meshtastic path unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- alert on any hazard touching a node (severity gate removed)
- alert regardless of position staleness; only skip nodes with no position
- snow tabled: grayed-out in GUI + skipped in correlator pending snowfall+elevation pipeline
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Move all application source (meshai/, dashboard-frontend/, tests/,
config/, docs/, Dockerfile, etc.) into work/ directory
- Add Node.js multi-stage build to Dockerfile for frontend compilation;
remove compiled static assets from git tracking
- Fix satpass missing time windows: consolidation was splitting wire on
newline and only putting line 1 in event.title, dropping the time
window line that the composer uses for precomposed broadcasts
- Fix satpass burst flooding: stagger consolidation timers (+60s per
pending pass) so Central batch publishes don't blast the mesh
- Update CI workflow build context to work/
- Anchor lib/ and data/ gitignore patterns to repo root to prevent
false matches on nested directories
- Add dashboard-frontend/node_modules/ to .dockerignore
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>