From 4aaba7619e69ff348ea4ac219a8865864de13617 Mon Sep 17 00:00:00 2001 From: MacRimi Date: Thu, 12 Mar 2026 18:12:04 +0100 Subject: [PATCH] Update notification service --- AppImage/components/proxmox-dashboard.tsx | 58 ++++++++++++++++++++++- AppImage/scripts/health_persistence.py | 18 +++++-- 2 files changed, 71 insertions(+), 5 deletions(-) diff --git a/AppImage/components/proxmox-dashboard.tsx b/AppImage/components/proxmox-dashboard.tsx index 6dd4495f..c2ec2ed6 100644 --- a/AppImage/components/proxmox-dashboard.tsx +++ b/AppImage/components/proxmox-dashboard.tsx @@ -85,6 +85,57 @@ export function ProxmoxDashboard() { const [showHealthModal, setShowHealthModal] = useState(false) const { showReleaseNotes, setShowReleaseNotes } = useVersionCheck() + // Category keys for health info count calculation + const HEALTH_CATEGORY_KEYS = [ + { key: "cpu", category: "temperature" }, + { key: "memory", category: "memory" }, + { key: "storage", category: "storage" }, + { key: "disks", category: "disks" }, + { key: "network", category: "network" }, + { key: "vms", category: "vms" }, + { key: "services", category: "pve_services" }, + { key: "logs", category: "logs" }, + { key: "updates", category: "updates" }, + { key: "security", category: "security" }, + ] + + // Fetch health info count independently (for initial load and refresh) + const fetchHealthInfoCount = useCallback(async () => { + try { + const response = await fetchApi("/api/health/full") + let calculatedInfoCount = 0 + + if (response && response.health?.details) { + // Get categories that have dismissed items (these become INFO) + const customCats = new Set((response.custom_suppressions || []).map((cs: { category: string }) => cs.category)) + const filteredDismissed = (response.dismissed || []).filter((item: { category: string }) => !customCats.has(item.category)) + const categoriesWithDismissed = new Set() + filteredDismissed.forEach((item: { category: string }) => { + const catMeta = HEALTH_CATEGORY_KEYS.find(c => c.category === item.category || c.key === item.category) + if (catMeta) { + categoriesWithDismissed.add(catMeta.key) + } + }) + + // Count effective INFO categories (original INFO + OK categories with dismissed) + HEALTH_CATEGORY_KEYS.forEach(({ key }) => { + const cat = response.health.details[key as keyof typeof response.health.details] + if (cat) { + const originalStatus = cat.status?.toUpperCase() + // Count as INFO if: originally INFO OR (originally OK and has dismissed items) + if (originalStatus === "INFO" || (originalStatus === "OK" && categoriesWithDismissed.has(key))) { + calculatedInfoCount++ + } + } + }) + } + + setInfoCount(calculatedInfoCount) + } catch (error) { + // Silently fail - infoCount will remain at 0 + } + }, []) + const fetchSystemData = useCallback(async () => { try { const data: FlaskSystemInfo = await fetchApi("/api/system-info") @@ -129,20 +180,25 @@ export function ProxmoxDashboard() { useEffect(() => { // Siempre fetch inicial fetchSystemData() + fetchHealthInfoCount() // Fetch info count on initial load // En overview: cada 30 segundos para actualización frecuente del estado de salud // En otras tabs: cada 60 segundos para reducir carga let interval: ReturnType | null = null + let healthInterval: ReturnType | null = null if (activeTab === "overview") { interval = setInterval(fetchSystemData, 30000) // 30 segundos + healthInterval = setInterval(fetchHealthInfoCount, 30000) // Also refresh info count } else { interval = setInterval(fetchSystemData, 60000) // 60 segundos + healthInterval = setInterval(fetchHealthInfoCount, 60000) // Also refresh info count } return () => { if (interval) clearInterval(interval) + if (healthInterval) clearInterval(healthInterval) } - }, [fetchSystemData, activeTab]) + }, [fetchSystemData, fetchHealthInfoCount, activeTab]) useEffect(() => { const handleChangeTab = (event: CustomEvent) => { diff --git a/AppImage/scripts/health_persistence.py b/AppImage/scripts/health_persistence.py index 54cdbae2..21ad0ca4 100644 --- a/AppImage/scripts/health_persistence.py +++ b/AppImage/scripts/health_persistence.py @@ -141,6 +141,10 @@ class HealthPersistence: if 'suppression_hours' not in columns: cursor.execute('ALTER TABLE errors ADD COLUMN suppression_hours INTEGER DEFAULT 24') + # Migration: add acknowledged_at column to errors if not present + if 'acknowledged_at' not in columns: + cursor.execute('ALTER TABLE errors ADD COLUMN acknowledged_at TEXT') + # Indexes for performance cursor.execute('CREATE INDEX IF NOT EXISTS idx_error_key ON errors(error_key)') cursor.execute('CREATE INDEX IF NOT EXISTS idx_category ON errors(category)') @@ -346,9 +350,10 @@ class HealthPersistence: configured_hours = int(stored) if configured_hours != self.DEFAULT_SUPPRESSION_HOURS: # Non-default setting found: auto-acknowledge + # Mark as acknowledged but DO NOT set resolved_at - error remains active cursor.execute(''' UPDATE errors - SET acknowledged = 1, resolved_at = ?, suppression_hours = ? + SET acknowledged = 1, acknowledged_at = ?, suppression_hours = ? WHERE error_key = ? AND acknowledged = 0 ''', (now, configured_hours, error_key)) @@ -514,9 +519,10 @@ class HealthPersistence: except (ValueError, TypeError): pass + # Insert as acknowledged but NOT resolved - error remains active cursor.execute(''' INSERT INTO errors (error_key, category, severity, reason, first_seen, last_seen, - occurrence_count, acknowledged, resolved_at, suppression_hours) + occurrence_count, acknowledged, acknowledged_at, suppression_hours) VALUES (?, ?, 'WARNING', 'Dismissed by user', ?, ?, 1, 1, ?, ?) ''', (error_key, category, now, now, now, sup_hours)) @@ -554,9 +560,12 @@ class HealthPersistence: except (ValueError, TypeError): pass + # Mark as acknowledged but DO NOT set resolved_at + # The error remains active until it actually disappears from the system + # resolved_at should only be set when the error is truly resolved cursor.execute(''' UPDATE errors - SET acknowledged = 1, resolved_at = ?, suppression_hours = ? + SET acknowledged = 1, acknowledged_at = ?, suppression_hours = ? WHERE error_key = ? ''', (now, sup_hours, error_key)) @@ -578,9 +587,10 @@ class HealthPersistence: if child_prefix: # Only cascade to active (unresolved) child errors. # Already-resolved/expired entries must NOT be re-surfaced. + # Mark as acknowledged but DO NOT set resolved_at cursor.execute(''' UPDATE errors - SET acknowledged = 1, resolved_at = ?, suppression_hours = ? + SET acknowledged = 1, acknowledged_at = ?, suppression_hours = ? WHERE error_key LIKE ? AND acknowledged = 0 AND resolved_at IS NULL ''', (now, sup_hours, child_prefix + '%'))