From 99605b6a5548547e2802c948c8f2b2ba2cd18314 Mon Sep 17 00:00:00 2001 From: MacRimi Date: Tue, 17 Feb 2026 17:09:00 +0100 Subject: [PATCH] Update health monitor --- AppImage/components/health-status-modal.tsx | 81 ++++++++++++++++++--- AppImage/scripts/flask_health_routes.py | 2 + AppImage/scripts/health_persistence.py | 38 ++++++++++ 3 files changed, 109 insertions(+), 12 deletions(-) diff --git a/AppImage/components/health-status-modal.tsx b/AppImage/components/health-status-modal.tsx index 783f1d6d..0fa3dd46 100644 --- a/AppImage/components/health-status-modal.tsx +++ b/AppImage/components/health-status-modal.tsx @@ -27,6 +27,7 @@ import { Clock, BellOff, ChevronRight, + Settings2, } from "lucide-react" interface CategoryCheck { @@ -50,6 +51,14 @@ interface CategoryCheck { resolved_at: string } + interface CustomSuppression { + key: string + label: string + category: string + icon: string + hours: number + } + interface HealthDetails { overall: string summary: string @@ -68,12 +77,13 @@ interface HealthDetails { timestamp: string } -interface FullHealthData { + interface FullHealthData { health: HealthDetails active_errors: any[] dismissed: DismissedError[] + custom_suppressions: CustomSuppression[] timestamp: string -} + } interface HealthStatusModalProps { open: boolean @@ -82,22 +92,23 @@ interface HealthStatusModalProps { } const CATEGORIES = [ - { key: "cpu", label: "CPU Usage & Temperature", Icon: Cpu }, - { key: "memory", label: "Memory & Swap", Icon: MemoryStick }, - { key: "storage", label: "Storage Mounts & Space", Icon: HardDrive }, - { key: "disks", label: "Disk I/O & Errors", Icon: Disc }, - { key: "network", label: "Network Interfaces", Icon: Network }, - { key: "vms", label: "VMs & Containers", Icon: Box }, - { key: "services", label: "PVE Services", Icon: Settings }, - { key: "logs", label: "System Logs", Icon: FileText }, - { key: "updates", label: "System Updates", Icon: RefreshCw }, - { key: "security", label: "Security & Certificates", Icon: Shield }, + { key: "cpu", category: "temperature", label: "CPU Usage & Temperature", Icon: Cpu }, + { key: "memory", category: "memory", label: "Memory & Swap", Icon: MemoryStick }, + { key: "storage", category: "storage", label: "Storage Mounts & Space", Icon: HardDrive }, + { key: "disks", category: "disks", label: "Disk I/O & Errors", Icon: Disc }, + { key: "network", category: "network", label: "Network Interfaces", Icon: Network }, + { key: "vms", category: "vms", label: "VMs & Containers", Icon: Box }, + { key: "services", category: "pve_services", label: "PVE Services", Icon: Settings }, + { key: "logs", category: "logs", label: "System Logs", Icon: FileText }, + { key: "updates", category: "updates", label: "System Updates", Icon: RefreshCw }, + { key: "security", category: "security", label: "Security & Certificates", Icon: Shield }, ] export function HealthStatusModal({ open, onOpenChange, getApiUrl }: HealthStatusModalProps) { const [loading, setLoading] = useState(true) const [healthData, setHealthData] = useState(null) const [dismissedItems, setDismissedItems] = useState([]) + const [customSuppressions, setCustomSuppressions] = useState([]) const [error, setError] = useState(null) const [dismissingKey, setDismissingKey] = useState(null) const [expandedCategories, setExpandedCategories] = useState>(new Set()) @@ -118,11 +129,13 @@ export function HealthStatusModal({ open, onOpenChange, getApiUrl }: HealthStatu const data = await legacyResponse.json() setHealthData(data) setDismissedItems([]) + setCustomSuppressions([]) newOverallStatus = data?.overall || "OK" } else { const fullData: FullHealthData = await response.json() setHealthData(fullData.health) setDismissedItems(fullData.dismissed || []) + setCustomSuppressions(fullData.custom_suppressions || []) newOverallStatus = fullData.health?.overall || "OK" } @@ -617,6 +630,50 @@ export function HealthStatusModal({ open, onOpenChange, getApiUrl }: HealthStatu )} + {/* Custom Suppression Settings Summary */} + {customSuppressions.length > 0 && ( +
+
+ + Custom Suppression Settings +
+
+
+ {customSuppressions.map((cs) => { + const catMeta = CATEGORIES.find(c => c.category === cs.category || c.key === cs.category || c.label === cs.label) + const CatIcon = catMeta?.Icon || Settings2 + const durationLabel = cs.hours === -1 + ? "Permanent" + : cs.hours >= 8760 + ? `${Math.floor(cs.hours / 8760)} year(s)` + : cs.hours >= 720 + ? `${Math.floor(cs.hours / 720)} month(s)` + : cs.hours >= 168 + ? `${Math.floor(cs.hours / 168)} week(s)` + : cs.hours >= 72 + ? `${Math.floor(cs.hours / 24)} days` + : `${cs.hours}h` + + return ( +
+
+ + {cs.label} +
+ + {durationLabel} + +
+ ) + })} +
+

+ Alerts in these categories are auto-suppressed when detected. +

+
+
+ )} + {healthData.timestamp && (
Last updated: {new Date(healthData.timestamp).toLocaleString()} diff --git a/AppImage/scripts/flask_health_routes.py b/AppImage/scripts/flask_health_routes.py index f5b9ebc5..14c5df26 100644 --- a/AppImage/scripts/flask_health_routes.py +++ b/AppImage/scripts/flask_health_routes.py @@ -155,11 +155,13 @@ def get_full_health(): details = health_monitor.get_detailed_status() active_errors = health_persistence.get_active_errors() dismissed = health_persistence.get_dismissed_errors() + custom_suppressions = health_persistence.get_custom_suppressions() return jsonify({ 'health': details, 'active_errors': active_errors, 'dismissed': dismissed, + 'custom_suppressions': custom_suppressions, 'timestamp': details.get('timestamp') }) except Exception as e: diff --git a/AppImage/scripts/health_persistence.py b/AppImage/scripts/health_persistence.py index d2473adc..ea6179b1 100644 --- a/AppImage/scripts/health_persistence.py +++ b/AppImage/scripts/health_persistence.py @@ -221,6 +221,36 @@ class HealthPersistence: event_info['type'] = 'new' event_info['needs_notification'] = True + # ─── Auto-suppress: if the category has a non-default setting, ─── + # auto-dismiss immediately so the user never sees it as active. + # Exception: CRITICAL CPU temperature is never auto-suppressed. + if not (error_key == 'cpu_temperature' and severity == 'CRITICAL'): + setting_key = self.CATEGORY_SETTING_MAP.get(category, '') + if setting_key: + stored = self.get_setting(setting_key) + if stored is not None: + configured_hours = int(stored) + if configured_hours != self.DEFAULT_SUPPRESSION_HOURS: + # Non-default setting found: auto-acknowledge + cursor.execute(''' + UPDATE errors + SET acknowledged = 1, resolved_at = ?, suppression_hours = ? + WHERE error_key = ? AND acknowledged = 0 + ''', (now, configured_hours, error_key)) + + if cursor.rowcount > 0: + self._record_event(cursor, 'auto_suppressed', error_key, { + 'severity': severity, + 'reason': reason, + 'suppression_hours': configured_hours, + 'note': 'Auto-suppressed by user settings' + }) + event_info['type'] = 'auto_suppressed' + event_info['needs_notification'] = False + conn.commit() + conn.close() + return event_info + # Record event self._record_event(cursor, event_info['type'], error_key, {'severity': severity, 'reason': reason}) @@ -844,6 +874,14 @@ class HealthPersistence: }) return result + + def get_custom_suppressions(self) -> List[Dict[str, Any]]: + """ + Get only categories with non-default suppression settings. + Used by the health modal to show a summary of custom suppressions. + """ + all_cats = self.get_suppression_categories() + return [c for c in all_cats if c['hours'] != self.DEFAULT_SUPPRESSION_HOURS] # Global instance