mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-02-18 16:36:27 +00:00
Update health monitor
This commit is contained in:
@@ -27,6 +27,7 @@ import {
|
|||||||
Clock,
|
Clock,
|
||||||
BellOff,
|
BellOff,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
|
Settings2,
|
||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
|
|
||||||
interface CategoryCheck {
|
interface CategoryCheck {
|
||||||
@@ -50,6 +51,14 @@ interface CategoryCheck {
|
|||||||
resolved_at: string
|
resolved_at: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface CustomSuppression {
|
||||||
|
key: string
|
||||||
|
label: string
|
||||||
|
category: string
|
||||||
|
icon: string
|
||||||
|
hours: number
|
||||||
|
}
|
||||||
|
|
||||||
interface HealthDetails {
|
interface HealthDetails {
|
||||||
overall: string
|
overall: string
|
||||||
summary: string
|
summary: string
|
||||||
@@ -68,12 +77,13 @@ interface HealthDetails {
|
|||||||
timestamp: string
|
timestamp: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FullHealthData {
|
interface FullHealthData {
|
||||||
health: HealthDetails
|
health: HealthDetails
|
||||||
active_errors: any[]
|
active_errors: any[]
|
||||||
dismissed: DismissedError[]
|
dismissed: DismissedError[]
|
||||||
|
custom_suppressions: CustomSuppression[]
|
||||||
timestamp: string
|
timestamp: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface HealthStatusModalProps {
|
interface HealthStatusModalProps {
|
||||||
open: boolean
|
open: boolean
|
||||||
@@ -82,22 +92,23 @@ interface HealthStatusModalProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const CATEGORIES = [
|
const CATEGORIES = [
|
||||||
{ key: "cpu", label: "CPU Usage & Temperature", Icon: Cpu },
|
{ key: "cpu", category: "temperature", label: "CPU Usage & Temperature", Icon: Cpu },
|
||||||
{ key: "memory", label: "Memory & Swap", Icon: MemoryStick },
|
{ key: "memory", category: "memory", label: "Memory & Swap", Icon: MemoryStick },
|
||||||
{ key: "storage", label: "Storage Mounts & Space", Icon: HardDrive },
|
{ key: "storage", category: "storage", label: "Storage Mounts & Space", Icon: HardDrive },
|
||||||
{ key: "disks", label: "Disk I/O & Errors", Icon: Disc },
|
{ key: "disks", category: "disks", label: "Disk I/O & Errors", Icon: Disc },
|
||||||
{ key: "network", label: "Network Interfaces", Icon: Network },
|
{ key: "network", category: "network", label: "Network Interfaces", Icon: Network },
|
||||||
{ key: "vms", label: "VMs & Containers", Icon: Box },
|
{ key: "vms", category: "vms", label: "VMs & Containers", Icon: Box },
|
||||||
{ key: "services", label: "PVE Services", Icon: Settings },
|
{ key: "services", category: "pve_services", label: "PVE Services", Icon: Settings },
|
||||||
{ key: "logs", label: "System Logs", Icon: FileText },
|
{ key: "logs", category: "logs", label: "System Logs", Icon: FileText },
|
||||||
{ key: "updates", label: "System Updates", Icon: RefreshCw },
|
{ key: "updates", category: "updates", label: "System Updates", Icon: RefreshCw },
|
||||||
{ key: "security", label: "Security & Certificates", Icon: Shield },
|
{ key: "security", category: "security", label: "Security & Certificates", Icon: Shield },
|
||||||
]
|
]
|
||||||
|
|
||||||
export function HealthStatusModal({ open, onOpenChange, getApiUrl }: HealthStatusModalProps) {
|
export function HealthStatusModal({ open, onOpenChange, getApiUrl }: HealthStatusModalProps) {
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [healthData, setHealthData] = useState<HealthDetails | null>(null)
|
const [healthData, setHealthData] = useState<HealthDetails | null>(null)
|
||||||
const [dismissedItems, setDismissedItems] = useState<DismissedError[]>([])
|
const [dismissedItems, setDismissedItems] = useState<DismissedError[]>([])
|
||||||
|
const [customSuppressions, setCustomSuppressions] = useState<CustomSuppression[]>([])
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
const [dismissingKey, setDismissingKey] = useState<string | null>(null)
|
const [dismissingKey, setDismissingKey] = useState<string | null>(null)
|
||||||
const [expandedCategories, setExpandedCategories] = useState<Set<string>>(new Set())
|
const [expandedCategories, setExpandedCategories] = useState<Set<string>>(new Set())
|
||||||
@@ -118,11 +129,13 @@ export function HealthStatusModal({ open, onOpenChange, getApiUrl }: HealthStatu
|
|||||||
const data = await legacyResponse.json()
|
const data = await legacyResponse.json()
|
||||||
setHealthData(data)
|
setHealthData(data)
|
||||||
setDismissedItems([])
|
setDismissedItems([])
|
||||||
|
setCustomSuppressions([])
|
||||||
newOverallStatus = data?.overall || "OK"
|
newOverallStatus = data?.overall || "OK"
|
||||||
} else {
|
} else {
|
||||||
const fullData: FullHealthData = await response.json()
|
const fullData: FullHealthData = await response.json()
|
||||||
setHealthData(fullData.health)
|
setHealthData(fullData.health)
|
||||||
setDismissedItems(fullData.dismissed || [])
|
setDismissedItems(fullData.dismissed || [])
|
||||||
|
setCustomSuppressions(fullData.custom_suppressions || [])
|
||||||
newOverallStatus = fullData.health?.overall || "OK"
|
newOverallStatus = fullData.health?.overall || "OK"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -617,6 +630,50 @@ export function HealthStatusModal({ open, onOpenChange, getApiUrl }: HealthStatu
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Custom Suppression Settings Summary */}
|
||||||
|
{customSuppressions.length > 0 && (
|
||||||
|
<div className="space-y-2 pt-2">
|
||||||
|
<div className="flex items-center gap-2 text-xs sm:text-sm font-medium text-muted-foreground">
|
||||||
|
<Settings2 className="h-3.5 w-3.5 sm:h-4 sm:w-4" />
|
||||||
|
Custom Suppression Settings
|
||||||
|
</div>
|
||||||
|
<div className="rounded-lg border border-blue-500/20 bg-blue-500/5 p-2.5 sm:p-3">
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
{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 (
|
||||||
|
<div key={cs.key} className="flex items-center justify-between gap-2">
|
||||||
|
<div className="flex items-center gap-2 min-w-0">
|
||||||
|
<CatIcon className="h-3 w-3 sm:h-3.5 sm:w-3.5 text-blue-400/70 shrink-0" />
|
||||||
|
<span className="text-[11px] sm:text-xs text-blue-400/80 truncate">{cs.label}</span>
|
||||||
|
</div>
|
||||||
|
<Badge variant="outline" className="text-[9px] sm:text-[10px] border-blue-500/30 text-blue-400/80 bg-transparent shrink-0">
|
||||||
|
{durationLabel}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<p className="text-[10px] text-muted-foreground/60 mt-2 pt-1.5 border-t border-blue-500/10">
|
||||||
|
Alerts in these categories are auto-suppressed when detected.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{healthData.timestamp && (
|
{healthData.timestamp && (
|
||||||
<div className="text-xs text-muted-foreground text-center pt-2">
|
<div className="text-xs text-muted-foreground text-center pt-2">
|
||||||
Last updated: {new Date(healthData.timestamp).toLocaleString()}
|
Last updated: {new Date(healthData.timestamp).toLocaleString()}
|
||||||
|
|||||||
@@ -155,11 +155,13 @@ def get_full_health():
|
|||||||
details = health_monitor.get_detailed_status()
|
details = health_monitor.get_detailed_status()
|
||||||
active_errors = health_persistence.get_active_errors()
|
active_errors = health_persistence.get_active_errors()
|
||||||
dismissed = health_persistence.get_dismissed_errors()
|
dismissed = health_persistence.get_dismissed_errors()
|
||||||
|
custom_suppressions = health_persistence.get_custom_suppressions()
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'health': details,
|
'health': details,
|
||||||
'active_errors': active_errors,
|
'active_errors': active_errors,
|
||||||
'dismissed': dismissed,
|
'dismissed': dismissed,
|
||||||
|
'custom_suppressions': custom_suppressions,
|
||||||
'timestamp': details.get('timestamp')
|
'timestamp': details.get('timestamp')
|
||||||
})
|
})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -221,6 +221,36 @@ class HealthPersistence:
|
|||||||
event_info['type'] = 'new'
|
event_info['type'] = 'new'
|
||||||
event_info['needs_notification'] = True
|
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
|
# Record event
|
||||||
self._record_event(cursor, event_info['type'], error_key,
|
self._record_event(cursor, event_info['type'], error_key,
|
||||||
{'severity': severity, 'reason': reason})
|
{'severity': severity, 'reason': reason})
|
||||||
@@ -844,6 +874,14 @@ class HealthPersistence:
|
|||||||
})
|
})
|
||||||
|
|
||||||
return result
|
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
|
# Global instance
|
||||||
|
|||||||
Reference in New Issue
Block a user