"use client" import type React from "react" import { useState, useEffect, useCallback } from "react" import { getAuthToken } from "@/lib/api-config" import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" import { Loader2, CheckCircle2, AlertTriangle, XCircle, Info, Activity, Cpu, MemoryStick, HardDrive, Disc, Network, Box, Settings, FileText, RefreshCw, Shield, Download, X, Clock, BellOff, ChevronRight, Settings2, HelpCircle, } from "lucide-react" import { ScriptTerminalModal } from "./script-terminal-modal" import { useT } from "@/lib/i18n/provider" 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 permanent?: boolean suppression_remaining_hours: number suppression_hours?: number resolved_at: string } interface CustomSuppression { key: string label: string category: string icon: string hours: number } 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[] custom_suppressions: CustomSuppression[] timestamp: string } interface HealthStatusModalProps { open: boolean onOpenChange: (open: boolean) => void getApiUrl: (path: string) => string } const CATEGORIES = [ { key: "cpu", category: "temperature", Icon: Cpu }, { key: "memory", category: "memory", Icon: MemoryStick }, { key: "storage", category: "storage", Icon: HardDrive }, { key: "disks", category: "disks", Icon: Disc }, { key: "network", category: "network", Icon: Network }, { key: "vms", category: "vms", Icon: Box }, { key: "services", category: "pve_services", Icon: Settings }, { key: "logs", category: "logs", Icon: FileText }, { key: "updates", category: "updates", Icon: RefreshCw }, { key: "security", category: "security", Icon: Shield }, ] export function HealthStatusModal({ open, onOpenChange, getApiUrl }: HealthStatusModalProps) { const t = useT() 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()) const [showUpdateTerminal, setShowUpdateTerminal] = useState(false) const fetchHealthDetails = useCallback(async (force = false) => { setLoading(true) setError(null) try { let newOverallStatus = "OK" // Use the new combined endpoint for fewer round-trips const token = getAuthToken() const authHeaders: Record = {} if (token) { authHeaders["Authorization"] = `Bearer ${token}` } const response = await fetch(getApiUrl(force ? "/api/health/full?refresh=1" : "/api/health/full"), { headers: authHeaders }) let infoCount = 0 if (!response.ok) { // Fallback to legacy endpoint const legacyResponse = await fetch(getApiUrl("/api/health/details"), { headers: authHeaders }) if (!legacyResponse.ok) throw new Error(t("healthStatus.errors.fetchFailed")) const data = await legacyResponse.json() setHealthData(data) setDismissedItems([]) setCustomSuppressions([]) newOverallStatus = data?.overall || "OK" // Count INFO categories from legacy data if (data?.details) { CATEGORIES.forEach(({ key }) => { const cat = data.details[key as keyof typeof data.details] if (cat && cat.status?.toUpperCase() === "INFO") { infoCount++ } }) } } else { const fullData: FullHealthData = await response.json() setHealthData(fullData.health) setDismissedItems(fullData.dismissed || []) setCustomSuppressions(fullData.custom_suppressions || []) newOverallStatus = fullData.health?.overall || "OK" // Get categories that have dismissed items (these become INFO) const customCats = new Set((fullData.custom_suppressions || []).map((cs: { category: string }) => cs.category)) const filteredDismissed = (fullData.dismissed || []).filter((item: { category: string }) => !customCats.has(item.category)) const categoriesWithDismissed = new Set() filteredDismissed.forEach((item: { category: string }) => { const catMeta = CATEGORIES.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) if (fullData.health?.details) { CATEGORIES.forEach(({ key }) => { const cat = fullData.health.details[key as keyof typeof fullData.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))) { infoCount++ } } }) } } const totalInfoCount = infoCount // Emit event with the FRESH data from the response, not the stale state const event = new CustomEvent("healthStatusUpdated", { detail: { status: newOverallStatus, infoCount: totalInfoCount }, }) window.dispatchEvent(event) } catch (err) { setError(err instanceof Error ? err.message : t("healthStatus.errors.unknown")) } finally { setLoading(false) } }, [getApiUrl, t]) // Tick counter to force re-render every 30s so "X minutes ago" stays current const [, setTick] = useState(0) useEffect(() => { if (!open) return const tickInterval = setInterval(() => setTick(t => t + 1), 30000) return () => clearInterval(tickInterval) }, [open]) useEffect(() => { if (open) { fetchHealthDetails() // Auto-refresh every 5 minutes while modal is open const refreshInterval = setInterval(() => fetchHealthDetails(), 300000) return () => clearInterval(refreshInterval) } }, [open, fetchHealthDetails]) // 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") { // Updates section: only auto-expand on WARNING+, not INFO if (key === "updates" && cat.status?.toUpperCase() === "INFO") { return } 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 case "UNKNOWN": return default: return } } const getStatusBadge = (status: string) => { const statusUpper = status?.toUpperCase() switch (statusUpper) { case "OK": return {t("healthStatus.status.ok")} case "INFO": return {t("healthStatus.status.info")} case "WARNING": return {t("healthStatus.status.warning")} case "CRITICAL": return {t("healthStatus.status.critical")} case "UNKNOWN": return {t("healthStatus.status.unknown")} default: return {t("healthStatus.status.unknown")} } } const formatStatus = (status: string) => { const key = status?.toLowerCase() return ["ok", "info", "warning", "critical", "unknown"].includes(key) ? t(`healthStatus.status.${key}`) : status } const translateHealthText = (value?: string): string => { if (!value) return "" const exact: Record = { "All systems operational": t("healthStatus.details.allOperational"), "Normal": t("healthStatus.details.normal"), "No I/O errors in dmesg": t("healthStatus.details.noIoErrors"), "Mounted read-write, space OK": t("healthStatus.details.rootFilesystemOk"), "No SMART warnings in journal": t("healthStatus.details.noSmartWarnings"), "No critical errors": t("healthStatus.details.noCriticalErrors"), "No cascading errors": t("healthStatus.details.noCascadingErrors"), "No error spikes": t("healthStatus.details.noErrorSpikes"), "No persistent patterns": t("healthStatus.details.noPersistentPatterns"), "Certificate valid": t("healthStatus.details.certificateValid"), "Cluster detected (corosync.conf present)": t("healthStatus.details.clusterDetected"), "Active": t("healthStatus.details.active"), "UP": t("healthStatus.details.up"), "Kernel/PVE up to date": t("healthStatus.details.kernelUpToDate"), "Proxmox VE is up to date": t("healthStatus.details.proxmoxUpToDate"), "No security updates pending": t("healthStatus.details.noSecurityUpdates"), "No container startup errors": t("healthStatus.details.noContainerErrors"), "No OOM events detected": t("healthStatus.details.noOomEvents"), "No QMP timeouts detected": t("healthStatus.details.noQmpTimeouts"), "No VM startup failures": t("healthStatus.details.noVmFailures"), "Dismissed by user": t("healthStatus.details.dismissedByUser"), } if (exact[value]) return exact[value] let match = value.match(/^Latency ([\d.]+)ms to gateway$/) if (match) return t("healthStatus.details.gatewayLatency", { latency: match[1] }) match = value.match(/^(\d+) failed login attempts in 24h$/) if (match) return t("healthStatus.details.failedLogins", { count: match[1] }) match = value.match(/^(\d+) IP\(s\) currently banned by Fail2Ban \(jails: (.+)\)$/) if (match) return t("healthStatus.details.fail2banBannedIps", { count: match[1], jails: match[2] }) match = value.match(/^Uptime (\d+) days?$/) if (match) return t("healthStatus.details.uptimeDays", { count: match[1] }) match = value.match(/^(\d+) package\(s\) pending$/) if (match) return t("healthStatus.details.pendingPackages", { count: match[1] }) match = value.match(/^Last updated (\d+) day\(s\) ago$/) if (match) return t("healthStatus.details.updatedDaysAgo", { count: match[1] }) match = value.match(/^(.+) storage available$/) if (match) return t("healthStatus.details.storageAvailable", { type: match[1] }) match = value.match(/^(.+) mount reachable$/) if (match) return t("healthStatus.details.mountReachable", { type: match[1] }) match = value.match(/^rootfs ([\d.]+)% used \((.+)\)$/) if (match) return t("healthStatus.details.rootfsUsed", { percent: match[1], size: match[2] }) match = value.match(/^(\d+) running CT\(s\) within safe rootfs usage$/) if (match) return t("healthStatus.details.runningCtsSafe", { count: match[1] }) match = value.match(/^(\d+) PVE block storage\(s\) within safe usage$/) if (match) return t("healthStatus.details.pveStorageSafe", { count: match[1] }) match = value.match(/^(\d+) remote mount\(s\) healthy$/) if (match) return t("healthStatus.details.remoteMountsHealthy", { count: match[1] }) return value } const formatDuration = (hours: number) => { if (hours === -1) return t("healthStatus.permanent") if (hours >= 8760) return t("healthStatus.duration.years", { count: Math.floor(hours / 8760) }) if (hours >= 720) return t("healthStatus.duration.months", { count: Math.floor(hours / 720) }) if (hours >= 168) return t("healthStatus.duration.weeks", { count: Math.floor(hours / 168) }) if (hours >= 24) return t("healthStatus.duration.days", { count: Math.floor(hours / 24) }) return t("healthStatus.duration.hours", { count: Math.round(hours) }) } // Get categories that have dismissed items (to show as INFO) const getCategoriesWithDismissed = () => { const customCats = new Set(customSuppressions.map(cs => cs.category)) const filteredDismissed = dismissedItems.filter(item => !customCats.has(item.category)) const categoriesWithDismissed = new Set() filteredDismissed.forEach(item => { // Map dismissed category to our CATEGORIES keys const catMeta = CATEGORIES.find(c => c.category === item.category || c.key === item.category) if (catMeta) { categoriesWithDismissed.add(catMeta.key) } }) return categoriesWithDismissed } const categoriesWithDismissed = getCategoriesWithDismissed() // Get effective status for a category (considers dismissed items) const getEffectiveStatus = (key: string, originalStatus: string) => { // If category has dismissed items and original status is OK, show as INFO if (categoriesWithDismissed.has(key) && originalStatus?.toUpperCase() === "OK") { return "INFO" } return originalStatus?.toUpperCase() || "UNKNOWN" } const getHealthStats = () => { if (!healthData?.details) return { total: 0, healthy: 0, info: 0, warnings: 0, critical: 0, unknown: 0 } let healthy = 0 let info = 0 let warnings = 0 let critical = 0 let unknown = 0 CATEGORIES.forEach(({ key }) => { const categoryData = healthData.details[key as keyof typeof healthData.details] if (categoryData) { const effectiveStatus = getEffectiveStatus(key, categoryData.status) if (effectiveStatus === "OK") healthy++ else if (effectiveStatus === "INFO") info++ else if (effectiveStatus === "WARNING") warnings++ else if (effectiveStatus === "CRITICAL") critical++ else if (effectiveStatus === "UNKNOWN") unknown++ } }) return { total: CATEGORIES.length, healthy, info, warnings, critical, unknown } } 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) } } // `suppressionHours` overrides the category default for this dismiss: // - undefined → backend uses the category's configured suppression // - 24, 168 (7 days) → silence for that many hours // - -1 → permanent dismiss; only revertible from // Settings → Active Suppressions const handleAcknowledge = async ( errorKey: string, suppressionHours?: number, ) => { setDismissingKey(errorKey) try { const url = getApiUrl("/api/health/acknowledge") const token = getAuthToken() const headers: Record = { "Content-Type": "application/json" } if (token) { headers["Authorization"] = `Bearer ${token}` } const body: Record = { error_key: errorKey } if (suppressionHours !== undefined) { body.suppression_hours = suppressionHours } const response = await fetch(url, { method: "POST", headers, body: JSON.stringify(body), }) const responseData = await response.json().catch(() => ({})) if (!response.ok) { throw new Error(responseData.error || `Failed to dismiss error (${response.status})`) } // Optimistically update local state to avoid slow re-fetch // Add the dismissed item to the local list immediately if (responseData.result || responseData.success) { const dismissedItem = { error_key: errorKey, category: responseData.result?.category || responseData.category || '', severity: responseData.result?.original_severity || 'WARNING', reason: 'Dismissed by user', dismissed: true, // Surface the chosen duration so the row shows the right badge // (countdown vs. "Permanent") without waiting for the refetch. permanent: suppressionHours === -1, suppression_remaining_hours: suppressionHours === -1 ? -1 : undefined, suppression_hours: suppressionHours, acknowledged_at: new Date().toISOString(), } setDismissedItems(prev => [...prev, dismissedItem]) } // Fetch fresh data in background (non-blocking) fetchHealthDetails().catch(() => {}) // Notify other mounted views (e.g. Settings → Active Suppressions // panel) that the suppression set has changed so they can refresh. try { window.dispatchEvent(new CustomEvent("health-suppression-changed")) } catch {} } 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 t("healthStatus.time.justNow") if (diffMin === 1) return t("healthStatus.time.oneMinuteAgo") if (diffMin < 60) return t("healthStatus.time.minutesAgo", { count: diffMin }) const diffHours = Math.floor(diffMin / 60) return t("healthStatus.time.hoursMinutesAgo", { hours: diffHours, minutes: diffMin % 60 }) } 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 === "UNKNOWN") return "bg-amber-500/5 border-amber-500/20 hover:bg-amber-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" if (s === "UNKNOWN") return "border-amber-400 text-amber-400 bg-amber-500/5" return "" } const formatCheckLabel = (key: string): string => { const knownKeys = new Set([ "cpu_usage", "cpu_temperature", "ram_usage", "swap_usage", "root_filesystem", "smart_health", "io_errors", "zfs_pools", "lvm_volumes", "lvm_check", "connectivity", "qmp_communication", "container_startup", "vm_startup", "oom_killer", "cluster_mode", "log_error_cascade", "log_error_spike", "log_persistent_errors", "log_critical_errors", "pve_version", "security_updates", "system_age", "pending_updates", "kernel_pve", "uptime", "certificates", "login_attempts", "fail2ban", "proxmox_storages", ]) if (knownKeys.has(key)) return t(`healthStatus.checks.${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) .filter(([, checkData]) => checkData.installed !== false) .map(([checkKey, checkData]) => { const isDismissable = checkData.dismissable === true const checkStatus = checkData.status?.toUpperCase() || "OK" return (
{getStatusIcon(checkData.dismissed ? "INFO" : checkData.status, "sm")} {formatCheckLabel(checkKey)} {translateHealthText(checkData.detail)} {checkData.dismissed && ( checkData.permanent ? ( {t("healthStatus.permanent")} ) : ( {t("healthStatus.dismissed")} ) )}
{(checkStatus === "WARNING" || checkStatus === "CRITICAL" || checkStatus === "UNKNOWN") && isDismissable && !checkData.dismissed && ( handleAcknowledge(checkData.error_key || checkKey, hours) } busy={dismissingKey === (checkData.error_key || checkKey)} t={t} /> )}
) })}
) } return (
{t("healthStatus.title")} {healthData &&
{getStatusBadge(healthData.overall)}
}
{t("healthStatus.description")} {getTimeSinceCheck() && ( {getTimeSinceCheck()} )}
{loading && (
)} {error && (

{t("healthStatus.errors.loading")}

{error}

)} {healthData && !loading && (
{/* Overall Stats Summary */}
0 ? "grid-cols-5" : "grid-cols-4"}`}>
{stats.total}
{t("healthStatus.stats.total")}
{stats.healthy}
{t("healthStatus.stats.healthy")}
{stats.info > 0 && (
{stats.info}
{t("healthStatus.stats.info")}
)}
{stats.warnings}
{t("healthStatus.stats.warning")}
{stats.critical}
{t("healthStatus.stats.critical")}
{stats.unknown > 0 && (
{stats.unknown}
{t("healthStatus.stats.unknown")}
)}
{healthData.summary && healthData.summary !== "All systems operational" && (

{translateHealthText(healthData.summary)}

)} {/* Category List */}
{CATEGORIES.map(({ key, Icon }) => { const categoryData = healthData.details[key as keyof typeof healthData.details] const originalStatus = categoryData?.status || "UNKNOWN" const status = getEffectiveStatus(key, originalStatus) const reason = translateHealthText(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)}

{t(`healthStatus.categories.${key}`)}

{hasChecks && ( ({Object.values(checks).filter(c => c.installed !== false).length}) )}
{reason && !isExpanded && (

{reason}

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

{reason}

{/* Show dismiss button for UNKNOWN status at category level when dismissable */} {status === "UNKNOWN" && categoryData?.dismissable && !hasChecks && ( handleAcknowledge(`category_${key}_unknown`, hours) } busy={dismissingKey === `category_${key}_unknown`} t={t} /> )}
)} {hasChecks ? ( renderChecks(checks, key) ) : (
{t("healthStatus.noIssues")}
)} {/* Only offer "Update Now" when the category is not already OK — hiding it when there's nothing pending prevents the operator from spawning a terminal that would only report "System is already up to date". */} {key === "updates" && status?.toUpperCase() !== "OK" && (
)}
)}
) })}
{/* Dismissed Items Section -- hide items whose category has custom suppression */} {(() => { const customCats = new Set(customSuppressions.map(cs => cs.category)) const filteredDismissed = dismissedItems.filter(item => !customCats.has(item.category)) if (filteredDismissed.length === 0) return null return (
{t("healthStatus.dismissedItems", { count: filteredDismissed.length })}
{filteredDismissed.map((item) => { const catMeta = CATEGORIES.find(c => c.category === item.category || c.key === item.category) const CatIcon = catMeta?.Icon || BellOff const catLabel = catMeta ? t(`healthStatus.categories.${catMeta.key}`) : item.category const isPermanent = item.permanent || item.suppression_remaining_hours === -1 return (

{catLabel}

{translateHealthText(item.reason)}

{isPermanent ? ( {t("healthStatus.permanent")} ) : ( {t("healthStatus.dismissed")} )} {t("healthStatus.wasStatus", { status: formatStatus(item.severity) })}

{isPermanent ? t("healthStatus.permanentlySuppressed") : t("healthStatus.suppressedForMore", { duration: formatDuration(item.suppression_remaining_hours) }) }

) })}
) })()} {/* Custom Suppression Settings Summary */} {customSuppressions.length > 0 && (
{t("healthStatus.customSuppressionSettings")}
{customSuppressions.map((cs) => { const catMeta = CATEGORIES.find(c => c.category === cs.category || c.key === cs.category) const CatIcon = catMeta?.Icon || Settings2 const durationLabel = formatDuration(cs.hours) return (
{catMeta ? t(`healthStatus.categories.${catMeta.key}`) : cs.label}
{durationLabel}
) })}

{t("healthStatus.autoSuppressedHint")}

)} {healthData.timestamp && (
{t("healthStatus.lastUpdated", { date: new Date(healthData.timestamp).toLocaleString(document.documentElement.lang) })}
)}
)}
{ setShowUpdateTerminal(false) // Force a fresh read (cache-busting via ?refresh=1) so the // "System Updates" row reflects the state right after the // update finished, instead of the pre-update cached value. fetchHealthDetails(true).catch(() => {}) }} scriptPath="/usr/local/share/proxmenux/scripts/utilities/proxmox_update.sh" scriptName="proxmox_update" params={{ EXECUTION_MODE: "web", }} title={t("healthStatus.updateTerminalTitle")} description={t("healthStatus.updateTerminalDescription")} />
) } // Small split button: the visible click opens a 3-option menu so the user // chooses how long this specific alert stays silenced. ``-1`` is the // permanent sentinel — backend stores it as `suppression_hours = -1` and // the alert can only be brought back from Settings → Active Suppressions. function DismissDropdown({ onSelect, busy, t, }: { onSelect: (suppressionHours: number) => void busy: boolean t: ReturnType }) { return ( e.stopPropagation()}> {t("healthStatus.silenceFor")} onSelect(24)} className="text-xs"> {t("healthStatus.duration.24hours")} onSelect(168)} className="text-xs"> {t("healthStatus.duration.7days")} onSelect(-1)} className="text-xs text-red-500 focus:text-red-500 focus:bg-red-500/10" > {t("healthStatus.permanently")} ) }