2026-06-16 03:40:31 +00:00
|
|
|
# MeshAI Configuration
|
|
|
|
|
# LLM-powered Meshtastic assistant
|
|
|
|
|
#
|
|
|
|
|
# Copy this to config.yaml and customize as needed
|
|
|
|
|
# For Docker: mount as /data/config.yaml
|
|
|
|
|
|
|
|
|
|
# === BOT IDENTITY ===
|
|
|
|
|
bot:
|
|
|
|
|
name: ai # Bot's display name
|
|
|
|
|
owner: "" # Owner's callsign (optional)
|
|
|
|
|
respond_to_dms: true # Respond to direct messages
|
|
|
|
|
filter_bbs_protocols: true # Ignore advBBS sync/notification messages
|
|
|
|
|
|
|
|
|
|
# === MESHTASTIC CONNECTION ===
|
|
|
|
|
connection:
|
|
|
|
|
type: tcp # serial | tcp
|
|
|
|
|
serial_port: /dev/ttyUSB0 # For serial connection
|
|
|
|
|
tcp_host: localhost # For TCP connection (meshtasticd)
|
|
|
|
|
tcp_port: 4403
|
|
|
|
|
|
|
|
|
|
# === RESPONSE BEHAVIOR ===
|
|
|
|
|
response:
|
|
|
|
|
delay_min: 2.2 # Min delay before responding (seconds)
|
|
|
|
|
delay_max: 3.0 # Max delay before responding
|
|
|
|
|
max_length: 200 # Max chars per message chunk
|
|
|
|
|
max_messages: 3 # Max message chunks per response
|
|
|
|
|
|
|
|
|
|
# === CONVERSATION HISTORY ===
|
|
|
|
|
history:
|
|
|
|
|
database: /data/conversations.db
|
|
|
|
|
max_messages_per_user: 50 # Messages to keep per user
|
|
|
|
|
conversation_timeout: 86400 # Conversation expiry (seconds, 86400=24h)
|
|
|
|
|
|
|
|
|
|
# === MEMORY OPTIMIZATION ===
|
|
|
|
|
memory:
|
|
|
|
|
enabled: true # Enable rolling summary memory
|
|
|
|
|
window_size: 4 # Recent message pairs to keep in full
|
|
|
|
|
summarize_threshold: 8 # Messages before re-summarizing
|
|
|
|
|
|
|
|
|
|
# === MESH CONTEXT ===
|
|
|
|
|
context:
|
|
|
|
|
enabled: true # Observe channel traffic for LLM context
|
|
|
|
|
observe_channels: [] # Channel indices to observe (empty = all)
|
|
|
|
|
ignore_nodes: [] # Node IDs to exclude from observation
|
2026-07-03 18:39:26 -06:00
|
|
|
max_age: 1209600 # Max age in seconds (default 14 days)
|
2026-06-16 03:40:31 +00:00
|
|
|
max_context_items: 20 # Max observations injected into LLM context
|
|
|
|
|
|
|
|
|
|
# === LLM BACKEND ===
|
|
|
|
|
llm:
|
|
|
|
|
backend: openai # openai | anthropic | google
|
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
|
|
|
api_key: ${GOOGLE_API_KEY} # Secret: set via dashboard or /data/secrets/.env (var depends on backend: GOOGLE_API_KEY/OPENAI_API_KEY/ANTHROPIC_API_KEY)
|
2026-06-16 03:40:31 +00:00
|
|
|
base_url: https://api.openai.com/v1 # API base URL
|
|
|
|
|
model: gpt-4o-mini # Model name
|
|
|
|
|
timeout: 30 # Request timeout (seconds)
|
|
|
|
|
system_prompt: >-
|
|
|
|
|
You are a helpful assistant on a Meshtastic mesh network.
|
|
|
|
|
Keep responses very brief - 1-2 short sentences, under 300 characters.
|
|
|
|
|
Only give longer answers if the user explicitly asks for detail or explanation.
|
|
|
|
|
Be concise but friendly. No markdown formatting.
|
|
|
|
|
google_grounding: false # Enable Google Search grounding (Gemini only, $35/1k queries)
|
|
|
|
|
|
|
|
|
|
# === WEATHER ===
|
|
|
|
|
weather:
|
|
|
|
|
primary: openmeteo # openmeteo | wttr | llm
|
|
|
|
|
fallback: llm # openmeteo | wttr | llm | none
|
|
|
|
|
default_location: "" # Default location for !weather (optional)
|
|
|
|
|
|
|
|
|
|
# === MESHMONITOR INTEGRATION ===
|
|
|
|
|
meshmonitor:
|
|
|
|
|
enabled: false # Enable MeshMonitor trigger sync
|
|
|
|
|
url: "" # MeshMonitor web UI URL (e.g. http://192.168.1.100:3333)
|
|
|
|
|
inject_into_prompt: true # Include trigger list in LLM prompt
|
|
|
|
|
refresh_interval: 300 # Seconds between trigger refreshes
|
|
|
|
|
|
|
|
|
|
# === KNOWLEDGE BASE (RAG) ===
|
|
|
|
|
knowledge:
|
|
|
|
|
enabled: false # Enable knowledge base search
|
|
|
|
|
db_path: "" # Path to knowledge SQLite database
|
|
|
|
|
top_k: 5 # Number of chunks to retrieve per query
|
|
|
|
|
|
|
|
|
|
# === MESH DATA SOURCES ===
|
|
|
|
|
# Connect to Meshview and/or MeshMonitor instances for live mesh
|
2026-07-05 16:21:32 -06:00
|
|
|
# network analysis. Supports multiple sources. Configure via the
|
|
|
|
|
# dashboard (Mesh Sources) or by editing this file directly.
|
2026-06-16 03:40:31 +00:00
|
|
|
#
|
|
|
|
|
# mesh_sources:
|
|
|
|
|
# - name: "my-meshview"
|
|
|
|
|
# type: meshview
|
|
|
|
|
# url: "https://meshview.example.com"
|
|
|
|
|
# refresh_interval: 300
|
|
|
|
|
# enabled: true
|
|
|
|
|
#
|
|
|
|
|
# - name: "my-meshmonitor"
|
|
|
|
|
# type: meshmonitor
|
|
|
|
|
# url: "http://192.168.1.100:3333"
|
|
|
|
|
# api_token: "${MM_API_TOKEN}"
|
|
|
|
|
# refresh_interval: 300
|
|
|
|
|
# enabled: true
|
|
|
|
|
#
|
|
|
|
|
# - name: "mqtt-broker"
|
|
|
|
|
# type: mqtt
|
|
|
|
|
# host: "mqtt.meshtastic.org"
|
|
|
|
|
# port: 1883
|
|
|
|
|
# username: "meshdev"
|
|
|
|
|
# password: "large4cats"
|
|
|
|
|
# topic_root: "msh/US"
|
|
|
|
|
# use_tls: false
|
|
|
|
|
# enabled: true
|
|
|
|
|
mesh_sources: []
|
|
|
|
|
|
|
|
|
|
# === MESH INTELLIGENCE ===
|
|
|
|
|
# Geographic clustering and health scoring for mesh analysis.
|
|
|
|
|
# Requires mesh_sources to be configured with at least one data source.
|
|
|
|
|
#
|
|
|
|
|
# mesh_intelligence:
|
|
|
|
|
# enabled: true
|
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
|
|
|
# regions: # Fixed region anchors (explicit, not auto-clustered)
|
|
|
|
|
# - name: "magic_valley"
|
|
|
|
|
# lat: 42.56
|
|
|
|
|
# lon: -114.47
|
|
|
|
|
# local_name: "Magic Valley"
|
|
|
|
|
# description: "Twin Falls, Burley, Jerome along I-84/US-93"
|
|
|
|
|
# aliases: ["southern Idaho"]
|
|
|
|
|
# cities: ["Twin Falls", "Burley", "Jerome"]
|
|
|
|
|
# locality_radius_miles: 8.0 # Radius for locality clustering within regions
|
|
|
|
|
# offline_threshold_hours: 2 # Hours before node considered offline
|
|
|
|
|
# packet_threshold: 500 # Non-text packets per 24h to flag
|
|
|
|
|
# battery_warning_percent: 30 # Battery level for warnings
|
|
|
|
|
# critical_nodes: [] # Short names of critical nodes (e.g., ["MHR", "HPR"])
|
|
|
|
|
# alert_channel: -1 # Channel to broadcast alerts on. -1 = disabled, 0+ = channel index
|
|
|
|
|
# alert_rules: {} # Per-condition alert toggles/thresholds (see AlertRulesConfig)
|
2026-06-16 03:40:31 +00:00
|
|
|
mesh_intelligence:
|
|
|
|
|
enabled: false
|
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
|
|
|
regions: []
|
2026-06-16 03:40:31 +00:00
|
|
|
locality_radius_miles: 8.0
|
|
|
|
|
offline_threshold_hours: 2
|
|
|
|
|
packet_threshold: 500
|
|
|
|
|
battery_warning_percent: 30
|
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
|
|
|
critical_nodes: []
|
|
|
|
|
alert_channel: -1
|
|
|
|
|
alert_rules: {}
|
2026-06-16 03:40:31 +00:00
|
|
|
|
|
|
|
|
# === ENVIRONMENTAL FEEDS ===
|
|
|
|
|
# Live situational awareness from NWS, NOAA Space Weather, and Open-Meteo.
|
|
|
|
|
# Provides weather alerts, HF propagation assessment, and tropospheric ducting.
|
|
|
|
|
#
|
|
|
|
|
environmental:
|
|
|
|
|
enabled: false
|
|
|
|
|
nws_zones:
|
|
|
|
|
- "IDZ016" # Western Magic Valley
|
|
|
|
|
- "IDZ030" # Southern Twin Falls County
|
|
|
|
|
|
|
|
|
|
# NWS Weather Alerts (api.weather.gov)
|
|
|
|
|
nws:
|
|
|
|
|
enabled: true
|
|
|
|
|
tick_seconds: 60
|
|
|
|
|
areas: ["ID"]
|
|
|
|
|
severity_min: "moderate"
|
|
|
|
|
user_agent: "(meshai.example.com, ops@example.com)" # REQUIRED by NWS
|
|
|
|
|
|
|
|
|
|
# NOAA Space Weather (services.swpc.noaa.gov)
|
|
|
|
|
swpc:
|
|
|
|
|
enabled: true
|
|
|
|
|
|
|
|
|
|
# Tropospheric ducting assessment (Open-Meteo GFS, no auth)
|
|
|
|
|
ducting:
|
|
|
|
|
enabled: true
|
|
|
|
|
tick_seconds: 10800 # 3 hours
|
|
|
|
|
latitude: 42.56 # center of mesh coverage area
|
|
|
|
|
longitude: -114.47
|
|
|
|
|
|
|
|
|
|
# NIFC Fire Perimeters (Phase 2)
|
|
|
|
|
fires:
|
|
|
|
|
enabled: false
|
|
|
|
|
tick_seconds: 600
|
|
|
|
|
state: "US-ID"
|
|
|
|
|
|
|
|
|
|
# Avalanche Advisories (Phase 2)
|
|
|
|
|
avalanche:
|
|
|
|
|
enabled: false
|
|
|
|
|
tick_seconds: 1800
|
|
|
|
|
center_ids: ["SNFAC"]
|
|
|
|
|
season_months: [12, 1, 2, 3, 4]
|
|
|
|
|
|
|
|
|
|
# USGS Stream Gauges (waterservices.usgs.gov)
|
|
|
|
|
# Find site IDs at https://waterdata.usgs.gov/nwis
|
|
|
|
|
usgs:
|
|
|
|
|
enabled: false
|
|
|
|
|
tick_seconds: 900 # Min 15 min per USGS guidelines
|
|
|
|
|
sites: [] # e.g. ["13090500", "13088000"]
|
|
|
|
|
|
|
|
|
|
# TomTom Traffic Flow (api.tomtom.com, requires API key)
|
|
|
|
|
traffic:
|
|
|
|
|
enabled: false
|
|
|
|
|
tick_seconds: 300
|
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
|
|
|
api_key: ${TOMTOM_API_KEY} # Secret: set via dashboard or /data/secrets/.env (get key at developer.tomtom.com)
|
2026-06-16 03:40:31 +00:00
|
|
|
corridors: []
|
|
|
|
|
# Example corridors:
|
|
|
|
|
# - name: "I-84 Twin Falls"
|
|
|
|
|
# lat: 42.56
|
|
|
|
|
# lon: -114.47
|
|
|
|
|
|
|
|
|
|
# 511 Road Conditions (state-specific, configurable base URL)
|
|
|
|
|
roads511:
|
|
|
|
|
enabled: false
|
|
|
|
|
tick_seconds: 300
|
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
|
|
|
api_key: ${ROADS511_API_KEY} # Secret: set via dashboard or /data/secrets/.env (leave var empty if 511 needs no key)
|
2026-06-16 03:40:31 +00:00
|
|
|
base_url: "" # e.g. "https://511.idaho.gov/api/v2"
|
|
|
|
|
endpoints: ["/get/event"]
|
|
|
|
|
bbox: [] # [west, south, east, north]
|
|
|
|
|
|
|
|
|
|
# NASA FIRMS Satellite Fire Detection
|
|
|
|
|
# Early warning via satellite hotspots, hours before official perimeters
|
|
|
|
|
# Get MAP_KEY at: https://firms.modaps.eosdis.nasa.gov/api/area/
|
|
|
|
|
firms:
|
|
|
|
|
enabled: false
|
|
|
|
|
tick_seconds: 1800 # 30 min default
|
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
|
|
|
map_key: ${FIRMS_MAP_KEY} # Secret: set via dashboard or /data/secrets/.env (NASA FIRMS MAP_KEY)
|
2026-06-16 03:40:31 +00:00
|
|
|
source: "VIIRS_SNPP_NRT" # VIIRS_SNPP_NRT, VIIRS_NOAA20_NRT, MODIS_NRT
|
|
|
|
|
bbox: [] # [west, south, east, north] - Required
|
|
|
|
|
day_range: 1 # 1-10 days of data
|
|
|
|
|
confidence_min: "nominal" # low, nominal, high
|
|
|
|
|
proximity_km: 10.0 # km to match known fire perimeters
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# === NOTIFICATION DELIVERY (TRANSITIONAL) ===
|
|
|
|
|
# NOTE: This notifications schema will be replaced in v0.3 by the 8-toggle model.
|
|
|
|
|
# These rule examples are transitional until Phase 1.2 lands. Do not extend.
|
|
|
|
|
# Severity levels: routine (informational), priority (needs attention), immediate (act now)
|
|
|
|
|
#
|
|
|
|
|
# Route alerts to channels (mesh, email, webhook) based on rules.
|
|
|
|
|
# Categories match alert types from alert_engine.py.
|
|
|
|
|
notifications:
|
|
|
|
|
enabled: false
|
|
|
|
|
|
|
|
|
|
# Digest scheduler settings
|
|
|
|
|
# The digest collects priority/routine events and delivers a summary
|
|
|
|
|
# at the configured time to rules with trigger_type='schedule' and
|
|
|
|
|
# schedule_match='digest'.
|
|
|
|
|
digest:
|
|
|
|
|
schedule: "07:00" # HH:MM local time to fire digest
|
|
|
|
|
include: [] # Toggle names to include (empty = default set)
|
|
|
|
|
# Default set: weather, fire, seismic, avalanche, roads, mesh_health, tracking, other
|
|
|
|
|
# Excludes rf_propagation by default
|
|
|
|
|
# Example: include: ["weather", "fire", "mesh_health"]
|
|
|
|
|
|
|
|
|
|
# Notification rules - each rule is self-contained with its own delivery config
|
|
|
|
|
# Default baseline rules are created on fresh install
|
|
|
|
|
rules:
|
|
|
|
|
# Emergency Broadcast - all emergencies go out immediately
|
|
|
|
|
- name: "Emergency Broadcast"
|
|
|
|
|
enabled: true
|
|
|
|
|
trigger_type: condition
|
|
|
|
|
categories: [] # Empty = all categories
|
|
|
|
|
min_severity: "immediate"
|
|
|
|
|
delivery_type: mesh_broadcast
|
|
|
|
|
broadcast_channel: 0
|
|
|
|
|
cooldown_minutes: 5
|
|
|
|
|
|
|
|
|
|
# Infrastructure Down - critical node and infrastructure offline alerts
|
|
|
|
|
- name: "Infrastructure Down"
|
|
|
|
|
enabled: true
|
|
|
|
|
trigger_type: condition
|
|
|
|
|
categories: ["infra_offline", "critical_node_down"]
|
|
|
|
|
min_severity: "priority"
|
|
|
|
|
delivery_type: mesh_broadcast
|
|
|
|
|
broadcast_channel: 0
|
|
|
|
|
cooldown_minutes: 30
|
|
|
|
|
|
|
|
|
|
# Fire Alert - wildfire proximity and new ignition
|
|
|
|
|
- name: "Fire Alert"
|
|
|
|
|
enabled: true
|
|
|
|
|
trigger_type: condition
|
|
|
|
|
categories: ["wildfire_proximity", "new_ignition"]
|
|
|
|
|
min_severity: "routine"
|
|
|
|
|
delivery_type: mesh_broadcast
|
|
|
|
|
broadcast_channel: 0
|
|
|
|
|
cooldown_minutes: 60
|
|
|
|
|
|
|
|
|
|
# Severe Weather - weather warnings
|
|
|
|
|
- name: "Severe Weather"
|
|
|
|
|
enabled: true
|
|
|
|
|
trigger_type: condition
|
|
|
|
|
categories: ["weather_warning"]
|
|
|
|
|
min_severity: "priority"
|
|
|
|
|
delivery_type: mesh_broadcast
|
|
|
|
|
broadcast_channel: 0
|
|
|
|
|
cooldown_minutes: 30
|
|
|
|
|
|
|
|
|
|
# Example: Morning Digest -> mesh broadcast
|
|
|
|
|
# Delivers the accumulated digest at the configured schedule time
|
|
|
|
|
# - name: "Morning Digest Mesh"
|
|
|
|
|
# enabled: false
|
|
|
|
|
# trigger_type: schedule
|
|
|
|
|
# schedule_match: "digest" # Required for digest delivery
|
|
|
|
|
# delivery_type: mesh_broadcast
|
|
|
|
|
# broadcast_channel: 0
|
|
|
|
|
|
|
|
|
|
# Example: Morning Digest -> email
|
|
|
|
|
# - name: "Morning Digest Email"
|
|
|
|
|
# enabled: false
|
|
|
|
|
# trigger_type: schedule
|
|
|
|
|
# schedule_match: "digest"
|
|
|
|
|
# delivery_type: email
|
|
|
|
|
# smtp_host: "smtp.gmail.com"
|
|
|
|
|
# smtp_port: 587
|
|
|
|
|
# smtp_user: "you@gmail.com"
|
|
|
|
|
# smtp_password: "${SMTP_PASSWORD}"
|
|
|
|
|
# smtp_tls: true
|
|
|
|
|
# from_address: "meshai@yourdomain.com"
|
|
|
|
|
# recipients: ["admin@yourdomain.com"]
|
|
|
|
|
|
|
|
|
|
# Example: Fire alerts -> email
|
|
|
|
|
# - name: "Fire Alerts Email"
|
|
|
|
|
# enabled: true
|
|
|
|
|
# trigger_type: condition
|
|
|
|
|
# categories: ["wildfire_proximity", "new_ignition"]
|
|
|
|
|
# min_severity: "routine"
|
|
|
|
|
# delivery_type: email
|
|
|
|
|
# smtp_host: "smtp.gmail.com"
|
|
|
|
|
# smtp_port: 587
|
|
|
|
|
# smtp_user: "you@gmail.com"
|
|
|
|
|
# smtp_password: "${SMTP_PASSWORD}"
|
|
|
|
|
# smtp_tls: true
|
|
|
|
|
# from_address: "meshai@yourdomain.com"
|
|
|
|
|
# recipients: ["admin@yourdomain.com"]
|
|
|
|
|
# cooldown_minutes: 30
|
|
|
|
|
|
|
|
|
|
# Example: All warnings -> Discord webhook
|
|
|
|
|
# - name: "Discord Alerts"
|
|
|
|
|
# enabled: true
|
|
|
|
|
# trigger_type: condition
|
|
|
|
|
# categories: []
|
|
|
|
|
# min_severity: "priority"
|
|
|
|
|
# delivery_type: webhook
|
|
|
|
|
# webhook_url: "https://discord.com/api/webhooks/..."
|
|
|
|
|
# cooldown_minutes: 10
|
|
|
|
|
|
|
|
|
|
# Example: Rule with no delivery (matches and logs, but doesn't send)
|
|
|
|
|
# - name: "Monitor Only"
|
|
|
|
|
# enabled: true
|
|
|
|
|
# trigger_type: condition
|
|
|
|
|
# categories: ["battery_warning"]
|
|
|
|
|
# min_severity: "priority"
|
|
|
|
|
# delivery_type: "" # Empty = no delivery, just tracks matches
|
|
|
|
|
|
|
|
|
|
# === WEB DASHBOARD ===
|
|
|
|
|
dashboard:
|
|
|
|
|
enabled: true
|
|
|
|
|
port: 8080
|
|
|
|
|
host: "0.0.0.0"
|