mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-04-18 01:52:20 +00:00
Update notification service
This commit is contained in:
@@ -235,7 +235,7 @@ export function NotificationSettings() {
|
||||
<Label className="text-[11px] text-muted-foreground">Notification Categories</Label>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{EVENT_CATEGORIES.map(cat => {
|
||||
{EVENT_CATEGORIES.filter(cat => cat.key !== "other").map(cat => {
|
||||
const isEnabled = overrides.categories[cat.key] ?? true
|
||||
const isExpanded = expandedCategories.has(`${chName}.${cat.key}`)
|
||||
const eventsForGroup = evtByGroup[cat.key] || []
|
||||
@@ -244,7 +244,7 @@ export function NotificationSettings() {
|
||||
).length
|
||||
|
||||
return (
|
||||
<div key={cat.key} className="rounded-lg border border-border transition-colors hover:border-border/80 hover:bg-muted/30">
|
||||
<div key={cat.key} className="rounded-lg border border-border transition-all duration-150 hover:border-muted-foreground/50 hover:bg-muted/40">
|
||||
{/* Category row -- entire block is clickable to expand/collapse */}
|
||||
<div
|
||||
className="flex items-center gap-2.5 py-2.5 px-3 cursor-pointer"
|
||||
@@ -286,7 +286,7 @@ export function NotificationSettings() {
|
||||
disabled={!editMode}
|
||||
className={`relative w-9 h-[18px] shrink-0 rounded-full transition-colors ${
|
||||
!editMode ? "opacity-50 cursor-not-allowed" : "cursor-pointer"
|
||||
} ${isEnabled ? "bg-blue-600" : "bg-muted-foreground/30"}`}
|
||||
} ${isEnabled ? "bg-blue-600" : "bg-muted-foreground/20 border border-muted-foreground/40"}`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
if (!editMode) return
|
||||
@@ -321,7 +321,7 @@ export function NotificationSettings() {
|
||||
{eventsForGroup.map(evt => {
|
||||
const evtEnabled = overrides.events?.[evt.type] ?? evt.default_enabled
|
||||
return (
|
||||
<div key={evt.type} className="flex items-center justify-between py-1.5 px-2 rounded-md hover:bg-muted/30 transition-colors">
|
||||
<div key={evt.type} className="flex items-center justify-between py-1.5 px-2 rounded-md hover:bg-muted/40 transition-colors">
|
||||
<span className={`text-[11px] sm:text-xs ${evtEnabled ? "text-foreground" : "text-muted-foreground"}`}>
|
||||
{evt.title}
|
||||
</span>
|
||||
@@ -332,7 +332,7 @@ export function NotificationSettings() {
|
||||
disabled={!editMode}
|
||||
className={`relative w-9 h-[18px] shrink-0 rounded-full transition-colors ${
|
||||
!editMode ? "opacity-50 cursor-not-allowed" : "cursor-pointer"
|
||||
} ${evtEnabled ? "bg-blue-600" : "bg-muted-foreground/30"}`}
|
||||
} ${evtEnabled ? "bg-blue-600" : "bg-muted-foreground/20 border border-muted-foreground/40"}`}
|
||||
onClick={() => {
|
||||
if (!editMode) return
|
||||
updateConfig(p => {
|
||||
@@ -788,7 +788,7 @@ matcher: proxmenux-pbs
|
||||
</div>
|
||||
<button
|
||||
className={`relative w-10 h-5 rounded-full transition-colors ${
|
||||
config.enabled ? "bg-blue-600" : "bg-muted-foreground/30"
|
||||
config.enabled ? "bg-blue-600" : "bg-muted-foreground/20 border border-muted-foreground/40"
|
||||
} ${!editMode ? "opacity-60 cursor-not-allowed" : "cursor-pointer"}`}
|
||||
onClick={() => editMode && updateConfig(p => ({ ...p, enabled: !p.enabled }))}
|
||||
disabled={!editMode}
|
||||
@@ -835,7 +835,7 @@ matcher: proxmenux-pbs
|
||||
<Label className="text-xs font-medium">Enable Telegram</Label>
|
||||
<button
|
||||
className={`relative w-9 h-[18px] rounded-full transition-colors ${
|
||||
config.channels.telegram?.enabled ? "bg-blue-600" : "bg-muted-foreground/30"
|
||||
config.channels.telegram?.enabled ? "bg-blue-600" : "bg-muted-foreground/20 border border-muted-foreground/40"
|
||||
} ${!editMode ? "opacity-50 cursor-not-allowed" : "cursor-pointer"}`}
|
||||
onClick={() => { if (editMode) updateChannel("telegram", "enabled", !config.channels.telegram?.enabled) }}
|
||||
disabled={!editMode}
|
||||
@@ -898,7 +898,7 @@ matcher: proxmenux-pbs
|
||||
<Label className="text-xs font-medium">Enable Gotify</Label>
|
||||
<button
|
||||
className={`relative w-9 h-[18px] rounded-full transition-colors ${
|
||||
config.channels.gotify?.enabled ? "bg-blue-600" : "bg-muted-foreground/30"
|
||||
config.channels.gotify?.enabled ? "bg-blue-600" : "bg-muted-foreground/20 border border-muted-foreground/40"
|
||||
} ${!editMode ? "opacity-50 cursor-not-allowed" : "cursor-pointer"}`}
|
||||
onClick={() => { if (editMode) updateChannel("gotify", "enabled", !config.channels.gotify?.enabled) }}
|
||||
disabled={!editMode}
|
||||
@@ -961,7 +961,7 @@ matcher: proxmenux-pbs
|
||||
<Label className="text-xs font-medium">Enable Discord</Label>
|
||||
<button
|
||||
className={`relative w-9 h-[18px] rounded-full transition-colors ${
|
||||
config.channels.discord?.enabled ? "bg-blue-600" : "bg-muted-foreground/30"
|
||||
config.channels.discord?.enabled ? "bg-blue-600" : "bg-muted-foreground/20 border border-muted-foreground/40"
|
||||
} ${!editMode ? "opacity-50 cursor-not-allowed" : "cursor-pointer"}`}
|
||||
onClick={() => { if (editMode) updateChannel("discord", "enabled", !config.channels.discord?.enabled) }}
|
||||
disabled={!editMode}
|
||||
@@ -1015,7 +1015,7 @@ matcher: proxmenux-pbs
|
||||
<Label className="text-xs font-medium">Enable Email</Label>
|
||||
<button
|
||||
className={`relative w-9 h-[18px] rounded-full transition-colors ${
|
||||
config.channels.email?.enabled ? "bg-blue-600" : "bg-muted-foreground/30"
|
||||
config.channels.email?.enabled ? "bg-blue-600" : "bg-muted-foreground/20 border border-muted-foreground/40"
|
||||
} ${!editMode ? "opacity-50 cursor-not-allowed" : "cursor-pointer"}`}
|
||||
onClick={() => { if (editMode) updateChannel("email", "enabled", !config.channels.email?.enabled) }}
|
||||
disabled={!editMode}
|
||||
@@ -1187,7 +1187,7 @@ matcher: proxmenux-pbs
|
||||
</div>
|
||||
<button
|
||||
className={`relative w-9 h-[18px] rounded-full transition-colors ${
|
||||
config.ai_enabled ? "bg-purple-600" : "bg-muted-foreground/30"
|
||||
config.ai_enabled ? "bg-purple-600" : "bg-muted-foreground/20 border border-muted-foreground/40"
|
||||
} ${!editMode ? "opacity-60 cursor-not-allowed" : "cursor-pointer"}`}
|
||||
onClick={() => editMode && updateConfig(p => ({ ...p, ai_enabled: !p.ai_enabled }))}
|
||||
disabled={!editMode}
|
||||
|
||||
@@ -1433,8 +1433,9 @@ class PollingCollector:
|
||||
self._last_update_check = 0
|
||||
# In-memory cache: error_key -> last notification timestamp
|
||||
self._last_notified: Dict[str, float] = {}
|
||||
# Track known error keys so we can detect truly new ones
|
||||
self._known_errors: set = set()
|
||||
# Track known error keys + metadata so we can detect new ones AND emit recovery
|
||||
# Dict[error_key, dict(category, severity, reason, first_seen, error_key)]
|
||||
self._known_errors: Dict[str, dict] = {}
|
||||
self._first_poll_done = False
|
||||
|
||||
def start(self):
|
||||
@@ -1492,14 +1493,20 @@ class PollingCollector:
|
||||
return
|
||||
|
||||
now = time.time()
|
||||
current_keys = set()
|
||||
current_keys: Dict[str, dict] = {}
|
||||
|
||||
for error in errors:
|
||||
error_key = error.get('error_key', '')
|
||||
if not error_key:
|
||||
continue
|
||||
|
||||
current_keys.add(error_key)
|
||||
current_keys[error_key] = {
|
||||
'category': error.get('category', ''),
|
||||
'severity': error.get('severity', 'WARNING'),
|
||||
'reason': error.get('reason', ''),
|
||||
'first_seen': error.get('first_seen', ''),
|
||||
'error_key': error_key,
|
||||
}
|
||||
category = error.get('category', '')
|
||||
severity = error.get('severity', 'WARNING')
|
||||
reason = error.get('reason', '')
|
||||
@@ -1605,9 +1612,66 @@ class PollingCollector:
|
||||
self._last_notified[error_key] = now
|
||||
self._persist_last_notified(error_key, now)
|
||||
|
||||
# Remove tracking for errors that resolved
|
||||
resolved = self._known_errors - current_keys
|
||||
for key in resolved:
|
||||
# ── Emit recovery notifications for errors that resolved ──
|
||||
resolved_keys = set(self._known_errors.keys()) - set(current_keys.keys())
|
||||
for key in resolved_keys:
|
||||
old_meta = self._known_errors.get(key, {})
|
||||
category = old_meta.get('category', '')
|
||||
reason = old_meta.get('reason', '')
|
||||
first_seen = old_meta.get('first_seen', '')
|
||||
|
||||
# Skip recovery for INFO/OK - they never triggered an alert
|
||||
if old_meta.get('severity', '') in ('INFO', 'OK'):
|
||||
self._last_notified.pop(key, None)
|
||||
continue
|
||||
|
||||
# Skip recovery on first poll (we don't know what was before)
|
||||
if not self._first_poll_done:
|
||||
self._last_notified.pop(key, None)
|
||||
continue
|
||||
|
||||
# Calculate duration
|
||||
duration = ''
|
||||
if first_seen:
|
||||
try:
|
||||
from datetime import datetime
|
||||
fs_dt = datetime.fromisoformat(first_seen)
|
||||
delta = datetime.now() - fs_dt
|
||||
total_sec = int(delta.total_seconds())
|
||||
if total_sec < 60:
|
||||
duration = f'{total_sec}s'
|
||||
elif total_sec < 3600:
|
||||
duration = f'{total_sec // 60}m'
|
||||
elif total_sec < 86400:
|
||||
h = total_sec // 3600
|
||||
m = (total_sec % 3600) // 60
|
||||
duration = f'{h}h {m}m'
|
||||
else:
|
||||
d = total_sec // 86400
|
||||
h = (total_sec % 86400) // 3600
|
||||
duration = f'{d}d {h}h'
|
||||
except Exception:
|
||||
duration = 'unknown'
|
||||
|
||||
entity, eid = self._ENTITY_MAP.get(category, ('node', ''))
|
||||
|
||||
data = {
|
||||
'hostname': self._hostname,
|
||||
'category': category,
|
||||
'reason': f'{reason} (recovered)' if reason else 'Condition resolved',
|
||||
'error_key': key,
|
||||
'severity': 'OK',
|
||||
'original_severity': old_meta.get('severity', 'WARNING'),
|
||||
'first_seen': first_seen,
|
||||
'duration': duration,
|
||||
'is_recovery': True,
|
||||
}
|
||||
|
||||
self._queue.put(NotificationEvent(
|
||||
'error_resolved', 'OK', data, source='health',
|
||||
entity=entity, entity_id=eid or key,
|
||||
))
|
||||
|
||||
self._last_notified.pop(key, None)
|
||||
|
||||
self._known_errors = current_keys
|
||||
|
||||
@@ -355,8 +355,8 @@ TEMPLATES = {
|
||||
},
|
||||
'error_resolved': {
|
||||
'title': '{hostname}: Resolved - {category}',
|
||||
'body': '{reason}\nDuration: {duration}',
|
||||
'label': 'Health issue resolved',
|
||||
'body': 'The {category} issue has been resolved.\n{reason}\nPrevious severity: {original_severity}\nDuration: {duration}',
|
||||
'label': 'Recovery notification',
|
||||
'group': 'health',
|
||||
'default_enabled': True,
|
||||
},
|
||||
@@ -739,6 +739,7 @@ TEMPLATES = {
|
||||
'label': 'Health issue resolved',
|
||||
'group': 'health',
|
||||
'default_enabled': True,
|
||||
'hidden': True, # Use error_resolved instead (avoids duplicate in UI)
|
||||
},
|
||||
|
||||
# ── Update notifications ──
|
||||
|
||||
Reference in New Issue
Block a user