"use client" import type React from "react" import { useState, useEffect, useCallback } from "react" import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" import { Loader2, CheckCircle2, AlertTriangle, XCircle, Info, Activity, Cpu, MemoryStick, HardDrive, Disc, Network, Box, Settings, FileText, RefreshCw, Shield, X, Clock, BellOff, ChevronRight, } from "lucide-react" interface CategoryCheck { status: string reason?: string details?: any checks?: Record dismissable?: boolean [key: string]: any } interface DismissedError { error_key: string category: string severity: string reason: string dismissed: boolean suppression_remaining_hours: number resolved_at: string } interface HealthDetails { overall: string summary: string details: { cpu: CategoryCheck memory: CategoryCheck storage: CategoryCheck disks: CategoryCheck network: CategoryCheck vms: CategoryCheck services: CategoryCheck logs: CategoryCheck updates: CategoryCheck security: CategoryCheck } timestamp: string } interface FullHealthData { health: HealthDetails active_errors: any[] dismissed: DismissedError[] timestamp: string } interface HealthStatusModalProps { open: boolean onOpenChange: (open: boolean) => void getApiUrl: (path: string) => string } 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 }, ] export function HealthStatusModal({ open, onOpenChange, getApiUrl }: HealthStatusModalProps) { const [loading, setLoading] = useState(true) const [healthData, setHealthData] = useState(null) const [dismissedItems, setDismissedItems] = useState([]) const [error, setError] = useState(null) const [dismissingKey, setDismissingKey] = useState(null) const [expandedCategories, setExpandedCategories] = useState>(new Set()) const fetchHealthDetails = useCallback(async () => { setLoading(true) setError(null) try { // Use the new combined endpoint for fewer round-trips const response = await fetch(getApiUrl("/api/health/full")) if (!response.ok) { // Fallback to legacy endpoint const legacyResponse = await fetch(getApiUrl("/api/health/details")) if (!legacyResponse.ok) throw new Error("Failed to fetch health details") const data = await legacyResponse.json() setHealthData(data) setDismissedItems([]) } else { const fullData: FullHealthData = await response.json() setHealthData(fullData.health) setDismissedItems(fullData.dismissed || []) } const event = new CustomEvent("healthStatusUpdated", { detail: { status: healthData?.overall || "OK" }, }) window.dispatchEvent(event) } catch (err) { setError(err instanceof Error ? err.message : "Unknown error") } finally { setLoading(false) } }, [getApiUrl, healthData?.overall]) useEffect(() => { if (open) { fetchHealthDetails() } }, [open]) // Auto-expand non-OK categories when data loads useEffect(() => { if (healthData?.details) { const nonOkCategories = new Set() CATEGORIES.forEach(({ key }) => { const cat = healthData.details[key as keyof typeof healthData.details] if (cat && cat.status?.toUpperCase() !== "OK") { nonOkCategories.add(key) } }) setExpandedCategories(nonOkCategories) } }, [healthData]) const toggleCategory = (key: string) => { setExpandedCategories(prev => { const next = new Set(prev) if (next.has(key)) { next.delete(key) } else { next.add(key) } return next }) } const getStatusIcon = (status: string, size: "sm" | "md" = "md") => { const statusUpper = status?.toUpperCase() const cls = size === "sm" ? "h-4 w-4" : "h-5 w-5" switch (statusUpper) { case "OK": return case "INFO": return case "WARNING": return case "CRITICAL": return default: return } } const getStatusBadge = (status: string) => { const statusUpper = status?.toUpperCase() switch (statusUpper) { case "OK": return OK case "INFO": return Info case "WARNING": return Warning case "CRITICAL": return Critical default: return Unknown } } const getHealthStats = () => { if (!healthData?.details) { return { total: 0, healthy: 0, info: 0, warnings: 0, critical: 0 } } let healthy = 0 let info = 0 let warnings = 0 let critical = 0 CATEGORIES.forEach(({ key }) => { const categoryData = healthData.details[key as keyof typeof healthData.details] if (categoryData) { const status = categoryData.status?.toUpperCase() if (status === "OK") healthy++ else if (status === "INFO") info++ else if (status === "WARNING") warnings++ else if (status === "CRITICAL") critical++ } }) return { total: CATEGORIES.length, healthy, info, warnings, critical } } const stats = getHealthStats() const handleCategoryClick = (categoryKey: string, status: string) => { if (status === "OK" || status === "INFO") return onOpenChange(false) const categoryToTab: Record = { storage: "storage", disks: "storage", network: "network", vms: "vms", logs: "logs", hardware: "hardware", services: "hardware", } const targetTab = categoryToTab[categoryKey] if (targetTab) { const event = new CustomEvent("changeTab", { detail: { tab: targetTab } }) window.dispatchEvent(event) } } const handleAcknowledge = async (errorKey: string, e: React.MouseEvent) => { e.stopPropagation() setDismissingKey(errorKey) try { const response = await fetch(getApiUrl("/api/health/acknowledge"), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ error_key: errorKey }), }) if (!response.ok) { const errorData = await response.json() throw new Error(errorData.error || "Failed to dismiss error") } await fetchHealthDetails() } catch (err) { console.error("Error dismissing:", err) } finally { setDismissingKey(null) } } const getTimeSinceCheck = () => { if (!healthData?.timestamp) return null const checkTime = new Date(healthData.timestamp) const now = new Date() const diffMs = now.getTime() - checkTime.getTime() const diffMin = Math.floor(diffMs / 60000) if (diffMin < 1) return "just now" if (diffMin === 1) return "1 minute ago" if (diffMin < 60) return `${diffMin} minutes ago` const diffHours = Math.floor(diffMin / 60) return `${diffHours}h ${diffMin % 60}m ago` } const getCategoryRowStyle = (status: string) => { const s = status?.toUpperCase() if (s === "CRITICAL") return "bg-red-500/5 border-red-500/20 hover:bg-red-500/10 cursor-pointer" if (s === "WARNING") return "bg-yellow-500/5 border-yellow-500/20 hover:bg-yellow-500/10 cursor-pointer" if (s === "INFO") return "bg-blue-500/5 border-blue-500/20 hover:bg-blue-500/10" return "bg-card border-border hover:bg-muted/30" } const getOutlineBadgeStyle = (status: string) => { const s = status?.toUpperCase() if (s === "OK") return "border-green-500 text-green-500 bg-transparent" if (s === "INFO") return "border-blue-500 text-blue-500 bg-blue-500/5" if (s === "WARNING") return "border-yellow-500 text-yellow-500 bg-yellow-500/5" if (s === "CRITICAL") return "border-red-500 text-red-500 bg-red-500/5" return "" } const formatCheckLabel = (key: string): string => { const labels: Record = { cpu_usage: "CPU Usage", cpu_temperature: "Temperature", ram_usage: "RAM Usage", swap_usage: "Swap Usage", root_filesystem: "Root Filesystem", lvm_check: "LVM Status", connectivity: "Connectivity", all_vms_cts: "VMs & Containers", cluster_mode: "Cluster Mode", error_cascade: "Error Cascade", error_spike: "Error Spike", persistent_errors: "Persistent Errors", critical_errors: "Critical Errors", security_updates: "Security Updates", system_age: "System Age", pending_updates: "Pending Updates", kernel_pve: "Kernel / PVE", uptime: "Uptime", certificates: "Certificates", login_attempts: "Login Attempts", fail2ban: "Fail2Ban", } if (labels[key]) return labels[key] // Convert snake_case or camelCase to Title Case return key .replace(/_/g, " ") .replace(/([a-z])([A-Z])/g, "$1 $2") .replace(/\b\w/g, (c) => c.toUpperCase()) } const renderChecks = ( checks: Record, categoryKey: string ) => { if (!checks || Object.keys(checks).length === 0) return null return (
{Object.entries(checks).map(([checkKey, checkData]) => { const isDismissable = checkData.dismissable === true const checkStatus = checkData.status?.toUpperCase() || "OK" return (
{getStatusIcon(checkData.status, "sm")} {formatCheckLabel(checkKey)} {checkData.detail}
{checkData.thresholds && ( ({checkData.thresholds}) )} {(checkStatus === "WARNING" || checkStatus === "CRITICAL") && isDismissable && ( )}
) })}
) } return (
System Health Status {healthData &&
{getStatusBadge(healthData.overall)}
}
Detailed health checks for all system components {getTimeSinceCheck() && ( Last check: {getTimeSinceCheck()} )}
{loading && (
)} {error && (

Error loading health status

{error}

)} {healthData && !loading && (
{/* Overall Stats Summary */}
0 ? "grid-cols-5" : "grid-cols-4"}`}>
{stats.total}
Total
{stats.healthy}
Healthy
{stats.info > 0 && (
{stats.info}
Info
)}
{stats.warnings}
Warnings
{stats.critical}
Critical
{healthData.summary && healthData.summary !== "All systems operational" && (
{healthData.summary}
)} {/* Category List */}
{CATEGORIES.map(({ key, label, Icon }) => { const categoryData = healthData.details[key as keyof typeof healthData.details] const status = categoryData?.status || "UNKNOWN" const reason = categoryData?.reason const checks = categoryData?.checks const isExpanded = expandedCategories.has(key) const hasChecks = checks && Object.keys(checks).length > 0 return (
{/* Clickable header row */}
toggleCategory(key)} >
{getStatusIcon(status)}

{label}

{hasChecks && ( ({Object.keys(checks).length} checks) )}
{reason && !isExpanded && (

{reason}

)}
{status}
{/* Expandable checks section */} {isExpanded && (
{reason && (

{reason}

)} {hasChecks ? ( renderChecks(checks, key) ) : (
No issues detected
)}
)}
) })}
{/* Dismissed Items Section */} {dismissedItems.length > 0 && (
Dismissed Items ({dismissedItems.length})
{dismissedItems.map((item) => (
{getStatusIcon("INFO")}

{item.reason}

Dismissed was {item.severity}

Suppressed for {item.suppression_remaining_hours < 24 ? `${Math.round(item.suppression_remaining_hours)}h` : `${Math.round(item.suppression_remaining_hours / 24)} days` } more

))}
)} {healthData.timestamp && (
Last updated: {new Date(healthData.timestamp).toLocaleString()}
)}
)}
) }