fix(notifications): complete severity cleanup to 3-level system

- Replace 11 info fallbacks with routine in router.py + channels.py
- Replace 2 warning min_severity defaults with priority
- Update config.example.yaml rules to use routine/priority/immediate
- Annotate config.example.yaml notifications section as transitional pending v0.3 8-toggle rewrite Phase 1.2

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
K7ZVX 2026-05-14 07:00:58 +00:00
commit 344ca0677d
4 changed files with 24 additions and 22 deletions

View file

@ -217,11 +217,13 @@ environmental:
proximity_km: 10.0 # km to match known fire perimeters proximity_km: 10.0 # km to match known fire perimeters
# === NOTIFICATION DELIVERY === # === 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. # Route alerts to channels (mesh, email, webhook) based on rules.
# Categories match alert types from alert_engine.py. # Categories match alert types from alert_engine.py.
# Severity levels: info, advisory, watch, warning, critical, emergency
#
notifications: notifications:
enabled: false enabled: false
quiet_hours_enabled: true # Master toggle for quiet hours feature quiet_hours_enabled: true # Master toggle for quiet hours feature
@ -236,7 +238,7 @@ notifications:
enabled: true enabled: true
trigger_type: condition trigger_type: condition
categories: [] # Empty = all categories categories: [] # Empty = all categories
min_severity: "emergency" min_severity: "immediate"
delivery_type: mesh_broadcast delivery_type: mesh_broadcast
broadcast_channel: 0 broadcast_channel: 0
cooldown_minutes: 5 cooldown_minutes: 5
@ -247,7 +249,7 @@ notifications:
enabled: true enabled: true
trigger_type: condition trigger_type: condition
categories: ["infra_offline", "critical_node_down"] categories: ["infra_offline", "critical_node_down"]
min_severity: "warning" min_severity: "priority"
delivery_type: mesh_broadcast delivery_type: mesh_broadcast
broadcast_channel: 0 broadcast_channel: 0
cooldown_minutes: 30 cooldown_minutes: 30
@ -258,7 +260,7 @@ notifications:
enabled: true enabled: true
trigger_type: condition trigger_type: condition
categories: ["wildfire_proximity", "new_ignition"] categories: ["wildfire_proximity", "new_ignition"]
min_severity: "advisory" min_severity: "routine"
delivery_type: mesh_broadcast delivery_type: mesh_broadcast
broadcast_channel: 0 broadcast_channel: 0
cooldown_minutes: 60 cooldown_minutes: 60
@ -269,7 +271,7 @@ notifications:
enabled: true enabled: true
trigger_type: condition trigger_type: condition
categories: ["weather_warning"] categories: ["weather_warning"]
min_severity: "warning" min_severity: "priority"
delivery_type: mesh_broadcast delivery_type: mesh_broadcast
broadcast_channel: 0 broadcast_channel: 0
cooldown_minutes: 30 cooldown_minutes: 30
@ -280,7 +282,7 @@ notifications:
# enabled: true # enabled: true
# trigger_type: condition # trigger_type: condition
# categories: ["wildfire_proximity", "new_ignition"] # categories: ["wildfire_proximity", "new_ignition"]
# min_severity: "advisory" # min_severity: "routine"
# delivery_type: email # delivery_type: email
# smtp_host: "smtp.gmail.com" # smtp_host: "smtp.gmail.com"
# smtp_port: 587 # smtp_port: 587
@ -296,7 +298,7 @@ notifications:
# enabled: true # enabled: true
# trigger_type: condition # trigger_type: condition
# categories: [] # categories: []
# min_severity: "warning" # min_severity: "priority"
# delivery_type: webhook # delivery_type: webhook
# webhook_url: "https://discord.com/api/webhooks/..." # webhook_url: "https://discord.com/api/webhooks/..."
# cooldown_minutes: 10 # cooldown_minutes: 10
@ -316,7 +318,7 @@ notifications:
# enabled: true # enabled: true
# trigger_type: condition # trigger_type: condition
# categories: ["battery_warning"] # categories: ["battery_warning"]
# min_severity: "warning" # min_severity: "priority"
# delivery_type: "" # Empty = no delivery, just tracks matches # delivery_type: "" # Empty = no delivery, just tracks matches
# === WEB DASHBOARD === # === WEB DASHBOARD ===

View file

@ -578,7 +578,7 @@ def _migrate_legacy_channels(notifications, data: dict):
enabled=ch.get("enabled", True), enabled=ch.get("enabled", True),
trigger_type="condition", trigger_type="condition",
categories=old_rule.get("categories", []), categories=old_rule.get("categories", []),
min_severity=old_rule.get("min_severity", "warning"), min_severity=old_rule.get("min_severity", "priority"),
delivery_type=ch.get("type", "mesh_broadcast"), delivery_type=ch.get("type", "mesh_broadcast"),
broadcast_channel=ch.get("channel_index", 0), broadcast_channel=ch.get("channel_index", 0),
node_ids=ch.get("node_ids", []), node_ids=ch.get("node_ids", []),

View file

