"use client" import { useState, useEffect } from "react" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/card" import { Wrench, Package, Ruler, HeartPulse, Cpu, MemoryStick, HardDrive, CircleDot, Network, Server, Settings2, FileText, RefreshCw, Shield, AlertTriangle, Info, Loader2, Check, Database, CloudOff } from "lucide-react" import { NotificationSettings } from "./notification-settings" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select" import { Switch } from "./ui/switch" import { Input } from "./ui/input" import { Badge } from "./ui/badge" import { getNetworkUnit } from "../lib/format-network" import { fetchApi } from "../lib/api-config" interface SuppressionCategory { key: string label: string category: string icon: string hours: number } const SUPPRESSION_OPTIONS = [ { value: "24", label: "24 hours" }, { value: "72", label: "3 days" }, { value: "168", label: "1 week" }, { value: "720", label: "1 month" }, { value: "8760", label: "1 year" }, { value: "custom", label: "Custom" }, { value: "-1", label: "Permanent" }, ] const CATEGORY_ICONS: Record = { cpu: Cpu, memory: MemoryStick, storage: HardDrive, disk: CircleDot, network: Network, vms: Server, services: Settings2, logs: FileText, updates: RefreshCw, security: Shield, } interface ProxMenuxTool { key: string name: string enabled: boolean } interface RemoteStorage { name: string type: string status: string total: number used: number available: number percent: number exclude_health: boolean exclude_notifications: boolean excluded_at?: string reason?: string } interface NetworkInterface { name: string type: string is_up: boolean speed: number ip_address: string | null exclude_health: boolean exclude_notifications: boolean excluded_at?: string reason?: string } export function Settings() { const [proxmenuxTools, setProxmenuxTools] = useState([]) const [loadingTools, setLoadingTools] = useState(true) const [networkUnitSettings, setNetworkUnitSettings] = useState<"Bytes" | "Bits">("Bytes") const [loadingUnitSettings, setLoadingUnitSettings] = useState(true) // Health Monitor suppression settings const [suppressionCategories, setSuppressionCategories] = useState([]) const [loadingHealth, setLoadingHealth] = useState(true) const [healthEditMode, setHealthEditMode] = useState(false) const [savingAllHealth, setSavingAllHealth] = useState(false) const [savedAllHealth, setSavedAllHealth] = useState(false) const [pendingChanges, setPendingChanges] = useState>({}) const [customValues, setCustomValues] = useState>({}) // Remote Storage Exclusions const [remoteStorages, setRemoteStorages] = useState([]) const [loadingStorages, setLoadingStorages] = useState(true) const [savingStorage, setSavingStorage] = useState(null) // Network Interface Exclusions const [networkInterfaces, setNetworkInterfaces] = useState([]) const [loadingInterfaces, setLoadingInterfaces] = useState(true) const [savingInterface, setSavingInterface] = useState(null) useEffect(() => { loadProxmenuxTools() getUnitsSettings() loadHealthSettings() loadRemoteStorages() loadNetworkInterfaces() }, []) const loadProxmenuxTools = async () => { try { const data = await fetchApi("/api/proxmenux/installed-tools") if (data.success) { setProxmenuxTools(data.installed_tools || []) } } catch (err) { console.error("Failed to load ProxMenux tools:", err) } finally { setLoadingTools(false) } } const changeNetworkUnit = (unit: string) => { const networkUnit = unit as "Bytes" | "Bits" localStorage.setItem("proxmenux-network-unit", networkUnit) setNetworkUnitSettings(networkUnit) window.dispatchEvent(new CustomEvent("networkUnitChanged", { detail: networkUnit })) window.dispatchEvent(new StorageEvent("storage", { key: "proxmenux-network-unit", newValue: networkUnit, url: window.location.href })) } const getUnitsSettings = () => { const networkUnit = getNetworkUnit() setNetworkUnitSettings(networkUnit) setLoadingUnitSettings(false) } const loadHealthSettings = async () => { try { const data = await fetchApi("/api/health/settings") if (data.categories) { setSuppressionCategories(data.categories) } } catch (err) { console.error("Failed to load health settings:", err) } finally { setLoadingHealth(false) } } const loadRemoteStorages = async () => { try { const data = await fetchApi("/api/health/remote-storages") if (data.storages) { setRemoteStorages(data.storages) } } catch (err) { console.error("Failed to load remote storages:", err) } finally { setLoadingStorages(false) } } const handleStorageExclusionChange = async (storageName: string, storageType: string, excludeHealth: boolean, excludeNotifications: boolean) => { setSavingStorage(storageName) try { // If both are false, remove the exclusion if (!excludeHealth && !excludeNotifications) { await fetchApi(`/api/health/storage-exclusions/${encodeURIComponent(storageName)}`, { method: "DELETE" }) } else { await fetchApi("/api/health/storage-exclusions", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ storage_name: storageName, storage_type: storageType, exclude_health: excludeHealth, exclude_notifications: excludeNotifications }) }) } // Update local state setRemoteStorages(prev => prev.map(s => s.name === storageName ? { ...s, exclude_health: excludeHealth, exclude_notifications: excludeNotifications } : s )) } catch (err) { console.error("Failed to update storage exclusion:", err) } finally { setSavingStorage(null) } } const loadNetworkInterfaces = async () => { try { const data = await fetchApi("/api/health/interfaces") if (data.interfaces) { setNetworkInterfaces(data.interfaces) } } catch (err) { console.error("Failed to load network interfaces:", err) } finally { setLoadingInterfaces(false) } } const handleInterfaceExclusionChange = async (interfaceName: string, interfaceType: string, excludeHealth: boolean, excludeNotifications: boolean) => { setSavingInterface(interfaceName) try { // If both are false, remove the exclusion if (!excludeHealth && !excludeNotifications) { await fetchApi(`/api/health/interface-exclusions/${encodeURIComponent(interfaceName)}`, { method: "DELETE" }) } else { await fetchApi("/api/health/interface-exclusions", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ interface_name: interfaceName, interface_type: interfaceType, exclude_health: excludeHealth, exclude_notifications: excludeNotifications }) }) } // Reload interfaces to get updated state await loadNetworkInterfaces() } catch (err) { console.error("Failed to update interface exclusion:", err) } finally { setSavingInterface(null) } } const getSelectValue = (hours: number, key: string): string => { if (hours === -1) return "-1" const preset = SUPPRESSION_OPTIONS.find(o => o.value === String(hours)) if (preset && preset.value !== "custom") return String(hours) return "custom" } const getEffectiveHours = (cat: SuppressionCategory): number => { if (cat.key in pendingChanges) return pendingChanges[cat.key] return cat.hours } const handleSuppressionChange = (settingKey: string, value: string) => { if (value === "custom") { const current = suppressionCategories.find(c => c.key === settingKey) const effectiveHours = current ? getEffectiveHours(current) : 48 setCustomValues(prev => ({ ...prev, [settingKey]: String(effectiveHours > 0 ? effectiveHours : 48) })) // Mark as custom mode in pending setPendingChanges(prev => ({ ...prev, [settingKey]: -2 })) return } const hours = parseInt(value, 10) if (isNaN(hours)) return setPendingChanges(prev => ({ ...prev, [settingKey]: hours })) // Clear custom input if switching away setCustomValues(prev => { const next = { ...prev } delete next[settingKey] return next }) } const handleCustomConfirm = (settingKey: string) => { const raw = customValues[settingKey] const hours = parseInt(raw, 10) if (isNaN(hours) || hours < 1) return setPendingChanges(prev => ({ ...prev, [settingKey]: hours })) setCustomValues(prev => { const next = { ...prev } delete next[settingKey] return next }) } const handleCancelEdit = () => { setHealthEditMode(false) setPendingChanges({}) setCustomValues({}) } const handleSaveAllHealth = async () => { // Merge pending changes into a payload: only changed categories const payload: Record = {} for (const cat of suppressionCategories) { if (cat.key in pendingChanges && pendingChanges[cat.key] !== -2) { payload[cat.key] = String(pendingChanges[cat.key]) } } if (Object.keys(payload).length === 0) { setHealthEditMode(false) setPendingChanges({}) return } setSavingAllHealth(true) try { await fetchApi("/api/health/settings", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }) // Update local state with saved values setSuppressionCategories(prev => prev.map(c => { if (c.key in pendingChanges && pendingChanges[c.key] !== -2) { return { ...c, hours: pendingChanges[c.key] } } return c }) ) setPendingChanges({}) setCustomValues({}) setHealthEditMode(false) setSavedAllHealth(true) setTimeout(() => setSavedAllHealth(false), 3000) } catch (err) { console.error("Failed to save health settings:", err) } finally { setSavingAllHealth(false) } } const hasPendingChanges = Object.keys(pendingChanges).some( k => pendingChanges[k] !== -2 ) return (

Settings

Manage your dashboard preferences

{/* Network Units Settings */}
Network Units
Change how network traffic is displayed
{loadingUnitSettings ? (
) : (
Network Unit Display
)} {/* Health Monitor Settings */}
Health Monitor
{!loadingHealth && (
{savedAllHealth && ( Saved )} {healthEditMode ? ( <> ) : ( )}
)}
Configure how long dismissed alerts stay suppressed for each category. Changes apply immediately to both existing and future dismissed alerts.
{loadingHealth ? (
) : (
{/* Header */}
Category Suppression Duration
{/* Per-category rows */}
{suppressionCategories.map((cat) => { const IconComp = CATEGORY_ICONS[cat.icon] || HeartPulse const effectiveHours = getEffectiveHours(cat) const isCustomMode = effectiveHours === -2 || (cat.key in customValues) const isPermanent = effectiveHours === -1 const isLong = effectiveHours >= 720 && effectiveHours !== -1 && effectiveHours !== -2 const hasChanged = cat.key in pendingChanges && pendingChanges[cat.key] !== cat.hours const selectVal = isCustomMode ? "custom" : getSelectValue(effectiveHours, cat.key) return (
{cat.label} {hasChanged && healthEditMode && ( )}
{isCustomMode && healthEditMode ? (
setCustomValues(prev => ({ ...prev, [cat.key]: e.target.value }))} placeholder="Hours" /> h
) : ( )}
{/* Notice for Permanent */} {isPermanent && healthEditMode && (

Alerts for {cat.label} will be permanently suppressed when dismissed. {cat.category === "temperature" && ( Critical CPU temperature alerts will still trigger for hardware safety. )}

)} {/* Notice for long duration (> 1 month) */} {isLong && healthEditMode && (

Long suppression period. Dismissed alerts for this category will not reappear for an extended time.

)}
) })}
{/* Info footer */}

These settings apply when you dismiss a warning from the Health Monitor. Critical CPU temperature alerts always trigger regardless of settings to protect your hardware.

)} {/* Remote Storage Exclusions */}
Remote Storage Exclusions
Exclude remote storages (PBS, NFS, CIFS, etc.) from health monitoring and notifications. Use this for storages that are intentionally offline or have limited API access.
{loadingStorages ? (
) : remoteStorages.length === 0 ? (

No remote storages detected

PBS, NFS, CIFS, and other remote storages will appear here when configured

) : (
{/* Header */}
Storage Health Alerts
{/* Storage rows - scrollable container */}
{remoteStorages.map((storage) => { const isExcluded = storage.exclude_health || storage.exclude_notifications const isSaving = savingStorage === storage.name const isOffline = storage.status === 'error' || storage.total === 0 return (
{storage.name} {storage.type}
{isOffline && (

Offline or unavailable

)}
{isSaving ? ( ) : ( { handleStorageExclusionChange( storage.name, storage.type, !checked, storage.exclude_notifications ) }} className="data-[state=checked]:bg-blue-600 data-[state=unchecked]:bg-input border border-border" /> )}
{isSaving ? ( ) : ( { handleStorageExclusionChange( storage.name, storage.type, storage.exclude_health, !checked ) }} className="data-[state=checked]:bg-blue-600 data-[state=unchecked]:bg-input border border-border" /> )}
) })}
{/* Info footer */}

