mirror of
https://github.com/zvx-echo6/meshai.git
synced 2026-06-10 17:04:45 +02:00
feat(v0.6-phase2): rip out quiet hours entirely -- dashboard toggle, config schema, pipeline checks. Per Matt's repeated feedback (saved as feedback-quiet-hours-trash.md): silent is better than ugly, mesh users who need a fire alert at 3 AM need it at 3 AM. No replacement.
Backend removals:
meshai/config.py
- NotificationRuleConfig.override_quiet field
- NotificationToggle.quiet_hours_override field
- NotificationsConfig.quiet_hours_enabled / quiet_hours_start /
quiet_hours_end fields
- _default_toggles() no longer sets quiet_hours_override=True
- rule migration helper no longer copies override_quiet
meshai/notifications/router.py
- self._quiet_enabled / _quiet_start / _quiet_end instance vars
- _in_quiet_hours() method (deleted entirely)
- The dispatch-time check that suppressed non-overriding rules
during quiet hours
- 'override_quiet': False dropped from subscription rule dicts
meshai/notifications/pipeline/dispatcher.py
- _toggle_to_rule() no longer passes override_quiet=... to the
NotificationRuleConfig constructor
Test changes:
tests/test_notification_toggles.py
- RecChannel.deliver() no longer records override_quiet
- test_quiet_hours_override_immediate_only deleted (only tested the
removed feature)
Frontend removals (dashboard-frontend/src/pages/Notifications.tsx):
- The 'Enable Quiet Hours' card with its time-range inputs deleted
- 'Override Quiet Hours' per-rule toggle deleted
- 'Quiet-hours override (immediate only)' per-toggle field deleted
- quiet_hours_* fields removed from TS interfaces
- quietHoursEnabled prop + state plumbing removed from the RuleEditor
- All override_quiet: false defaults dropped from rule scaffolds
- Unused Moon icon import dropped
Verification (post-strip):
grep -rn 'quiet_hours\|override_quiet' meshai/*.py meshai/**/*.py
-> 0 hits
grep -rn 'quiet_hours\|override_quiet\|quietHours' dashboard-frontend/src
-> 0 hits
Test count: 830 -> 829 (-1: test_quiet_hours_override_immediate_only
deleted; no other regressions).
No replacement. Mesh users who need a fire alert at 3 AM need it at 3 AM.
This commit is contained in:
parent
90783376e8
commit
b948ed775f
8 changed files with 88 additions and 205 deletions
|
|
@ -2,7 +2,7 @@ import { useState, useEffect, useCallback } from 'react'
|
|||
import {
|
||||
Save, RotateCcw, RefreshCw, Plus, Trash2, ChevronDown, ChevronRight,
|
||||
Check, X, Eye as EyeIcon, EyeOff, Send, Clock, Zap,
|
||||
Calendar, AlertTriangle, Copy, Moon, AlertCircle, Layers,
|
||||
Calendar, AlertTriangle, Copy, AlertCircle, Layers,
|
||||
Wifi, WifiOff, Mail, Globe, Radio, MessageSquare,
|
||||
Activity, Cloud, Flame, Car, Snowflake, Mountain, MapPin
|
||||
} from 'lucide-react'
|
||||
|
|
@ -35,7 +35,6 @@ interface NotificationRuleConfig {
|
|||
webhook_url: string
|
||||
webhook_headers: Record<string, string>
|
||||
cooldown_minutes: number
|
||||
override_quiet: boolean
|
||||
region_scope: string[]
|
||||
}
|
||||
|
||||
|
|
@ -45,7 +44,6 @@ interface NotificationToggle {
|
|||
min_severity: string
|
||||
regions: string[]
|
||||
severity_channels: Record<string, string[]>
|
||||
quiet_hours_override: boolean
|
||||
broadcast_channel: number | null
|
||||
node_ids: string[]
|
||||
smtp_host: string
|
||||
|
|
@ -61,9 +59,6 @@ interface NotificationToggle {
|
|||
|
||||
interface NotificationsConfig {
|
||||
enabled: boolean
|
||||
quiet_hours_enabled: boolean
|
||||
quiet_hours_start: string
|
||||
quiet_hours_end: string
|
||||
cold_start_grace_seconds?: number
|
||||
band_conditions_enabled?: boolean
|
||||
band_conditions_schedule?: string[]
|
||||
|
|
@ -150,7 +145,6 @@ const RULE_TEMPLATES = [
|
|||
delivery_type: "mesh_broadcast",
|
||||
broadcast_channel: 0,
|
||||
cooldown_minutes: 30,
|
||||
override_quiet: false,
|
||||
schedule_frequency: "daily" as const,
|
||||
schedule_time: "07:00",
|
||||
schedule_time_2: "",
|
||||
|
|
@ -182,7 +176,6 @@ const RULE_TEMPLATES = [
|
|||
delivery_type: "mesh_broadcast",
|
||||
broadcast_channel: 0,
|
||||
cooldown_minutes: 15,
|
||||
override_quiet: false,
|
||||
schedule_frequency: "daily" as const,
|
||||
schedule_time: "07:00",
|
||||
schedule_time_2: "",
|
||||
|
|
@ -214,7 +207,6 @@ const RULE_TEMPLATES = [
|
|||
delivery_type: "mesh_broadcast",
|
||||
broadcast_channel: 0,
|
||||
cooldown_minutes: 60,
|
||||
override_quiet: false,
|
||||
schedule_frequency: "daily" as const,
|
||||
schedule_time: "07:00",
|
||||
schedule_time_2: "",
|
||||
|
|
@ -246,7 +238,6 @@ const RULE_TEMPLATES = [
|
|||
delivery_type: "mesh_broadcast",
|
||||
broadcast_channel: 0,
|
||||
cooldown_minutes: 30,
|
||||
override_quiet: false,
|
||||
schedule_frequency: "daily" as const,
|
||||
schedule_time: "07:00",
|
||||
schedule_time_2: "",
|
||||
|
|
@ -278,7 +269,6 @@ const RULE_TEMPLATES = [
|
|||
delivery_type: "mesh_broadcast",
|
||||
broadcast_channel: 0,
|
||||
cooldown_minutes: 5,
|
||||
override_quiet: true,
|
||||
schedule_frequency: "daily" as const,
|
||||
schedule_time: "07:00",
|
||||
schedule_time_2: "",
|
||||
|
|
@ -316,7 +306,6 @@ const RULE_TEMPLATES = [
|
|||
delivery_type: "mesh_broadcast",
|
||||
broadcast_channel: 0,
|
||||
cooldown_minutes: 0,
|
||||
override_quiet: false,
|
||||
node_ids: [] as string[],
|
||||
smtp_host: "",
|
||||
smtp_port: 587,
|
||||
|
|
@ -730,7 +719,6 @@ function NotificationRuleCard({
|
|||
ruleIndex,
|
||||
categories,
|
||||
regions,
|
||||
quietHoursEnabled,
|
||||
onChange,
|
||||
onDelete,
|
||||
onDuplicate,
|
||||
|
|
@ -740,7 +728,6 @@ function NotificationRuleCard({
|
|||
ruleIndex: number
|
||||
categories: AlertCategory[]
|
||||
regions: RegionInfo[]
|
||||
quietHoursEnabled: boolean
|
||||
onChange: (r: NotificationRuleConfig) => void
|
||||
onDelete: () => void
|
||||
onDuplicate: () => void
|
||||
|
|
@ -1400,18 +1387,7 @@ function NotificationRuleCard({
|
|||
min={0}
|
||||
helper="Min time between repeat sends"
|
||||
info="Prevents alert spam. Same condition won't re-trigger this rule within this window."
|
||||
/>
|
||||
{quietHoursEnabled && (
|
||||
<div className="flex items-end pb-1">
|
||||
<Toggle
|
||||
label="Override Quiet Hours"
|
||||
checked={rule.override_quiet ?? false}
|
||||
onChange={(v) => onChange({ ...rule, override_quiet: v })}
|
||||
helper="Deliver during quiet hours"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
/> </div>
|
||||
|
||||
{/* Rule statistics */}
|
||||
{ruleStats && (
|
||||
|
|
@ -1631,9 +1607,7 @@ function MasterToggles({ toggles, onChange }: {
|
|||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<ListInput label="Regions (empty = all)" value={t.regions || []} onChange={(v) => upd(key, { regions: v })} placeholder="Add region..." />
|
||||
<Toggle label="Quiet-hours override (immediate only)" checked={!!t.quiet_hours_override} onChange={(v) => upd(key, { quiet_hours_override: v })} />
|
||||
<div className="text-xs text-slate-500 pt-1">Channel config</div>
|
||||
<ListInput label="Regions (empty = all)" value={t.regions || []} onChange={(v) => upd(key, { regions: v })} placeholder="Add region..." /> <div className="text-xs text-slate-500 pt-1">Channel config</div>
|
||||
<NumberInput label="Broadcast channel" value={t.broadcast_channel ?? 0} onChange={(v) => upd(key, { broadcast_channel: v })} />
|
||||
<ListInput label="DM node IDs" value={t.node_ids || []} onChange={(v) => upd(key, { node_ids: v })} placeholder="!nodeid" />
|
||||
<ListInput label="Email recipients" value={t.recipients || []} onChange={(v) => upd(key, { recipients: v })} placeholder="ops@example.com" />
|
||||
|
|
@ -1764,7 +1738,6 @@ export default function Notifications() {
|
|||
webhook_url: '',
|
||||
webhook_headers: {},
|
||||
cooldown_minutes: 10,
|
||||
override_quiet: false,
|
||||
region_scope: [],
|
||||
})
|
||||
|
||||
|
|
@ -2082,46 +2055,7 @@ export default function Notifications() {
|
|||
/>
|
||||
|
||||
{config.enabled && (
|
||||
<>
|
||||
{/* Quiet Hours Section */}
|
||||
<div className="space-y-3 p-4 bg-[#0a0e17] rounded-lg border border-[#1e2a3a]">
|
||||
<div className="flex items-center gap-2">
|
||||
<Moon size={14} className="text-slate-400" />
|
||||
<label className="text-xs text-slate-500 uppercase tracking-wide">Quiet Hours</label>
|
||||
</div>
|
||||
|
||||
<Toggle
|
||||
label="Enable Quiet Hours"
|
||||
checked={config.quiet_hours_enabled ?? true}
|
||||
onChange={(v) => setConfig({ ...config, quiet_hours_enabled: v })}
|
||||
helper="Suppress non-emergency alerts during sleeping hours"
|
||||
info="When enabled, ROUTINE alerts are suppressed during quiet hours. PRIORITY and IMMEDIATE always deliver."
|
||||
/>
|
||||
|
||||
{config.quiet_hours_enabled && (
|
||||
<>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<TimeInput
|
||||
label="Start Time"
|
||||
value={config.quiet_hours_start || '22:00'}
|
||||
onChange={(v) => setConfig({ ...config, quiet_hours_start: v })}
|
||||
helper="When quiet hours begin"
|
||||
/>
|
||||
<TimeInput
|
||||
label="End Time"
|
||||
value={config.quiet_hours_end || '06:00'}
|
||||
onChange={(v) => setConfig({ ...config, quiet_hours_end: v })}
|
||||
helper="When quiet hours end"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-slate-600">
|
||||
Emergency alerts and rules with "Override Quiet Hours" enabled always deliver.
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Cold-start grace -- v0.5.8b */}
|
||||
<> {/* Cold-start grace -- v0.5.8b */}
|
||||
<div className="space-y-3 p-4 bg-[#0a0e17] rounded-lg border border-[#1e2a3a]">
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="text-xs text-slate-500 uppercase tracking-wide">Cold-start grace</label>
|
||||
|
|
@ -2212,9 +2146,7 @@ export default function Notifications() {
|
|||
rule={rule}
|
||||
ruleIndex={i}
|
||||
categories={categories}
|
||||
regions={regions}
|
||||
quietHoursEnabled={config.quiet_hours_enabled ?? true}
|
||||
onChange={(r) => {
|
||||
regions={regions} onChange={(r) => {
|
||||
const newRules = [...(config.rules || [])]
|
||||
newRules[i] = r
|
||||
setConfig({ ...config, rules: newRules })
|
||||
|
|
|
|||
|
|
@ -524,7 +524,6 @@ class NotificationRuleConfig:
|
|||
|
||||
# Behavior
|
||||
cooldown_minutes: int = 10
|
||||
override_quiet: bool = False
|
||||
|
||||
# Legacy field for migration (ignored in new format)
|
||||
channel_ids: list = field(default_factory=list)
|
||||
|
|
@ -542,7 +541,6 @@ class NotificationToggle:
|
|||
regions: list = field(default_factory=list) # [] = all regions
|
||||
# severity -> list of channel types (digest|mesh_broadcast|mesh_dm|email|webhook)
|
||||
severity_channels: dict = field(default_factory=dict)
|
||||
quiet_hours_override: bool = True # immediate-only quiet-hours bypass
|
||||
# v0.5.2: staleness drop + per-toggle cooldown (Matt's spam fix)
|
||||
freshness_seconds: int = 600 # drop events older than this at dispatcher entrance
|
||||
cooldown_seconds: int = 300 # per (toggle, category, region) throttle window
|
||||
|
|
@ -578,7 +576,6 @@ def _default_toggles() -> dict:
|
|||
"priority": ["mesh_broadcast"],
|
||||
"immediate": ["mesh_broadcast", "mesh_dm"],
|
||||
},
|
||||
quiet_hours_override=True,
|
||||
)
|
||||
for fam in TOGGLE_FAMILIES
|
||||
}
|
||||
|
|
@ -604,9 +601,6 @@ class NotificationsConfig:
|
|||
"""Notification system settings."""
|
||||
|
||||
enabled: bool = False
|
||||
quiet_hours_enabled: bool = True # Master toggle for quiet hours
|
||||
quiet_hours_start: str = "22:00"
|
||||
quiet_hours_end: str = "06:00"
|
||||
# v0.5.8b cold-start grace: after the first event the dispatcher sees,
|
||||
# suppress mesh broadcasts for N seconds to absorb any JetStream
|
||||
# backlog. Persistence rows still get written -- only broadcasts are
|
||||
|
|
@ -725,7 +719,6 @@ def _migrate_legacy_channels(notifications, data: dict):
|
|||
webhook_url=ch.get("url", ""),
|
||||
webhook_headers=ch.get("headers", {}),
|
||||
cooldown_minutes=10,
|
||||
override_quiet=old_rule.get("override_quiet", False),
|
||||
)
|
||||
migrated_rules.append(new_rule)
|
||||
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -8,8 +8,8 @@
|
|||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<script type="module" crossorigin src="/assets/index-Dc1UcqB9.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-Bj-HMHAO.css">
|
||||
<script type="module" crossorigin src="/assets/index-C_RINFTf.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-j88L17ja.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
|
|
|||
|
|
@ -614,7 +614,6 @@ class Dispatcher:
|
|||
recipients=list(getattr(tog, "recipients", []) or []),
|
||||
webhook_url=getattr(tog, "webhook_url", ""),
|
||||
webhook_headers=dict(getattr(tog, "webhook_headers", {}) or {}),
|
||||
override_quiet=bool(getattr(tog, "quiet_hours_override", False) and event.severity == "immediate"),
|
||||
)
|
||||
|
||||
def _matching_rules(self, event: Event) -> list:
|
||||
|
|
|
|||
|
|
@ -35,9 +35,6 @@ class NotificationRouter:
|
|||
timezone: str = "America/Boise",
|
||||
):
|
||||
self._rules: list[dict] = []
|
||||
self._quiet_enabled = getattr(config, "quiet_hours_enabled", True)
|
||||
self._quiet_start = getattr(config, "quiet_hours_start", "22:00")
|
||||
self._quiet_end = getattr(config, "quiet_hours_end", "06:00")
|
||||
self._timezone = timezone
|
||||
self._recent: dict[tuple, float] = {} # (rule_name, category, event_key) -> last_sent_time
|
||||
self._summarizer = MessageSummarizer(llm_backend) if llm_backend else None
|
||||
|
|
@ -165,11 +162,6 @@ class NotificationRouter:
|
|||
if not self._severity_meets(severity, min_severity):
|
||||
continue
|
||||
|
||||
if self._quiet_enabled and self._in_quiet_hours():
|
||||
if severity == "routine":
|
||||
if not rule.get("override_quiet", False):
|
||||
continue
|
||||
|
||||
cooldown = rule.get("cooldown_minutes", 10) * 60
|
||||
event_id = alert.get("event_id", alert.get("message", "")[:50])
|
||||
dedup_key = (rule_name, category, event_id)
|
||||
|
|
@ -231,24 +223,6 @@ class NotificationRouter:
|
|||
except ValueError:
|
||||
return True
|
||||
|
||||
def _in_quiet_hours(self) -> bool:
|
||||
"""Check if current time is within quiet hours."""
|
||||
if not self._quiet_enabled:
|
||||
return False
|
||||
try:
|
||||
from zoneinfo import ZoneInfo
|
||||
tz = ZoneInfo(self._timezone)
|
||||
now = datetime.now(tz)
|
||||
current_time = now.strftime("%H:%M")
|
||||
start = self._quiet_start
|
||||
end = self._quiet_end
|
||||
if start <= end:
|
||||
return start <= current_time <= end
|
||||
else:
|
||||
return current_time >= start or current_time <= end
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def get_rules(self) -> list[dict]:
|
||||
"""Get list of configured rules with stats."""
|
||||
rules_with_stats = []
|
||||
|
|
@ -780,7 +754,6 @@ class NotificationRouter:
|
|||
"delivery_type": "mesh_dm",
|
||||
"node_ids": [node_id],
|
||||
"cooldown_minutes": 10,
|
||||
"override_quiet": False,
|
||||
})
|
||||
|
||||
return rule_name
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ class RecChannel:
|
|||
"name": rule.name,
|
||||
"broadcast_channel": rule.broadcast_channel,
|
||||
"node_ids": list(rule.node_ids),
|
||||
"override_quiet": rule.override_quiet,
|
||||
})
|
||||
return True
|
||||
|
||||
|
|
@ -115,14 +114,6 @@ def test_digest_channel_skipped_in_live_dispatch():
|
|||
assert [r["delivery_type"] for r in rec] == ["mesh_broadcast"] # digest not live-dispatched
|
||||
|
||||
|
||||
def test_quiet_hours_override_immediate_only():
|
||||
cfg = _cfg(min_severity="routine",
|
||||
severity_channels={"priority": ["mesh_broadcast"], "immediate": ["mesh_broadcast"]})
|
||||
cfg.notifications.toggles["weather"].quiet_hours_override = True
|
||||
assert _dispatch(cfg, _ev(severity="priority"))[0]["override_quiet"] is False
|
||||
assert _dispatch(cfg, _ev(severity="immediate"))[0]["override_quiet"] is True
|
||||
|
||||
|
||||
def test_category_maps_to_correct_family():
|
||||
# seismic family toggle handles earthquake_event via get_toggle fallback
|
||||
cfg = Config(); cfg.notifications.rules = []
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue