chore(config): delete 51 dead keys, fix usgs_quake floor, secret-flag consistency

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: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matt Johnson 2026-07-05 23:34:23 +00:00
commit aacf053b63
8 changed files with 45 additions and 81 deletions

View file

@ -30,9 +30,6 @@ history:
database: /data/conversations.db
max_messages_per_user: 50 # Messages to keep per user
conversation_timeout: 86400 # Conversation expiry (seconds, 86400=24h)
auto_cleanup: true # Auto-delete old conversations
cleanup_interval_hours: 24 # How often to run cleanup
max_age_days: 30 # Delete conversations older than this
# === MEMORY OPTIMIZATION ===
memory:

View file

@ -5,7 +5,7 @@ Handlers use the singleton `adapter_config` from this package:
from meshai.adapter_config import adapter_config
cooldown_s = adapter_config.wfigs.cooldown_seconds # int
severities = adapter_config.nws.broadcast_severities # list[str]
tombstones = adapter_config.nws.tombstone_msgtypes # list[str]
Reads are dict-cached. The cache invalidates on `invalidate_cache()`
(called from the REST API's PUT handler in v0.6-3c). The cache is

View file

@ -73,23 +73,16 @@ REGISTRY: dict[tuple[str, str], dict[str, Any]] = {
},
# =================================================================
# NWS -- 3 settings (severity gate, tombstone msgTypes, suffix-promote toggle)
# NWS -- tombstone msgTypes (severity gate + suffix-promote were removed
# in the v0.x config-schema cleanup: both were [DEPRECATED — no longer
# enforced]; NWS breadth is governed by the per-toggle dispatcher
# severity threshold)
# =================================================================
("nws", "broadcast_severities"): {
"default": ["Extreme", "Severe"], # nws_handler.py:43
"type": "json",
"description": "CAP severity strings allowed onto the mesh. [DEPRECATED — no longer enforced; NWS breadth is governed by the per-toggle dispatcher severity threshold]",
},
("nws", "tombstone_msgtypes"): {
"default": ["Cancel", "Expire"], # nws_handler.py:46
"type": "json",
"description": "CAP msgType values that mark an alert as gone.",
},
("nws", "warning_suffix_promotes"): {
"default": True, # nws_handler.py:172
"type": "bool",
"description": "Promote category-name-ending-in-_warning to Severe when CAP severity is missing. [DEPRECATED — no longer enforced; NWS breadth is governed by the per-toggle dispatcher severity threshold]",
},
# =================================================================
# USGS_QUAKE -- 6 settings (regional geography + 3 mag floors + PAGER set)
@ -112,12 +105,12 @@ REGISTRY: dict[tuple[str, str], dict[str, Any]] = {
("usgs_quake", "global_mag_floor"): {
"default": 3.0, # quake_handler.py:69
"type": "float",
"description": "Global magnitude floor for unconditional broadcasts.",
"description": "Global magnitude floor for unconditional broadcasts. CENTRAL-PATH ONLY (central/quake_handler.py); the native feed_source gates on environmental.usgs_quake.min_magnitude instead.",
},
("usgs_quake", "regional_mag_floor"): {
"default": 2.5, # quake_handler.py:70
"type": "float",
"description": "Reduced magnitude floor for quakes within regional_radius_mi of centroid.",
"description": "Reduced magnitude floor for quakes within regional_radius_mi of centroid. CENTRAL-PATH ONLY (central/quake_handler.py); the native feed_source gates on environmental.usgs_quake.min_magnitude instead.",
},
("usgs_quake", "escalate_mag_floor"): {
"default": 5.0, # quake_handler.py:76

View file

@ -44,8 +44,10 @@ from meshai.persistence import get_db
logger = logging.getLogger(__name__)
# v0.6-3b: severity gate + tombstone msgTypes live in adapter_config.nws
# (broadcast_severities, tombstone_msgtypes). Read at handler call time.
# v0.6-3b: tombstone msgTypes live in adapter_config.nws
# (tombstone_msgtypes), read at handler call time. The old broadcast_severities
# severity gate was removed in the config-schema cleanup (no longer enforced;
# NWS breadth is governed by the per-toggle dispatcher severity threshold).
# Ordered (substring, emoji) checks; first match wins.
_EVENT_EMOJI = [

View file

@ -63,11 +63,6 @@ class HistoryConfig:
max_messages_per_user: int = 50
conversation_timeout: int = 86400 # 24 hours
# Cleanup settings
auto_cleanup: bool = True
cleanup_interval_hours: int = 24
max_age_days: int = 30 # Delete conversations older than this
@dataclass
class MemoryConfig:
@ -75,13 +70,6 @@ class MemoryConfig:
enabled: bool = True # Enable memory optimization
# MQTT-specific fields (type=mqtt only)
host: str = "" # MQTT broker hostname
port: int = 1883 # MQTT broker port (1883 plain, 8883 TLS)
username: str = "" # MQTT username (optional)
password: str = "" # MQTT password (optional, supports )
topic_root: str = "msh/US" # Topic root to subscribe to
use_tls: bool = False # Enable TLS for MQTT connection
window_size: int = 4 # Recent message pairs to keep in full
summarize_threshold: int = 8 # Messages before re-summarizing
@ -92,13 +80,6 @@ class ContextConfig:
enabled: bool = True
# MQTT-specific fields (type=mqtt only)
host: str = "" # MQTT broker hostname
port: int = 1883 # MQTT broker port (1883 plain, 8883 TLS)
username: str = "" # MQTT username (optional)
password: str = "" # MQTT password (optional, supports )
topic_root: str = "msh/US" # Topic root to subscribe to
use_tls: bool = False # Enable TLS for MQTT connection
observe_channels: list[int] = field(default_factory=list) # Empty = all channels
ignore_nodes: list[str] = field(default_factory=list) # Node IDs to ignore
max_age: int = 1_209_600 # 14 days in seconds
@ -120,13 +101,6 @@ class CommandsConfig:
enabled: bool = True
# MQTT-specific fields (type=mqtt only)
host: str = "" # MQTT broker hostname
port: int = 1883 # MQTT broker port (1883 plain, 8883 TLS)
username: str = "" # MQTT username (optional)
password: str = "" # MQTT password (optional, supports )
topic_root: str = "msh/US" # Topic root to subscribe to
use_tls: bool = False # Enable TLS for MQTT connection
prefix: str = "!"
disabled_commands: list[str] = field(default_factory=list)
custom_commands: dict = field(default_factory=dict)
@ -253,7 +227,6 @@ class RegionAnchor:
description: str = "" # e.g., "Twin Falls, Burley, Jerome along I-84/US-93"
aliases: list[str] = field(default_factory=list) # e.g., ["southern Idaho", "magic valley"]
cities: list[str] = field(default_factory=list) # e.g., ["Twin Falls", "Burley", "Jerome"]
nws_zones: list[str] = field(default_factory=list) # NWS zone codes (e.g., ["IDZ016", "IDZ030"])
@dataclass
@ -273,10 +246,6 @@ class AlertRulesConfig:
battery_warning_threshold: int = 30
battery_critical_threshold: int = 15
battery_emergency_threshold: int = 5
# Voltage-based thresholds (more accurate than percentage)
battery_warning_voltage: float = 3.60
battery_critical_voltage: float = 3.50
battery_emergency_voltage: float = 3.40
power_source_change: bool = True
solar_not_charging: bool = True
@ -314,7 +283,6 @@ class MeshIntelligenceConfig:
# Alert settings
critical_nodes: list[str] = field(default_factory=list) # Short names of critical nodes (e.g., ["MHR", "HPR"])
alert_channel: int = -1 # Channel to broadcast alerts on. -1 = disabled, 0+ = channel index
alert_cooldown_minutes: int = 30 # Min minutes between repeated alerts for same condition
alert_rules: AlertRulesConfig = field(default_factory=AlertRulesConfig)
@ -337,13 +305,6 @@ class NWSConfig(_SourcedFeed):
enabled: bool = True
# MQTT-specific fields (type=mqtt only)
host: str = "" # MQTT broker hostname
port: int = 1883 # MQTT broker port (1883 plain, 8883 TLS)
username: str = "" # MQTT username (optional)
password: str = "" # MQTT password (optional, supports )
topic_root: str = "msh/US" # Topic root to subscribe to
use_tls: bool = False # Enable TLS for MQTT connection
tick_seconds: int = 60
areas: list = field(default_factory=lambda: ["ID"])
severity_min: str = "moderate"
@ -356,14 +317,6 @@ class SWPCConfig(_SourcedFeed):
enabled: bool = True
# MQTT-specific fields (type=mqtt only)
host: str = "" # MQTT broker hostname
port: int = 1883 # MQTT broker port (1883 plain, 8883 TLS)
username: str = "" # MQTT username (optional)
password: str = "" # MQTT password (optional, supports )
topic_root: str = "msh/US" # Topic root to subscribe to
use_tls: bool = False # Enable TLS for MQTT connection
@dataclass
class DuctingConfig(_SourcedFeed):
@ -371,13 +324,6 @@ class DuctingConfig(_SourcedFeed):
enabled: bool = True
# MQTT-specific fields (type=mqtt only)
host: str = "" # MQTT broker hostname
port: int = 1883 # MQTT broker port (1883 plain, 8883 TLS)
username: str = "" # MQTT username (optional)
password: str = "" # MQTT password (optional, supports )
topic_root: str = "msh/US" # Topic root to subscribe to
use_tls: bool = False # Enable TLS for MQTT connection
tick_seconds: int = 10800 # 3 hours
latitude: float = 42.56 # Twin Falls area default
longitude: float = -114.47
@ -419,6 +365,11 @@ class USGSQuakeConfig(_SourcedFeed):
enabled: bool = False
tick_seconds: int = 300
feed_url: str = "https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/2.5_day.geojson"
# Native-path broadcast magnitude floor: the native adapter
# (env/usgs_quake.py) gates on this. THIS is the GUI-editable quake
# magnitude floor for the native feed_source. (The adapter_config
# REGISTRY keys usgs_quake.global_mag_floor / regional_mag_floor apply
# only to the Central-firehose path in central/quake_handler.py.)
min_magnitude: float = 2.5
# [west, south, east, north] -- Magic Valley -> Borah Peak -> Yellowstone
bbox: list = field(default_factory=lambda: [-115.5, 42.0, -110.0, 45.2])
@ -758,7 +709,18 @@ class DangerZoneHazardConfig:
enabled: bool = True
buffer_mi: float = 5.0
min_acres: float = 0.0 # fire-only; ignored by other families
@dataclass
class DangerZoneFireConfig(DangerZoneHazardConfig):
"""Fire-family danger-zone tuning; adds an acreage floor.
``min_acres`` is fire-only (read at danger_zone_correlator.py in the
``fam == "fire"`` branch); the non-fire families use the plain
DangerZoneHazardConfig, which no longer carries it.
"""
min_acres: float = 0.0 # skip fires smaller than this (0 = no floor)
@dataclass
@ -778,7 +740,7 @@ class DangerZonesConfig:
# Per-family sub-configs. snow->weather, flood->seismic resolved in the
# correlator; both still exposed here for distinct GUI tuning.
fire: DangerZoneHazardConfig = field(default_factory=DangerZoneHazardConfig)
fire: DangerZoneFireConfig = field(default_factory=DangerZoneFireConfig)
weather: DangerZoneHazardConfig = field(default_factory=DangerZoneHazardConfig)
snow: DangerZoneHazardConfig = field(default_factory=DangerZoneHazardConfig)
flood: DangerZoneHazardConfig = field(default_factory=DangerZoneHazardConfig)

View file

@ -79,6 +79,8 @@ SECRET_FIELDS: set[str] = {
"mesh_sources.*.password",
"environmental.traffic.api_key",
"environmental.firms.map_key",
"environmental.roads511.api_key",
"environmental.wzdx.api_key",
"notifications.rules.*.smtp_password",
"notifications.toggles.*.smtp_password",
"danger_zones.webhook_url",

View file

@ -36,7 +36,9 @@ def test_list_returns_all_59_keys(client):
# 14 adapters with at least one key (itd_511 has zero -- not in the
# grouped dict because the SQL only returns rows that exist).
total = sum(len(v) for v in body.values())
assert total == 96
# was 96; config-schema cleanup removed the two deprecated nws keys
# (broadcast_severities, warning_suffix_promotes) -> 94.
assert total == 94
def test_list_grouped_by_adapter(client):
@ -184,12 +186,14 @@ def test_put_str_validation(client):
def test_put_json_accepts_list(client):
# broadcast_severities was removed in the config-schema cleanup; use the
# surviving nws json-list key tombstone_msgtypes to exercise list PUTs.
r = client.put(
"/api/adapter-config/nws/broadcast_severities",
json={"value": ["Extreme"]},
"/api/adapter-config/nws/tombstone_msgtypes",
json={"value": ["Cancel"]},
)
assert r.status_code == 200
assert r.json()["value"] == ["Extreme"]
assert r.json()["value"] == ["Cancel"]
def test_put_json_accepts_dict(client):

View file

@ -75,7 +75,9 @@ def test_adapter_config_type_check_constrains_vocabulary(fresh_db):
def test_registry_at_59_entries():
"""v0.6-3a.1 trim: 43 CONFIG-only keys (was 77 in v0.6-3a draft)."""
assert len(REGISTRY) == 96, (
# was 96; config-schema cleanup removed the two deprecated nws keys
# (broadcast_severities, warning_suffix_promotes) -> 94.
assert len(REGISTRY) == 94, (
f"REGISTRY drift guard; got {len(REGISTRY)}. "
f"If a sentence template / emoji / heuristic snuck in, it belongs in CODE not config."
)
@ -225,7 +227,9 @@ def test_accessor_returns_bool(fresh_db):
def test_accessor_returns_json_list(fresh_db):
invalidate_cache()
assert adapter_config.nws.broadcast_severities == ["Extreme", "Severe"]
# broadcast_severities was removed in the config-schema cleanup (deprecated,
# no longer enforced); tombstone_msgtypes is the surviving nws json-list key.
assert adapter_config.nws.tombstone_msgtypes == ["Cancel", "Expire"]
def test_accessor_returns_json_dict(fresh_db):