Health: When OFF, the storage won't trigger warnings/critical alerts in the Health Monitor.
Alerts: When OFF, no notifications will be sent for this storage.

)} {/* Network Interface Exclusions */}
Network Interface Exclusions
Exclude network interfaces (bridges, bonds, physical NICs) from health monitoring and notifications. Use this for interfaces that are intentionally disabled or unused.
{loadingInterfaces ? (
) : networkInterfaces.length === 0 ? (

No network interfaces detected

) : (
{/* Header */}
Interface Health Alerts
{/* Interface rows - scrollable container */}
{networkInterfaces.map((iface) => { const isExcluded = iface.exclude_health || iface.exclude_notifications const isSaving = savingInterface === iface.name const isDown = !iface.is_up return (
{iface.name} {iface.type} {isDown && !isExcluded && ( DOWN )} {isExcluded && ( Excluded )}
{iface.ip_address || 'No IP'} {iface.speed > 0 ? `- ${iface.speed} Mbps` : ''}
{/* Health toggle */}
{isSaving ? ( ) : ( { handleInterfaceExclusionChange( iface.name, iface.type, !checked, iface.exclude_notifications ) }} className="data-[state=checked]:bg-blue-600 data-[state=unchecked]:bg-input border border-border" /> )}
{/* Notifications toggle */}
{isSaving ? ( ) : ( { handleInterfaceExclusionChange( iface.name, iface.type, iface.exclude_health, !checked ) }} className="data-[state=checked]:bg-blue-600 data-[state=unchecked]:bg-input border border-border" /> )}
) })}
{/* Info footer */}

Health: When OFF, the interface won't trigger warnings/critical alerts in the Health Monitor.
Alerts: When OFF, no notifications will be sent for this interface.

)} {/* Notification Settings */} {/* ProxMenux Optimizations */}
ProxMenux Optimizations
System optimizations and utilities installed via ProxMenux
{loadingTools ? (
) : proxmenuxTools.length === 0 ? (

No ProxMenux optimizations installed yet

Run ProxMenux to configure system optimizations

) : (
Installed Tools {proxmenuxTools.length} active
{proxmenuxTools.map((tool) => (
{tool.name}
))}
)}
) }