Commit graph

6 commits

Author SHA1 Message Date
2543dfa8d1
fix(config): warn on unknown config keys; delete 7 phantom keys from the example (#146)
* fix(config): warn on unknown config keys instead of silently dropping them

_dict_to_dataclass() silently continue'd past any key not in the target
dataclass's field set -- an operator could set a config key, restart, and
have it vanish with zero feedback (config.example.yaml's phantom
mesh_intelligence keys are exactly this bug, fixed separately).

Now logs a WARNING naming the key and the dataclass, hinting at a typo or
a renamed/removed field, via the module's existing _config_logger.

Traced every dynamic/free-form config path to rule out false positives:
notifications.toggles, notifications.destinations, generic_sources,
mesh_sources, and notifications.rules all route through explicit
dict-of-dataclass or verbatim-passthrough handling and never spuriously
warn. Two legitimate legacy shapes DO hit the strict field-check path with
keys that were never (and will never be) dataclass fields:

- notifications.channels (pre-v0.5 channel list), consumed directly from
  the raw dict by _migrate_legacy_channels
- notifications.region_routes.enabled (pre mt/mc-split master switch),
  read directly by the explicit region_routes handler

Both are allowlisted in _KNOWN_LEGACY_DROP_KEYS so users mid-migration
don't get spurious noise on every load.

Added tests/test_config_loader.py coverage: unknown key warns and does
not raise, both legacy shapes stay silent, and the free-form/dynamic
sections never warn for keys valid on their real target shape.

* fix(config): remove phantom config.example.yaml keys, add missing live ones

The new unknown-key warning (previous commit) caught config.example.yaml
loading with SEVEN warnings, all real drift -- none were false positives
of the warning itself:

mesh_intelligence block shipped three keys with no MeshIntelligenceConfig
field and no implementation anywhere (git log -S confirms they were never
built, not leftovers from a removal):
  - region_radius_miles, infra_overrides, region_labels
These only made sense under an older auto-clustering design; what
actually exists is explicit region anchors (regions: list[RegionAnchor]).
Deleted from both the live block and the commented-out example above it,
and added the four fields that DO exist and are live but were missing
from the example: regions, critical_nodes, alert_channel, alert_rules.

Also fixed the now-misleading comment at
dashboard/api/mesh_routes.py:281, which referenced region_labels --
comment only, no code change.

notifications block shipped a whole quiet-hours subsystem that was
deliberately ripped out (commit b948ed77, "silent is better than ugly")
and never implemented as override_quiet -- confirmed by zero readers and
zero dataclass fields anywhere in meshai/:
  - quiet_hours_enabled, quiet_hours_start, quiet_hours_end
  - override_quiet (on 4 rule entries, including "Emergency Broadcast",
    which falsely implied emergency alerts bypass quiet hours)
Deleted; no quiet-hours feature implemented (out of scope -- product
decision for the owner).

config.example.yaml now loads with exactly zero warnings: the loader and
the example finally agree.

---------

Co-authored-by: Matt Johnson <mj@k7zvx.com>
2026-07-17 14:07:23 -06:00
3495eb31de
feat(secrets): GUI-managed .env secrets store — keys are config, but gitignored (#47)
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>
2026-07-05 17:57:45 -06:00
a502778990
chore(config): delete 51 dead keys, fix usgs_quake floor, secret-flag consistency (#46)
Backend half of making the dashboard the complete config surface (per an
exhaustive per-key audit).

Delete 51 vestigial/unread config fields (load-safe: _dict_to_dataclass
whitelists by field, so existing files carrying these keys still load and the
keys drop on next save):
- 36 duplicated MQTT block (host/port/username/password/topic_root/use_tls)
  on memory/context/commands + env nws/swpc/ducting (grep-proven unread;
  mesh_sources keeps its real MQTT fields)
- 3 no-op history cleanup keys (auto_cleanup/cleanup_interval_hours/max_age_days)
- 5 alert scaffolding (alert_cooldown_minutes, RegionAnchor.nws_zones,
  battery_{warning,critical,emergency}_voltage)
- 5 danger-zone non-fire min_acres (kept fire.min_acres via a fire subclass)
- 2 deprecated adapter_config keys (nws.broadcast_severities/warning_suffix_promotes)

Bug: usgs_quake native magnitude floor was unreachable from the GUI (native
reads config.min_magnitude; the GUI "Global Floor" wrote the registry
global_mag_floor that only the Central path reads). Reconciled: min_magnitude
is the canonical native floor the frontend will bind; registry floors marked
Central-path-only. Effective filtering unchanged.

Secret-flag consistency: add environmental.roads511.api_key + wzdx.api_key to
SECRET_FIELDS (secrets move to .env in the follow-up; ${VAR} interpolation kept).

Suite at 10-failure baseline (1703 passed).

Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 17:34:27 -06:00
5de683f7a7
chore: remove legacy TUI configurator; surface last config gap in GUI (#45)
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>
2026-07-05 16:21:32 -06:00
a191200a12
feat(meshcore): decouple per-mesh LLM DM gate + mesh-scoped chat context (#19)
- 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>
2026-07-03 18:39:26 -06:00
Ubuntu
2e1fb325f7 refactor: move source tree into work/, multi-stage Docker build, fix satpass
- 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>
2026-06-16 03:40:31 +00:00