"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, Code, X, Copy } 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" // GitHub Dark color palette for bash syntax highlighting const BASH_KEYWORDS = new Set([ 'if','then','else','elif','fi','for','while','until','do','done','case','esac', 'function','return','local','readonly','export','declare','typeset','unset', 'source','alias','exit','break','continue','in','select','time','trap', ]) const BASH_BUILTINS = new Set([ 'echo','printf','read','cd','pwd','ls','cat','grep','sed','awk','cut','sort','uniq','tee','wc', 'head','tail','find','xargs','chmod','chown','chgrp','mkdir','rmdir','rm','cp','mv','ln','touch', 'ps','kill','killall','pkill','pgrep','top','htop','df','du','free','uptime','uname','hostname', 'systemctl','journalctl','service','apt','apt-get','dpkg','dnf','yum','zypper','pacman', 'curl','wget','ssh','scp','rsync','tar','gzip','gunzip','bzip2','zip','unzip', 'mount','umount','lsblk','blkid','fdisk','parted','mkfs','fsck','swapon','swapoff', 'ip','ifconfig','iptables','netstat','ss','ping','traceroute','dig','nslookup','nc', 'sudo','su','whoami','id','groups','passwd','useradd','userdel','usermod','groupadd', 'test','true','false','sleep','wait','eval','exec','command','type','which','hash', 'set','getopts','shift','let','expr','jq','sed','grep','awk','tr', 'modprobe','lsmod','rmmod','insmod','dmesg','sysctl','ulimit','nohup','disown','bg','fg', 'zpool','zfs','qm','pct','pvesh','pvesm','pvenode','pveam','pveversion','vzdump', 'smartctl','nvme','ipmitool','sensors','upsc','dkms','modinfo','lspci','lsusb','lscpu', ]) function escapeHtml(s: string): string { return s.replace(/&/g, '&').replace(//g, '>') } function highlightBash(code: string): string { // Token-based highlighter — processes line by line to avoid cross-line state issues const lines = code.split('\n') const out: string[] = [] for (const line of lines) { let i = 0 let result = '' while (i < line.length) { const ch = line[i] // Comments (# to end of line, but not inside strings — simple heuristic) if (ch === '#' && (i === 0 || /\s/.test(line[i - 1]))) { result += `${escapeHtml(line.slice(i))}` i = line.length continue } // Strings: double-quoted (may contain $variables) if (ch === '"') { let j = i + 1 let content = '' while (j < line.length && line[j] !== '"') { if (line[j] === '\\' && j + 1 < line.length) { content += line[j] + line[j + 1] j += 2 } else { content += line[j] j++ } } const str = '"' + content + (line[j] === '"' ? '"' : '') // Highlight $vars inside strings const strHtml = escapeHtml(str).replace( /(\$\{[^}]+\}|\$[A-Za-z_][A-Za-z0-9_]*|\$[0-9@#?*$!-])/g, '$1' ) result += `${strHtml}` i = j + 1 continue } // Strings: single-quoted (literal, no interpolation) if (ch === "'") { let j = i + 1 while (j < line.length && line[j] !== "'") j++ const str = line.slice(i, j + 1) result += `${escapeHtml(str)}` i = j + 1 continue } // Variables outside strings if (ch === '$') { const rest = line.slice(i) let m = rest.match(/^\$\{[^}]+\}/) if (!m) m = rest.match(/^\$[A-Za-z_][A-Za-z0-9_]*/) if (!m) m = rest.match(/^\$[0-9@#?*$!-]/) if (m) { result += `${escapeHtml(m[0])}` i += m[0].length continue } } // Numbers if (/[0-9]/.test(ch) && (i === 0 || /[\s=(\[,:;+\-*/]/.test(line[i - 1]))) { const rest = line.slice(i) const m = rest.match(/^[0-9]+/) if (m) { result += `${m[0]}` i += m[0].length continue } } // Identifiers — check if keyword, builtin, or function definition if (/[A-Za-z_]/.test(ch)) { const rest = line.slice(i) const m = rest.match(/^[A-Za-z_][A-Za-z0-9_-]*/) if (m) { const word = m[0] const after = line.slice(i + word.length) if (BASH_KEYWORDS.has(word)) { result += `${word}` } else if (/^\s*\(\)\s*\{?/.test(after)) { // function definition: name() { ... } result += `${word}` } else if (BASH_BUILTINS.has(word) && (i === 0 || /[\s|;&(]/.test(line[i - 1]))) { result += `${word}` } else { result += escapeHtml(word) } i += word.length continue } } // Operators and special chars if (/[|&;<>(){}[\]=!+*\/%~^]/.test(ch)) { result += `${escapeHtml(ch)}` i++ continue } // Default: escape and append result += escapeHtml(ch) i++ } out.push(result) } return out.join('\n') } 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 version?: string has_source?: boolean deprecated?: 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) // Code viewer modal state const [codeModal, setCodeModal] = useState<{ open: boolean loading: boolean toolName: string version: string functionName: string source: string script: string error: string deprecated: boolean }>({ open: false, loading: false, toolName: '', version: '', functionName: '', source: '', script: '', error: '', deprecated: false }) const [codeCopied, setCodeCopied] = useState(false) // 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 viewToolSource = async (tool: ProxMenuxTool) => { setCodeModal({ open: true, loading: true, toolName: tool.name, version: tool.version || '1.0', functionName: '', source: '', script: '', error: '', deprecated: !!tool.deprecated }) try { const data = await fetchApi(`/api/proxmenux/tool-source/${tool.key}`) if (data.success) { setCodeModal(prev => ({ ...prev, loading: false, functionName: data.function, source: data.source, script: data.script, deprecated: !!data.deprecated })) } else { setCodeModal(prev => ({ ...prev, loading: false, error: data.error || 'Source code not available' })) } } catch { setCodeModal(prev => ({ ...prev, loading: false, error: 'Failed to load source code' })) } } const copySourceCode = async () => { const text = codeModal.source let ok = false // Preferred path (HTTPS / localhost). On plain HTTP the Promise rejects, // so we catch and fall through to the textarea fallback. try { if (navigator.clipboard && window.isSecureContext) { await navigator.clipboard.writeText(text) ok = true } } catch { // fall through } if (!ok) { try { const ta = document.createElement("textarea") ta.value = text ta.style.position = "fixed" ta.style.left = "-9999px" ta.style.top = "-9999px" ta.style.opacity = "0" ta.readOnly = true document.body.appendChild(ta) ta.focus() ta.select() ok = document.execCommand("copy") document.body.removeChild(ta) } catch { ok = false } } if (ok) { setCodeCopied(true) setTimeout(() => setCodeCopied(false), 2000) } } 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) => { const clickable = !!tool.has_source const isDeprecated = !!tool.deprecated return (
viewToolSource(tool) : undefined} className={`flex items-center justify-between gap-2 p-3 bg-muted/50 rounded-lg border border-border transition-colors ${clickable ? 'hover:bg-muted hover:border-orange-500/40 cursor-pointer' : ''}`} title={clickable ? (isDeprecated ? 'Legacy optimization — click to view source' : 'Click to view source code') : undefined} >
{tool.name} {isDeprecated && ( legacy )}
v{tool.version || '1.0'}
) })}
)} {/* Code Viewer Modal */} {codeModal.open && (
setCodeModal(prev => ({ ...prev, open: false }))}>
e.stopPropagation()} > {/* Header */}

{codeModal.toolName}

{codeModal.deprecated && ( legacy )}

{codeModal.functionName && {codeModal.functionName}()} {codeModal.script && — {codeModal.script}} {codeModal.version && v{codeModal.version}}

{codeModal.source && ( )}
{/* Body */}
{codeModal.loading ? (
) : codeModal.error ? (

{codeModal.error}

) : (
${highlightBash(codeModal.source)}` }}
                />
              )}
            
)}
) }