@ -292,7 +292,7 @@ class EmailChannel(NotificationChannel):
return False return False
alert_type = alert.get("type", "alert") alert_type = alert.get("type", "alert")
severity = alert.get("severity", "info").upper() severity = alert.get("severity", "routine").upper()
message = alert.get("message", "") message = alert.get("message", "")
subject = "[MeshAI %s] %s" % (severity, alert_type.replace("_", " ").title()) subject = "[MeshAI %s] %s" % (severity, alert_type.replace("_", " ").title())
body = "MeshAI Alert\n\nType: %s\nSeverity: %s\nTime: %s\n\n%s\n\n---\nAutomated message from MeshAI." % ( body = "MeshAI Alert\n\nType: %s\nSeverity: %s\nTime: %s\n\n%s\n\n---\nAutomated message from MeshAI." % (
@ -518,7 +518,7 @@ class WebhookChannel(NotificationChannel):
"""POST alert to webhook URL.""" """POST alert to webhook URL."""
payload = { payload = {
"type": alert.get("type"), "type": alert.get("type"),
"severity": alert.get("severity", "info"), "severity": alert.get("severity", "routine"),
"message": alert.get("message", ""), "message": alert.get("message", ""),
"timestamp": time.time(), "timestamp": time.time(),
"node_name": alert.get("node_name"), "node_name": alert.get("node_name"),
@ -527,7 +527,7 @@ class WebhookChannel(NotificationChannel):
# Discord/Slack format # Discord/Slack format
if "discord.com" in self._url or "slack.com" in self._url: if "discord.com" in self._url or "slack.com" in self._url:
severity = alert.get("severity", "info") severity = alert.get("severity", "routine")
color = { color = {
"immediate": 0xFF0000, "immediate": 0xFF0000,
"priority": 0xFFAA00, "priority": 0xFFAA00,
@ -669,7 +669,7 @@ class WebhookChannel(NotificationChannel):
else: else:
payload = { payload = {
"type": "test", "type": "test",
"severity": "info", "severity": "routine",
"message": "MeshAI channel connectivity test", "message": "MeshAI channel connectivity test",
"timestamp": time.time(), "timestamp": time.time(),
} }
@ -730,7 +730,7 @@ class WebhookChannel(NotificationChannel):
async def deliver_test(self, message: str) -> tuple[bool, str]: async def deliver_test(self, message: str) -> tuple[bool, str]:
"""Deliver a specific test message via webhook.""" """Deliver a specific test message via webhook."""
try: try:
test_alert = {"type": "test", "severity": "info", "message": message} test_alert = {"type": "test", "severity": "routine", "message": message}
success = await self.deliver(test_alert, {}) success = await self.deliver(test_alert, {})
if success: if success:
try: try:

View file

@ -150,7 +150,7 @@ class NotificationRouter:
async def process_alert(self, alert: dict) -> bool: async def process_alert(self, alert: dict) -> bool:
"""Route an alert through matching rules.""" """Route an alert through matching rules."""
category = alert.get("type", "") category = alert.get("type", "")
severity = alert.get("severity", "info") severity = alert.get("severity", "routine")
delivered = False delivered = False
for rule in self._rules: for rule in self._rules:
@ -160,7 +160,7 @@ class NotificationRouter:
if rule_categories and category not in rule_categories: if rule_categories and category not in rule_categories:
continue continue
min_severity = rule.get("min_severity", "info") min_severity = rule.get("min_severity", "routine")
if not self._severity_meets(severity, min_severity): if not self._severity_meets(severity, min_severity):
continue continue
@ -392,7 +392,7 @@ class NotificationRouter:
rule_name = rule_dict.get("name", f"Rule {rule_index}") rule_name = rule_dict.get("name", f"Rule {rule_index}")
rule_categories = rule_dict.get("categories", []) rule_categories = rule_dict.get("categories", [])
min_severity = rule_dict.get("min_severity", "info") min_severity = rule_dict.get("min_severity", "routine")
delivery_type = rule_dict.get("delivery_type", "") delivery_type = rule_dict.get("delivery_type", "")
# Legacy support # Legacy support
@ -536,7 +536,7 @@ class NotificationRouter:
for alert in alert_engine.get_pending_alerts(): for alert in alert_engine.get_pending_alerts():
all_events.append({ all_events.append({
"type": alert.get("type", ""), "type": alert.get("type", ""),
"severity": alert.get("severity", "info"), "severity": alert.get("severity", "routine"),
"message": alert.get("message", ""), "message": alert.get("message", ""),
"headline": alert.get("message", "")[:80], "headline": alert.get("message", "")[:80],
}) })
@ -548,7 +548,7 @@ class NotificationRouter:
for event in env_store.get_active(): for event in env_store.get_active():
all_events.append({ all_events.append({
"type": event.get("type", event.get("category", "")), "type": event.get("type", event.get("category", "")),
"severity": event.get("severity", "info"), "severity": event.get("severity", "routine"),
"message": event.get("message", event.get("headline", str(event))), "message": event.get("message", event.get("headline", str(event))),
"headline": event.get("headline", event.get("message", "Event"))[:80], "headline": event.get("headline", event.get("message", "Event"))[:80],
}) })
@ -761,7 +761,7 @@ class NotificationRouter:
"enabled": True, "enabled": True,
"trigger_type": "condition", "trigger_type": "condition",
"categories": categories if categories else [], "categories": categories if categories else [],
"min_severity": "warning", "min_severity": "priority",
"delivery_type": "mesh_dm", "delivery_type": "mesh_dm",
"node_ids": [node_id], "node_ids": [node_id],
"cooldown_minutes": 10, "cooldown_minutes": 10,