import { useEffect, useState, useMemo, useRef } from 'react' import api from '../api/client' import StatusBadge from '../components/StatusBadge' import ProxmoxPanel from '../components/ProxmoxPanel' import { Search, ChevronUp, ChevronDown, Plus, X, Settings2 } from 'lucide-react' import InstallGuide from '../components/InstallGuide' const ALL_COLUMNS = [ { id: 'customer', label: 'Kunde', default: true }, { id: 'name', label: 'Name', default: true, locked: true }, { id: 'hostname', label: 'Hostname', default: false }, { id: 'version', label: 'PVE Version', default: true }, { id: 'ip', label: 'IP', default: true }, { id: 'uptime', label: 'Uptime', default: true }, { id: 'cpu', label: 'CPU', default: true }, { id: 'ram', label: 'RAM', default: true }, { id: 'disk', label: 'Disk', default: true }, { id: 'vms', label: 'VMs', default: true }, { id: 'containers', label: 'Container', default: true }, { id: 'lastresponse', label: 'Letzter Kontakt', default: false }, { id: 'agentversion', label: 'Agent Version', default: false }, ] const STORAGE_KEY = 'rmm-proxmox-columns' function loadColumns() { try { const saved = localStorage.getItem(STORAGE_KEY) if (saved) return JSON.parse(saved) } catch {} return ALL_COLUMNS.filter(c => c.default).map(c => c.id) } function saveColumns(cols) { localStorage.setItem(STORAGE_KEY, JSON.stringify(cols)) } export default function ProxmoxServers() { const [agents, setAgents] = useState([]) const [customers, setCustomers] = useState([]) const [search, setSearch] = useState('') const [loading, setLoading] = useState(true) const [sortKey, setSortKey] = useState('customer') const [sortDir, setSortDir] = useState('asc') const [selectedId, setSelectedId] = useState(null) const [showAddDialog, setShowAddDialog] = useState(false) const [addResult, setAddResult] = useState(null) const [visibleCols, setVisibleCols] = useState(loadColumns) const [showColMenu, setShowColMenu] = useState(false) const colMenuRef = useRef(null) const reload = () => { Promise.all([api.getAgents(), api.getCustomers()]) .then(([a, c]) => { setAgents(a || []) setCustomers(c || []) }) .finally(() => setLoading(false)) } useEffect(() => { reload() const iv = setInterval(reload, 30000) return () => clearInterval(iv) }, []) const customerMap = useMemo(() => { const m = {} customers.forEach((c) => (m[c.id] = c)) return m }, [customers]) // Enrich agents with system data summaries const [agentDetails, setAgentDetails] = useState({}) useEffect(() => { agents.forEach((a) => { if (!agentDetails[a.id]) { api.getAgent(a.id).then((d) => { setAgentDetails((prev) => ({ ...prev, [a.id]: d })) }) } }) }, [agents]) const toggleSort = (key) => { if (sortKey === key) { setSortDir(sortDir === 'asc' ? 'desc' : 'asc') } else { setSortKey(key) setSortDir('asc') } } // Close column menu on click outside useEffect(() => { const handler = (e) => { if (colMenuRef.current && !colMenuRef.current.contains(e.target)) setShowColMenu(false) } document.addEventListener('mousedown', handler) return () => document.removeEventListener('mousedown', handler) }, []) const toggleCol = (id) => { const col = ALL_COLUMNS.find(c => c.id === id) if (col?.locked) return const next = visibleCols.includes(id) ? visibleCols.filter(c => c !== id) : [...visibleCols, id] setVisibleCols(next) saveColumns(next) } const isColVisible = (id) => visibleCols.includes(id) const enriched = useMemo(() => { return agents.map((a) => { const detail = agentDetails[a.id] const sys = detail?.system_data const proxmox = sys?.proxmox const cust = a.customer_id ? customerMap[a.customer_id] : null // Nur Proxmox-Agents anzeigen if (!proxmox?.available) return null const rootDisk = sys?.disks?.find((d) => d.mount_point === '/') const diskPct = rootDisk && rootDisk.total_bytes > 0 ? (rootDisk.used_bytes / rootDisk.total_bytes * 100) : null const ramPct = sys?.memory?.total_bytes > 0 ? (sys.memory.used_bytes / sys.memory.total_bytes * 100) : null const vmsRunning = proxmox.vms?.filter(vm => vm.status === 'running').length || 0 const vmsTotal = proxmox.vms?.length || 0 const ctRunning = proxmox.containers?.filter(ct => ct.status === 'running').length || 0 const ctTotal = proxmox.containers?.length || 0 return { ...a, customer: cust, customerName: cust?.name || '', customerNumber: cust?.number || '', cpuPct: sys?.cpu?.usage_percent ?? null, ramPct, diskPct, uptime: sys?.uptime_seconds || 0, pveVersion: proxmox.version || '', vms: `${vmsRunning}/${vmsTotal}`, vmsRunning, vmsTotal, containers: `${ctRunning}/${ctTotal}`, ctRunning, ctTotal, sys, proxmox, } }).filter(Boolean) // Entferne null Werte }, [agents, agentDetails, customerMap]) const filtered = useMemo(() => { let list = enriched if (search) { const q = search.toLowerCase() list = list.filter((a) => a.name.toLowerCase().includes(q) || a.ip.toLowerCase().includes(q) || a.hostname.toLowerCase().includes(q) || a.customerName.toLowerCase().includes(q) || a.customerNumber.toLowerCase().includes(q) ) } list.sort((a, b) => { let va, vb switch (sortKey) { case 'customer': va = a.customerNumber; vb = b.customerNumber; break case 'name': va = a.name; vb = b.name; break case 'ip': va = a.ip; vb = b.ip; break case 'version': va = a.pveVersion; vb = b.pveVersion; break case 'uptime': return sortDir === 'asc' ? a.uptime - b.uptime : b.uptime - a.uptime case 'cpu': return sortDir === 'asc' ? (a.cpuPct ?? 999) - (b.cpuPct ?? 999) : (b.cpuPct ?? -1) - (a.cpuPct ?? -1) case 'ram': return sortDir === 'asc' ? (a.ramPct ?? 999) - (b.ramPct ?? 999) : (b.ramPct ?? -1) - (a.ramPct ?? -1) case 'disk': return sortDir === 'asc' ? (a.diskPct ?? 999) - (b.diskPct ?? 999) : (b.diskPct ?? -1) - (a.diskPct ?? -1) case 'vms': return sortDir === 'asc' ? a.vmsTotal - b.vmsTotal : b.vmsTotal - a.vmsTotal case 'containers': return sortDir === 'asc' ? a.ctTotal - b.ctTotal : b.ctTotal - a.ctTotal case 'status': va = a.status; vb = b.status; break default: va = a.name; vb = b.name } if (typeof va === 'string') { const cmp = va.localeCompare(vb) return sortDir === 'asc' ? cmp : -cmp } return 0 }) return list }, [enriched, search, sortKey, sortDir]) const SortHeader = ({ label, k, className = '' }) => ( toggleSort(k)} > {label} {sortKey === k && (sortDir === 'asc' ? : )} ) return (

Proxmox Server ({enriched.length})

setSearch(e.target.value)} className="w-full bg-gray-900 border border-gray-800 rounded pl-10 pr-3 py-2 text-sm text-white placeholder-gray-600 focus:outline-none focus:border-orange-500" />
{showColMenu && (
Sichtbare Spalten
{ALL_COLUMNS.map(col => ( ))}
)}
{loading ? (
Laden...
) : (
{isColVisible('customer') && } {isColVisible('name') && } {isColVisible('hostname') && } {isColVisible('version') && } {isColVisible('ip') && } {isColVisible('uptime') && } {isColVisible('cpu') && } {isColVisible('ram') && } {isColVisible('disk') && } {isColVisible('vms') && } {isColVisible('containers') && } {isColVisible('lastresponse') && } {isColVisible('agentversion') && } {filtered.map((a) => ( setSelectedId(a.id)} className={`hover:bg-gray-800/50 transition-colors cursor-pointer ${selectedId === a.id ? 'bg-gray-800/70' : ''}`} > {isColVisible('customer') && ( )} {isColVisible('name') && } {isColVisible('hostname') && } {isColVisible('version') && } {isColVisible('ip') && } {isColVisible('uptime') && } {isColVisible('cpu') && } {isColVisible('ram') && } {isColVisible('disk') && } {isColVisible('vms') && } {isColVisible('containers') && } {isColVisible('lastresponse') && ( )} {isColVisible('agentversion') && } ))}
HOSTNAMEIPLETZTER KONTAKTAGENT
{a.customer ? {a.customerNumber} : } {a.name}{a.hostname}{a.pveVersion || '—'}{a.ip}{a.uptime ? formatUptime(a.uptime) : '—'} {a.last_heartbeat ? new Date(a.last_heartbeat).toLocaleString('de-DE') : '—'} {a.agent_version || '—'}
{filtered.length === 0 && (
{search ? 'Keine Treffer' : 'Keine Proxmox Server registriert'}
)}
)} {/* Detail Panel */} {selectedId && ( setSelectedId(null)} onReload={reload} /> )} {/* Server hinzufuegen Dialog */} {showAddDialog && ( { const res = await api.preRegisterAgent(name, custId) setAddResult(res) reload() }} onClose={() => { setShowAddDialog(false); setAddResult(null) }} /> )}
) } function AddServerDialog({ customers, result, onAdd, onClose }) { const [name, setName] = useState('') const [custId, setCustId] = useState('') const [error, setError] = useState('') const [submitting, setSubmitting] = useState(false) const handleSubmit = async () => { if (!name.trim()) { setError('Name ist erforderlich'); return } setError('') setSubmitting(true) try { await onAdd(name.trim(), custId ? parseInt(custId) : null) } catch (e) { setError(e.message) } setSubmitting(false) } return (
e.stopPropagation()}>

Server hinzufuegen

{!result ? ( <>
setName(e.target.value)} placeholder="z.B. PVE-KUNDE.intra.example.net" className="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-sm text-white placeholder-gray-600 focus:outline-none focus:border-orange-500" autoFocus onKeyDown={e => e.key === 'Enter' && handleSubmit()} />
{error &&

{error}

} ) : ( <>
Server "{result.name}" erfolgreich angelegt (ID: {result.id?.substring(0, 12)}...)
)}
) } function VmBadge({ running, total }) { if (total === 0) return return ( {running} /{total} ) } function MiniBar({ value }) { if (value === null || value === undefined) return const color = value > 90 ? 'bg-red-500' : value > 70 ? 'bg-yellow-500' : value > 50 ? 'bg-blue-500' : 'bg-green-500' return (
{value.toFixed(0)}%
) } function formatUptime(seconds) { const d = Math.floor(seconds / 86400) const h = Math.floor((seconds % 86400) / 3600) const m = Math.floor((seconds % 3600) / 60) if (d > 0) return `${d}d ${h}h` return `${h}h ${m}m` }