mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-04-06 04:13:48 +00:00
update notification service
This commit is contained in:
@@ -223,6 +223,148 @@ export function NotificationSettings() {
|
||||
}))
|
||||
}
|
||||
|
||||
/** Reusable 10+1 category block rendered inside each channel tab. */
|
||||
const renderChannelCategories = (chName: string, accentColor: string) => {
|
||||
const overrides = config.channel_overrides?.[chName] || { categories: {}, events: {} }
|
||||
const evtByGroup = config.event_types_by_group || {}
|
||||
|
||||
return (
|
||||
<div className="space-y-1.5 border-t border-border/30 pt-3 mt-3">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Bell className="h-3 w-3 text-muted-foreground" />
|
||||
<Label className="text-[11px] text-muted-foreground">Notification Categories</Label>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
{EVENT_CATEGORIES.map(cat => {
|
||||
const isEnabled = overrides.categories[cat.key] ?? true
|
||||
const isExpanded = expandedCategories.has(`${chName}.${cat.key}`)
|
||||
const eventsForGroup = evtByGroup[cat.key] || []
|
||||
const enabledCount = eventsForGroup.filter(
|
||||
e => (overrides.events?.[e.type] ?? e.default_enabled)
|
||||
).length
|
||||
|
||||
return (
|
||||
<div key={cat.key} className={`rounded-md border transition-colors ${
|
||||
isEnabled ? `border-${accentColor}-500/30 bg-${accentColor}-500/5` : "border-border/50 bg-transparent"
|
||||
}`}>
|
||||
<div className="flex items-center gap-2 p-2">
|
||||
<button
|
||||
type="button"
|
||||
className={`shrink-0 transition-transform ${isExpanded ? "rotate-90" : ""} ${
|
||||
!isEnabled ? "opacity-30 pointer-events-none" : "text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
onClick={() => {
|
||||
if (!isEnabled) return
|
||||
setExpandedCategories(prev => {
|
||||
const next = new Set(prev)
|
||||
const key = `${chName}.${cat.key}`
|
||||
if (next.has(key)) next.delete(key)
|
||||
else next.add(key)
|
||||
return next
|
||||
})
|
||||
}}
|
||||
aria-label={isExpanded ? "Collapse" : "Expand"}
|
||||
>
|
||||
<ChevronRight className="h-3 w-3" />
|
||||
</button>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<span className={`text-[11px] font-medium block ${
|
||||
isEnabled ? `text-${accentColor}-400` : "text-foreground"
|
||||
}`}>{cat.label}</span>
|
||||
<span className="text-[9px] text-muted-foreground">{cat.desc}</span>
|
||||
</div>
|
||||
|
||||
{isEnabled && eventsForGroup.length > 0 && (
|
||||
<span className="text-[9px] text-muted-foreground tabular-nums">
|
||||
{enabledCount}/{eventsForGroup.length}
|
||||
</span>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={isEnabled}
|
||||
disabled={!editMode}
|
||||
className={`relative inline-flex h-4 w-7 shrink-0 items-center rounded-full transition-colors ${
|
||||
!editMode ? "opacity-50 cursor-not-allowed" : "cursor-pointer"
|
||||
} ${isEnabled ? `bg-${accentColor}-600` : "bg-muted-foreground/30"}`}
|
||||
onClick={() => {
|
||||
if (!editMode) return
|
||||
updateConfig(p => {
|
||||
const ch = { ...(p.channel_overrides?.[chName] || { categories: {}, events: {} }) }
|
||||
const newEnabled = !isEnabled
|
||||
const newEvents = { ...(ch.events || {}) }
|
||||
// When enabling, turn all sub-events on
|
||||
if (newEnabled && eventsForGroup.length > 0) {
|
||||
for (const evt of eventsForGroup) {
|
||||
newEvents[evt.type] = true
|
||||
}
|
||||
}
|
||||
return {
|
||||
...p,
|
||||
channel_overrides: {
|
||||
...p.channel_overrides,
|
||||
[chName]: { categories: { ...ch.categories, [cat.key]: newEnabled }, events: newEvents },
|
||||
},
|
||||
}
|
||||
})
|
||||
}}
|
||||
>
|
||||
<span className={`pointer-events-none block h-3 w-3 rounded-full bg-background shadow-sm transition-transform ${
|
||||
isEnabled ? "translate-x-3.5" : "translate-x-0.5"
|
||||
}`} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{isEnabled && isExpanded && eventsForGroup.length > 0 && (
|
||||
<div className="border-t border-border/30 px-2 py-1.5 space-y-0.5">
|
||||
{eventsForGroup.map(evt => {
|
||||
const evtEnabled = overrides.events?.[evt.type] ?? evt.default_enabled
|
||||
return (
|
||||
<div key={evt.type} className="flex items-center justify-between py-0.5 px-2 rounded hover:bg-muted/30 transition-colors">
|
||||
<span className={`text-[10px] ${evtEnabled ? `text-${accentColor}-400` : "text-muted-foreground"}`}>
|
||||
{evt.title}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={evtEnabled}
|
||||
disabled={!editMode}
|
||||
className={`relative inline-flex h-3.5 w-6 shrink-0 items-center rounded-full transition-colors ${
|
||||
!editMode ? "opacity-50 cursor-not-allowed" : "cursor-pointer"
|
||||
} ${evtEnabled ? `bg-${accentColor}-600` : "bg-muted-foreground/30"}`}
|
||||
onClick={() => {
|
||||
if (!editMode) return
|
||||
updateConfig(p => {
|
||||
const ch = { ...(p.channel_overrides?.[chName] || { categories: {}, events: {} }) }
|
||||
return {
|
||||
...p,
|
||||
channel_overrides: {
|
||||
...p.channel_overrides,
|
||||
[chName]: { ...ch, events: { ...(ch.events || {}), [evt.type]: !evtEnabled } },
|
||||
},
|
||||
}
|
||||
})
|
||||
}}
|
||||
>
|
||||
<span className={`pointer-events-none block h-2.5 w-2.5 rounded-full bg-background shadow-sm transition-transform ${
|
||||
evtEnabled ? "translate-x-3" : "translate-x-0.5"
|
||||
}`} />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/** Flatten the nested NotificationConfig into the flat key-value map the backend expects. */
|
||||
const flattenConfig = (cfg: NotificationConfig): Record<string, string> => {
|
||||
const flat: Record<string, string> = {
|
||||
@@ -244,28 +386,8 @@ export function NotificationSettings() {
|
||||
flat[`${chName}.${field}`] = String(value ?? "")
|
||||
}
|
||||
}
|
||||
// Flatten global event_categories: { vm_ct: true, backup: false } -> events.vm_ct, events.backup
|
||||
for (const [cat, enabled] of Object.entries(cfg.event_categories)) {
|
||||
flat[`events.${cat}`] = String(enabled)
|
||||
}
|
||||
// Flatten global event_toggles: { vm_start: true } -> event.vm_start
|
||||
if (cfg.event_toggles) {
|
||||
for (const [evt, enabled] of Object.entries(cfg.event_toggles)) {
|
||||
flat[`event.${evt}`] = String(enabled)
|
||||
}
|
||||
}
|
||||
// Write defaults for events NOT in toggles
|
||||
if (cfg.event_types_by_group) {
|
||||
for (const events of Object.values(cfg.event_types_by_group)) {
|
||||
for (const evt of (events as Array<{type: string, default_enabled: boolean}>)) {
|
||||
const key = `event.${evt.type}`
|
||||
if (!(key in flat)) {
|
||||
flat[key] = String(evt.default_enabled)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Flatten per-channel overrides: telegram.events.backup, telegram.event.vm_start, etc.
|
||||
// Per-channel category & event toggles: telegram.events.vm_ct, telegram.event.vm_start, etc.
|
||||
// Each channel independently owns its notification preferences.
|
||||
if (cfg.channel_overrides) {
|
||||
for (const [chName, overrides] of Object.entries(cfg.channel_overrides)) {
|
||||
if (overrides.categories) {
|
||||
@@ -754,6 +876,7 @@ matcher: proxmenux-pbs
|
||||
onChange={e => updateChannel("telegram", "chat_id", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
{renderChannelCategories("telegram", "blue")}
|
||||
{/* Per-channel action bar */}
|
||||
<div className="flex items-center gap-2 pt-2 border-t border-border/50">
|
||||
<button
|
||||
@@ -823,6 +946,7 @@ matcher: proxmenux-pbs
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{renderChannelCategories("gotify", "green")}
|
||||
{/* Per-channel action bar */}
|
||||
<div className="flex items-center gap-2 pt-2 border-t border-border/50">
|
||||
<button
|
||||
@@ -883,6 +1007,7 @@ matcher: proxmenux-pbs
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{renderChannelCategories("discord", "indigo")}
|
||||
{/* Per-channel action bar */}
|
||||
<div className="flex items-center gap-2 pt-2 border-t border-border/50">
|
||||
<button
|
||||
@@ -1024,6 +1149,7 @@ matcher: proxmenux-pbs
|
||||
For Gmail, use an App Password instead of your account password.
|
||||
</p>
|
||||
</div>
|
||||
{renderChannelCategories("email", "amber")}
|
||||
{/* Per-channel action bar */}
|
||||
<div className="flex items-center gap-2 pt-2 border-t border-border/50">
|
||||
<button
|
||||
@@ -1066,255 +1192,6 @@ matcher: proxmenux-pbs
|
||||
</div>{/* close bordered channel container */}
|
||||
</div>
|
||||
|
||||
{/* ── Filters ── */}
|
||||
<div className="space-y-3 border-t border-border pt-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<AlertTriangle className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wider">Filters & Events</span>
|
||||
</div>
|
||||
<div className="rounded-lg border border-border/50 bg-muted/20 p-3 space-y-4">
|
||||
{/* Event Categories (global defaults -- per-channel overrides in Channel Filters below) */}
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-[11px] text-muted-foreground">Event Categories</Label>
|
||||
<div className="space-y-1.5">
|
||||
{EVENT_CATEGORIES.map(cat => {
|
||||
const isEnabled = config.event_categories[cat.key] ?? true
|
||||
const isExpanded = expandedCategories.has(cat.key)
|
||||
const eventsForGroup = config.event_types_by_group?.[cat.key] || []
|
||||
const enabledCount = eventsForGroup.filter(e => config.event_toggles?.[e.type] ?? e.default_enabled).length
|
||||
|
||||
return (
|
||||
<div key={cat.key} className={`rounded-md border transition-colors ${
|
||||
isEnabled ? "border-green-500/30 bg-green-500/5" : "border-border/50 bg-transparent"
|
||||
}`}>
|
||||
{/* Category header row */}
|
||||
<div className="flex items-center gap-2.5 p-2.5">
|
||||
{/* Expand/collapse button */}
|
||||
<button
|
||||
type="button"
|
||||
className={`shrink-0 transition-transform ${isExpanded ? "rotate-90" : ""} ${
|
||||
!isEnabled ? "opacity-30 pointer-events-none" : "text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
onClick={() => {
|
||||
if (!isEnabled) return
|
||||
setExpandedCategories(prev => {
|
||||
const next = new Set(prev)
|
||||
if (next.has(cat.key)) next.delete(cat.key)
|
||||
else next.add(cat.key)
|
||||
return next
|
||||
})
|
||||
}}
|
||||
aria-label={isExpanded ? "Collapse" : "Expand"}
|
||||
>
|
||||
<ChevronRight className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
|
||||
{/* Label + description */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<span className={`text-xs font-medium block ${
|
||||
isEnabled ? "text-green-400" : "text-foreground"
|
||||
}`}>
|
||||
{cat.label}
|
||||
</span>
|
||||
<span className="text-[10px] text-muted-foreground">{cat.desc}</span>
|
||||
</div>
|
||||
|
||||
{/* Count badge */}
|
||||
{isEnabled && eventsForGroup.length > 0 && (
|
||||
<span className="text-[10px] text-muted-foreground tabular-nums">
|
||||
{enabledCount}/{eventsForGroup.length}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Category toggle */}
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={isEnabled}
|
||||
disabled={!editMode}
|
||||
className={`relative inline-flex h-5 w-9 shrink-0 items-center rounded-full transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring ${
|
||||
!editMode ? "opacity-50 cursor-not-allowed" : "cursor-pointer"
|
||||
} ${isEnabled ? "bg-green-600" : "bg-muted-foreground/30"}`}
|
||||
onClick={() => {
|
||||
if (!editMode) return
|
||||
const newEnabled = !isEnabled
|
||||
updateConfig(p => {
|
||||
const newToggles = { ...(p.event_toggles || {}) }
|
||||
// When enabling a category, turn all its events on by default
|
||||
if (newEnabled && eventsForGroup.length > 0) {
|
||||
for (const evt of eventsForGroup) {
|
||||
newToggles[evt.type] = true
|
||||
}
|
||||
}
|
||||
return {
|
||||
...p,
|
||||
event_categories: { ...p.event_categories, [cat.key]: newEnabled },
|
||||
event_toggles: newToggles,
|
||||
}
|
||||
})
|
||||
}}
|
||||
>
|
||||
<span className={`pointer-events-none block h-4 w-4 rounded-full bg-background shadow-sm transition-transform ${
|
||||
isEnabled ? "translate-x-4" : "translate-x-0.5"
|
||||
}`} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Per-event toggles (expanded) */}
|
||||
{isEnabled && isExpanded && eventsForGroup.length > 0 && (
|
||||
<div className="border-t border-border/30 px-2.5 py-2 space-y-0.5">
|
||||
{eventsForGroup.map(evt => {
|
||||
const evtEnabled = config.event_toggles?.[evt.type] ?? evt.default_enabled
|
||||
return (
|
||||
<div key={evt.type} className="flex items-center justify-between py-1 px-2 rounded hover:bg-muted/30 transition-colors">
|
||||
<span className={`text-[11px] ${evtEnabled ? "text-green-400" : "text-muted-foreground"}`}>
|
||||
{evt.title}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={evtEnabled}
|
||||
disabled={!editMode}
|
||||
className={`relative inline-flex h-4 w-7 shrink-0 items-center rounded-full transition-colors focus-visible:outline-none ${
|
||||
!editMode ? "opacity-50 cursor-not-allowed" : "cursor-pointer"
|
||||
} ${evtEnabled ? "bg-green-600" : "bg-muted-foreground/30"}`}
|
||||
onClick={() => {
|
||||
if (!editMode) return
|
||||
updateConfig(p => ({
|
||||
...p,
|
||||
event_toggles: { ...(p.event_toggles || {}), [evt.type]: !evtEnabled },
|
||||
}))
|
||||
}}
|
||||
>
|
||||
<span className={`pointer-events-none block h-3 w-3 rounded-full bg-background shadow-sm transition-transform ${
|
||||
evtEnabled ? "translate-x-3.5" : "translate-x-0.5"
|
||||
}`} />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Per-channel overrides */}
|
||||
<div className="space-y-2 border-t border-border/30 pt-3">
|
||||
<Label className="text-[11px] text-muted-foreground">Channel Filters</Label>
|
||||
<p className="text-[10px] text-muted-foreground leading-relaxed">
|
||||
By default every channel inherits the global settings above. Override specific categories per channel to customize what each destination receives.
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
{CHANNEL_TYPES.map(chName => {
|
||||
const chEnabled = config.channels[chName]?.enabled
|
||||
if (!chEnabled) return null
|
||||
const overrides = config.channel_overrides?.[chName] || { categories: {}, events: {} }
|
||||
const hasOverrides = Object.keys(overrides.categories).length > 0
|
||||
const chLabel = chName === "email" ? "Email" : chName.charAt(0).toUpperCase() + chName.slice(1)
|
||||
const chColor = chName === "telegram" ? "blue" : chName === "gotify" ? "green" : chName === "discord" ? "indigo" : "amber"
|
||||
|
||||
return (
|
||||
<details key={chName} className="group">
|
||||
<summary className={`flex items-center justify-between text-[11px] font-medium cursor-pointer hover:text-foreground transition-colors py-1.5 px-2 rounded-md hover:bg-muted/50 ${
|
||||
hasOverrides ? `text-${chColor}-400` : "text-muted-foreground"
|
||||
}`}>
|
||||
<div className="flex items-center gap-2">
|
||||
<ChevronDown className="h-3 w-3 group-open:rotate-180 transition-transform" />
|
||||
<span>{chLabel}</span>
|
||||
{hasOverrides && (
|
||||
<span className={`text-[9px] px-1.5 py-0.5 rounded-full bg-${chColor}-500/15 text-${chColor}-400`}>
|
||||
customized
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{!hasOverrides && (
|
||||
<span className="text-[9px] text-muted-foreground/60">inherits global</span>
|
||||
)}
|
||||
</summary>
|
||||
<div className="mt-1.5 ml-5 space-y-1">
|
||||
{EVENT_CATEGORIES.map(cat => {
|
||||
const globalEnabled = config.event_categories[cat.key] ?? true
|
||||
const override = overrides.categories[cat.key]
|
||||
const isCustomized = override !== undefined
|
||||
const effectiveEnabled = isCustomized ? override : globalEnabled
|
||||
|
||||
return (
|
||||
<div key={cat.key} className="flex items-center justify-between py-1 px-2 rounded hover:bg-muted/30">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`text-[11px] ${effectiveEnabled ? "text-foreground" : "text-muted-foreground/50"}`}>
|
||||
{cat.label}
|
||||
</span>
|
||||
{!isCustomized && (
|
||||
<span className="text-[9px] text-muted-foreground/40">global</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
{isCustomized && (
|
||||
<button
|
||||
type="button"
|
||||
className="text-[9px] text-muted-foreground hover:text-foreground px-1"
|
||||
disabled={!editMode}
|
||||
onClick={() => {
|
||||
if (!editMode) return
|
||||
updateConfig(p => {
|
||||
const ch = { ...(p.channel_overrides?.[chName] || { categories: {}, events: {} }) }
|
||||
const cats = { ...ch.categories }
|
||||
delete cats[cat.key]
|
||||
return { ...p, channel_overrides: { ...p.channel_overrides, [chName]: { ...ch, categories: cats } } }
|
||||
})
|
||||
}}
|
||||
>
|
||||
reset
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={effectiveEnabled}
|
||||
disabled={!editMode}
|
||||
className={`relative inline-flex h-3.5 w-6 shrink-0 items-center rounded-full transition-colors ${
|
||||
!editMode ? "opacity-50 cursor-not-allowed" : "cursor-pointer"
|
||||
} ${effectiveEnabled ? `bg-${chColor}-600` : "bg-muted-foreground/30"}`}
|
||||
onClick={() => {
|
||||
if (!editMode) return
|
||||
updateConfig(p => {
|
||||
const ch = { ...(p.channel_overrides?.[chName] || { categories: {}, events: {} }) }
|
||||
return {
|
||||
...p,
|
||||
channel_overrides: {
|
||||
...p.channel_overrides,
|
||||
[chName]: { ...ch, categories: { ...ch.categories, [cat.key]: !effectiveEnabled } }
|
||||
}
|
||||
}
|
||||
})
|
||||
}}
|
||||
>
|
||||
<span className={`pointer-events-none block h-2.5 w-2.5 rounded-full bg-background shadow-sm transition-transform ${
|
||||
effectiveEnabled ? "translate-x-3" : "translate-x-0.5"
|
||||
}`} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</details>
|
||||
)
|
||||
})}
|
||||
{CHANNEL_TYPES.every(ch => !config.channels[ch]?.enabled) && (
|
||||
<p className="text-[10px] text-muted-foreground/50 italic py-2">
|
||||
Enable at least one channel above to configure per-channel filters.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>{/* close bordered filters container */}
|
||||
</div>
|
||||
|
||||
{/* ── Proxmox Webhook ── */}
|
||||
<div className="space-y-3 border-t border-border pt-4">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
|
||||
@@ -560,28 +560,14 @@ class NotificationManager:
|
||||
print(f"[NotificationManager] Aggregation flush error: {e}")
|
||||
|
||||
def _process_event(self, event: NotificationEvent):
|
||||
"""Process a single event: filter -> aggregate -> cooldown -> rate limit -> dispatch.
|
||||
"""Process a single event: aggregate -> cooldown -> rate limit -> dispatch.
|
||||
|
||||
NOTE: Group and per-event filters are checked globally here.
|
||||
Per-channel overrides are applied later in _dispatch_to_channels().
|
||||
Per-channel category/event filters are applied in _dispatch_to_channels().
|
||||
No global category/event filter exists -- each channel decides independently.
|
||||
"""
|
||||
if not self._enabled:
|
||||
return
|
||||
|
||||
# Check if this event's GROUP is enabled globally.
|
||||
template = TEMPLATES.get(event.event_type, {})
|
||||
event_group = template.get('group', 'other')
|
||||
group_setting = f'events.{event_group}'
|
||||
if self._config.get(group_setting, 'true') == 'false':
|
||||
return
|
||||
|
||||
# Check if this SPECIFIC event type is enabled globally.
|
||||
# Default comes from the template's default_enabled field.
|
||||
default_enabled = 'true' if template.get('default_enabled', True) else 'false'
|
||||
event_specific = f'event.{event.event_type}'
|
||||
if self._config.get(event_specific, default_enabled) == 'false':
|
||||
return
|
||||
|
||||
# Try aggregation (may buffer the event)
|
||||
result = self._aggregator.ingest(event)
|
||||
if result is None:
|
||||
@@ -592,23 +578,10 @@ class NotificationManager:
|
||||
self._dispatch_event(event)
|
||||
|
||||
def _process_event_direct(self, event: NotificationEvent):
|
||||
"""Process a burst summary event. Bypasses aggregator but applies global filters."""
|
||||
"""Process a burst summary event. Bypasses aggregator but applies cooldown + rate limit."""
|
||||
if not self._enabled:
|
||||
return
|
||||
|
||||
# Check group filter
|
||||
template = TEMPLATES.get(event.event_type, {})
|
||||
event_group = template.get('group', 'other')
|
||||
group_setting = f'events.{event_group}'
|
||||
if self._config.get(group_setting, 'true') == 'false':
|
||||
return
|
||||
|
||||
# Check per-event filter
|
||||
default_enabled = 'true' if template.get('default_enabled', True) else 'false'
|
||||
event_specific = f'event.{event.event_type}'
|
||||
if self._config.get(event_specific, default_enabled) == 'false':
|
||||
return
|
||||
|
||||
self._dispatch_event(event)
|
||||
|
||||
def _dispatch_event(self, event: NotificationEvent):
|
||||
@@ -666,32 +639,32 @@ class NotificationManager:
|
||||
|
||||
def _dispatch_to_channels(self, title: str, body: str, severity: str,
|
||||
event_type: str, data: Dict, source: str):
|
||||
"""Send notification through configured channels, respecting per-channel overrides.
|
||||
"""Send notification through configured channels, respecting per-channel filters.
|
||||
|
||||
Each channel can override global category/event settings:
|
||||
- {channel}.events.{group} = "true"/"false" (category override)
|
||||
- {channel}.event.{type} = "true"/"false" (per-event override)
|
||||
If no override exists, the channel inherits the global setting (already checked).
|
||||
Each channel owns its own category/event preferences:
|
||||
- {channel}.events.{group} = "true"/"false" (category toggle, default "true")
|
||||
- {channel}.event.{type} = "true"/"false" (per-event toggle, default from template)
|
||||
No global fallback -- each channel decides independently what it receives.
|
||||
"""
|
||||
with self._lock:
|
||||
channels = dict(self._channels)
|
||||
|
||||
template = TEMPLATES.get(event_type, {})
|
||||
event_group = template.get('group', 'other')
|
||||
default_event_enabled = 'true' if template.get('default_enabled', True) else 'false'
|
||||
|
||||
for ch_name, channel in channels.items():
|
||||
# ── Per-channel override check ──
|
||||
# If the channel has an explicit override for this group or event, respect it.
|
||||
# If no override, the global filter already passed (checked in _process_event).
|
||||
# ── Per-channel category check ──
|
||||
# Default: category enabled (true) unless explicitly disabled.
|
||||
ch_group_key = f'{ch_name}.events.{event_group}'
|
||||
ch_group_override = self._config.get(ch_group_key)
|
||||
if ch_group_override == 'false':
|
||||
continue # Channel explicitly disabled this category
|
||||
if self._config.get(ch_group_key, 'true') == 'false':
|
||||
continue # Channel has this category disabled
|
||||
|
||||
# ── Per-channel event check ──
|
||||
# Default: from template default_enabled, unless explicitly set.
|
||||
ch_event_key = f'{ch_name}.event.{event_type}'
|
||||
ch_event_override = self._config.get(ch_event_key)
|
||||
if ch_event_override == 'false':
|
||||
continue # Channel explicitly disabled this event
|
||||
if self._config.get(ch_event_key, default_event_enabled) == 'false':
|
||||
continue # Channel has this specific event disabled
|
||||
|
||||
try:
|
||||
result = channel.send(title, body, severity, data)
|
||||
@@ -1178,45 +1151,30 @@ class NotificationManager:
|
||||
ch_cfg[config_key] = self._config.get(f'{ch_type}.{config_key}', '')
|
||||
channels[ch_type] = ch_cfg
|
||||
|
||||
# Build event_categories dict (group-level toggle)
|
||||
# EVENT_GROUPS is a dict: { 'vm_ct': {...}, 'services': {...}, 'health': {...}, ... }
|
||||
event_categories = {}
|
||||
for group_key in EVENT_GROUPS:
|
||||
event_categories[group_key] = self._config.get(f'events.{group_key}', 'true') == 'true'
|
||||
|
||||
# Build per-event toggles: { 'vm_start': true, 'vm_stop': false, ... }
|
||||
event_toggles = {}
|
||||
for event_type, tmpl in TEMPLATES.items():
|
||||
default = tmpl.get('default_enabled', True)
|
||||
saved = self._config.get(f'event.{event_type}', None)
|
||||
if saved is not None:
|
||||
event_toggles[event_type] = saved == 'true'
|
||||
else:
|
||||
event_toggles[event_type] = default
|
||||
|
||||
# Build event_types_by_group for UI rendering
|
||||
event_types_by_group = get_event_types_by_group()
|
||||
|
||||
# Build per-channel overrides
|
||||
# Each channel independently owns its category and event toggles.
|
||||
# Keys: {channel}.events.{group} and {channel}.event.{event_type}
|
||||
# Defaults: categories default to true, events default to template default_enabled.
|
||||
channel_overrides = {}
|
||||
for ch_type in CHANNEL_TYPES:
|
||||
ch_overrides = {'categories': {}, 'events': {}}
|
||||
for group_key in EVENT_GROUPS:
|
||||
val = self._config.get(f'{ch_type}.events.{group_key}')
|
||||
if val is not None:
|
||||
ch_overrides['categories'][group_key] = val == 'true'
|
||||
for event_type_key in TEMPLATES:
|
||||
val = self._config.get(f'{ch_type}.event.{event_type_key}')
|
||||
if val is not None:
|
||||
ch_overrides['events'][event_type_key] = val == 'true'
|
||||
saved = self._config.get(f'{ch_type}.events.{group_key}')
|
||||
ch_overrides['categories'][group_key] = (saved or 'true') == 'true'
|
||||
for event_type_key, tmpl in TEMPLATES.items():
|
||||
default = 'true' if tmpl.get('default_enabled', True) else 'false'
|
||||
saved = self._config.get(f'{ch_type}.event.{event_type_key}')
|
||||
ch_overrides['events'][event_type_key] = (saved or default) == 'true'
|
||||
channel_overrides[ch_type] = ch_overrides
|
||||
|
||||
config = {
|
||||
'enabled': self._enabled,
|
||||
'channels': channels,
|
||||
'event_categories': event_categories,
|
||||
'event_toggles': event_toggles,
|
||||
'event_categories': {},
|
||||
'event_toggles': {},
|
||||
'event_types_by_group': event_types_by_group,
|
||||
'channel_overrides': channel_overrides,
|
||||
'ai_enabled': self._config.get('ai_enabled', 'false') == 'true',
|
||||
|
||||
Reference in New Issue
Block a user