import { useEffect, useState } from 'react' import api from '../api/client' import { useSettingsStore } from '../stores/settings' import StatusBadge from './StatusBadge' import TerminalModal from './TerminalModal' import { X, Cpu, MemoryStick, HardDrive, Clock, Network, Server, Monitor, Container, Database, FolderTree, Settings, Download, Trash2, Copy, Check, MonitorSmartphone, Terminal, Cable, ExternalLink, Plus, Unplug, } from 'lucide-react' const buildTabs = (hasPBS) => [ { id: 'overview', label: 'Uebersicht' }, { id: 'vms', label: 'Virtuelle Maschinen' }, { id: 'containers', label: 'Container' }, { id: 'storage', label: 'Storage' }, { id: 'zfs', label: 'ZFS' }, ...(hasPBS ? [{ id: 'pbs', label: 'Backup Server' }] : []), { id: 'tunnel', label: 'Tunnel' }, { id: 'services', label: 'Dienste' }, { id: 'updates', label: 'Updates' }, { id: 'backups', label: 'Backups' }, { id: 'agent', label: 'Agent' }, ] export default function ProxmoxPanel({ agentId, customers, onClose, onReload }) { const [data, setData] = useState(null) const [tab, setTab] = useState('overview') const [loading, setLoading] = useState(true) useEffect(() => { setLoading(true) setTab('overview') api.getAgent(agentId).then(setData).finally(() => setLoading(false)) }, [agentId]) useEffect(() => { if (!data) return const iv = setInterval(() => { api.getAgent(agentId).then(setData) }, 30000) return () => clearInterval(iv) }, [agentId, data]) if (loading) return
Laden...
if (!data) return
Nicht gefunden
const { agent, system_data: sys, status } = data const proxmox = sys?.proxmox const pbs = sys?.pbs const cust = agent.customer_id ? customers.find((c) => c.id === agent.customer_id) : null const tabs = buildTabs(pbs?.available) if (!proxmox?.available) { return
Keine Proxmox-Daten verfuegbar
} return ( {/* Header */}

{agent.name}

{agent.hostname} · IP: {agent.ip} · PVE {proxmox.version || '—'}
{/* Tabs */}
{tabs.map((t) => ( ))}
{/* Content */}
{!sys ? (
Keine Systemdaten
) : tab === 'overview' ? ( ) : tab === 'vms' ? ( ) : tab === 'containers' ? ( ) : tab === 'storage' ? ( ) : tab === 'zfs' ? ( ) : tab === 'tunnel' ? ( ) : tab === 'services' ? ( ) : tab === 'updates' ? ( ) : tab === 'pbs' ? ( ) : tab === 'backups' ? ( ) : tab === 'agent' ? ( ) : null}
) } function CustomerAssign({ agent, customers, current, onReload }) { const [editing, setEditing] = useState(false) const [saving, setSaving] = useState(false) const handleChange = async (e) => { const val = e.target.value setSaving(true) try { if (val === '') { await api.unassignCustomer(agent.id) } else { await api.assignCustomer(agent.id, parseInt(val)) } if (onReload) onReload() } catch (err) { console.error(err) } finally { setSaving(false) setEditing(false) } } return (
{!editing ? ( ) : ( )}
) } function Panel({ onClose, children }) { return ( <>
{children}
) } function MetricCard({ icon: Icon, label, value, sub, bar, barColor }) { return (
{Icon && } {label}
{value}
{sub &&
{sub}
} {bar !== undefined && bar !== null && (
)}
) } function StatusCard({ icon: Icon, value, label }) { return (
{Icon && }
{value}
{label}
) } function OverviewTab({ sys, proxmox }) { 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 cpuPct = sys.cpu?.usage_percent const barColor = (v) => v > 90 ? 'bg-red-500' : v > 70 ? 'bg-yellow-500' : v > 50 ? 'bg-orange-500' : 'bg-green-500' 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 const storageCount = proxmox.storage?.length || 0 const zfsCount = proxmox.zfs_pools?.length || 0 return ( <>

System-Metriken

Proxmox-Info

PVE Version
{proxmox.version || '—'}
Node Name
{proxmox.node || '—'}
Cluster Name
{proxmox.cluster?.name || '—'}
{/* Subscription */} {proxmox.subscription && ( <>

Subscription

{proxmox.subscription.productname || 'Proxmox VE'}
{proxmox.subscription.key && (
{proxmox.subscription.key}
)}
{proxmox.subscription.status || 'Unknown'}
{proxmox.subscription.next_due_date && (
bis {proxmox.subscription.next_due_date}
)}
)}

Kurzuebersicht

) } function VmsTab({ proxmox }) { const vms = proxmox.vms || [] // Sort: running first, then by name const sortedVms = [...vms].sort((a, b) => { if (a.status === 'running' && b.status !== 'running') return -1 if (a.status !== 'running' && b.status === 'running') return 1 return a.name.localeCompare(b.name) }) return ( <>

Virtuelle Maschinen ({vms.length})

{sortedVms.length === 0 ? (
Keine VMs gefunden
) : (
{sortedVms.map((vm) => ( ))}
VMID Name Status CPU RAM Disk Uptime
{vm.vmid} {vm.name} {vm.cpu_usage != null ? `${vm.cpu_usage.toFixed(1)}%` : '—'} {vm.memory_used && vm.memory_max ? `${formatBytes(vm.memory_used)} / ${formatBytes(vm.memory_max)}` : '—'} {vm.disk_used != null && vm.disk_max ? `${formatBytes(vm.disk_used)} / ${formatBytes(vm.disk_max)}` : '—'} {vm.uptime ? formatUptime(vm.uptime) : '—'}
)} ) } function ContainersTab({ proxmox }) { const containers = proxmox.containers || [] // Sort: running first, then by name const sortedCt = [...containers].sort((a, b) => { if (a.status === 'running' && b.status !== 'running') return -1 if (a.status !== 'running' && b.status === 'running') return 1 return a.name.localeCompare(b.name) }) return ( <>

Container ({containers.length})

{sortedCt.length === 0 ? (
Keine Container gefunden
) : (
{sortedCt.map((ct) => ( ))}
VMID Name Status CPU RAM Uptime
{ct.vmid} {ct.name} {ct.cpu_usage != null ? `${ct.cpu_usage.toFixed(1)}%` : '—'} {ct.memory_used && ct.memory_max ? `${formatBytes(ct.memory_used)} / ${formatBytes(ct.memory_max)}` : '—'} {ct.uptime ? formatUptime(ct.uptime) : '—'}
)} ) } function StorageTab({ proxmox }) { const storage = proxmox.storage || [] return ( <>

Storage ({storage.length})

{storage.length === 0 ? (
Keine Storage-Pools gefunden
) : (
{storage.map((s) => { const usedPct = s.total && s.total > 0 ? (s.used / s.total * 100) : 0 const barColor = usedPct > 90 ? 'bg-red-500' : usedPct > 70 ? 'bg-yellow-500' : 'bg-green-500' return (
{s.storage}
{s.type} • {s.content}
{s.total && ( <>
{formatBytes(s.used)} / {formatBytes(s.total)} {usedPct.toFixed(1)}%
)}
) })}
)} ) } function ZfsTab({ proxmox }) { const zfsPools = proxmox.zfs_pools || [] return ( <>

ZFS Pools ({zfsPools.length})

{zfsPools.length === 0 ? (
Keine ZFS-Pools gefunden
) : (
{zfsPools.map((pool) => { const healthColor = pool.health === 'ONLINE' ? 'text-green-400' : pool.health === 'DEGRADED' ? 'text-yellow-400' : 'text-red-400' const healthBg = pool.health === 'ONLINE' ? 'bg-green-500' : pool.health === 'DEGRADED' ? 'bg-yellow-500' : 'bg-red-500' return (
{pool.name}
{pool.health}
{(() => { const usedPct = pool.size > 0 ? (pool.allocated / pool.size * 100) : 0 const pctColor = usedPct > 90 ? 'bg-red-500' : usedPct > 70 ? 'bg-yellow-500' : 'bg-green-500' return ( <>
Groesse: {formatBytes(pool.size)}
Belegt: {formatBytes(pool.allocated)} ({usedPct.toFixed(1)}%)
Frei: {formatBytes(pool.free)}
{pool.fragmentation && (
Fragmentierung: {pool.fragmentation}
)}
{pool.scrub_status && (
{pool.scrub_status}
)} ) })()}
) })}
)} ) } function ServicesTab({ sys }) { const services = sys.services || [] // Sort: running first const sortedServices = [...services].sort((a, b) => { const aRunning = a.running || a.status === 'running' const bRunning = b.running || b.status === 'running' if (aRunning && !bRunning) return -1 if (!aRunning && bRunning) return 1 return a.name.localeCompare(b.name) }) return ( <>

Systemd Dienste ({services.length})

{sortedServices.length === 0 ? (
Keine Dienste gefunden
) : (
{sortedServices.map((svc, i) => { const running = svc.running || svc.status === 'running' return ( ) })}
Name Status
{svc.name}
)} ) } function UpdatesTab({ sys }) { const pendingUpdates = sys.pending_updates || [] return ( <>

Updates

{pendingUpdates.length} Updates verfuegbar
{pendingUpdates.length === 0 ? (
Keine Updates verfuegbar
) : (
{pendingUpdates.map((update, i) => ( ))}
Package Current Version New Version
{update.package} {update.current_version} {update.new_version}
)} ) } function AgentTab({ agent, status }) { return ( <>

Agent Information

Agent Version
{agent?.agent_version || '—'}
Status
Last Heartbeat
{agent?.last_heartbeat ? new Date(agent.last_heartbeat).toLocaleString('de-DE') : '—'}
Agent ID
{agent?.id?.substring(0, 12)}...
) } function TunnelTab({ agentId, agent, webguiPort = 8006 }) { const [tunnels, setTunnels] = useState([]) const [loading, setLoading] = useState(true) const [creating, setCreating] = useState(null) const [customOpen, setCustomOpen] = useState(false) const [customHost, setCustomHost] = useState('127.0.0.1') const [customPort, setCustomPort] = useState('') const [error, setError] = useState('') const [sshCopied, setSshCopied] = useState(null) const [terminalOpen, setTerminalOpen] = useState(false) const backendHost = useSettingsStore.getState().getBackendHost() const loadTunnels = () => { api.getTunnels(agentId) .then((resp) => { const list = resp?.tunnels || resp?.data?.tunnels || resp?.data || resp || [] setTunnels(Array.isArray(list) ? list : []) }) .catch(() => setTunnels([])) .finally(() => setLoading(false)) } useEffect(() => { loadTunnels() }, [agentId]) useEffect(() => { if (tunnels.length > 0) { const iv = setInterval(loadTunnels, 10000) return () => clearInterval(iv) } }, [tunnels.length, agentId]) const openTunnel = async (targetHost, targetPort, label) => { setError('') setCreating(label) try { const resp = await api.openTunnel(agentId, targetHost, targetPort) const data = resp?.data || resp loadTunnels() if (data?.proxy_port) { setTimeout(() => { if (targetPort === webguiPort) { window.open(`https://${backendHost}:${data.proxy_port}`, '_blank') } else if (targetPort === 22) { const cmd = `ssh root@${backendHost} -p ${data.proxy_port}` navigator.clipboard.writeText(cmd).catch(() => {}) setSshCopied(data.proxy_port) setTimeout(() => setSshCopied(null), 4000) } }, 1000) } return data } catch (e) { setError(e.message) } finally { setCreating(null) } } const closeTunnel = async (tunnelId) => { try { await api.closeTunnel(agentId, tunnelId) setTunnels((prev) => prev.filter((t) => t.id !== tunnelId && t.tunnel_id !== tunnelId)) } catch (e) { setError(e.message) } } const handleCustom = () => { const port = parseInt(customPort) if (!port || port < 1 || port > 65535) return openTunnel(customHost, port, 'custom') setCustomOpen(false) setCustomPort('') } return (
{terminalOpen && ( setTerminalOpen(false)} /> )} {/* Schnellzugriff */}
Schnellzugriff:
{/* Custom Form */} {customOpen && (
setCustomHost(e.target.value)} className="bg-gray-900 border border-gray-700 rounded px-3 py-2 text-sm text-white w-40 focus:outline-none focus:border-orange-500" />
setCustomPort(e.target.value)} placeholder="z.B. 8080" className="bg-gray-900 border border-gray-700 rounded px-3 py-2 text-sm text-white w-28 focus:outline-none focus:border-orange-500" onKeyDown={(e) => e.key === 'Enter' && handleCustom()} />
)} {/* SSH auto-kopiert Bestätigung */} {sshCopied && (
SSH-Befehl kopiert — in CMD/Terminal einfuegen und ausfuehren
)} {error &&
{error}
} {/* Aktive Tunnel */}

Aktive Tunnel

{loading ? (
Laden...
) : tunnels.length === 0 ? (
Keine aktiven Tunnel
) : (
{tunnels.map((t) => { const tid = t.id || t.tunnel_id const proxyPort = t.proxy_port || t.local_port const target = t.target_host && t.target_port ? `${t.target_host}:${t.target_port}` : '—' const isWebGUI = t.target_port === webguiPort || t.target_port === String(webguiPort) const isSSH = t.target_port === 22 || t.target_port === '22' return ( ) })}
Ziel Lokaler Zugang Status Erstellt Aktionen
{target}
{backendHost}:{proxyPort} {isWebGUI && proxyPort && ( )} {isSSH && proxyPort && ( )}
aktiv {t.created_at ? new Date(t.created_at).toLocaleString('de-DE') : '—'}
)}
Tunnel werden ueber den RMM-Backend-Server ({backendHost}) geroutet. WebGUI-Tunnel oeffnen automatisch ein neues Browserfenster.
) } function BackupsTab({ sys }) { const backups = sys?.backups || {} const jobs = backups.jobs || [] const tasks = backups.tasks || [] const totalJobs = jobs.filter(j => j.enabled === 1 || j.enabled === true).length const okTasks = tasks.filter(t => t.status === 'OK').length const failedTasks = tasks.filter(t => t.status && t.status !== 'OK').length const successRate = tasks.length > 0 ? Math.round((okTasks / tasks.length) * 100) : null return (
{/* Stats */}
Jobs (aktiv)
{totalJobs}
Aufgaben (30d)
{tasks.length}
Erfolgreich
{okTasks}
Erfolgsrate
= 90 ? 'text-yellow-400' : 'text-red-400'}`}> {successRate !== null ? `${successRate}%` : '--'}
{/* Backup Jobs */}

Backup Jobs

{jobs.length === 0 ? (
Keine Backup-Jobs konfiguriert
) : (
{jobs.map((j, i) => ( ))}
ID Storage Schedule Modus VMs/CTs Status
{j.id || i} {j.storage || '—'} {j.schedule || '—'} {j.mode || '—'} {j.vmid || j.all ? 'Alle' : '—'} {j.enabled ? 'Aktiv' : 'Inaktiv'}
)} {/* Task History */}

Letzte Backup-Aufgaben

{tasks.length === 0 ? (
Keine Backup-Aufgaben in den letzten 30 Tagen
) : (
{tasks.slice(0, 50).map((t, i) => ( ))}
Zeitpunkt Typ VMID Status
{t.starttime ? new Date(t.starttime * 1000).toLocaleString('de-DE') : '—'} {t.type || 'vzdump'} {t.id || '—'} {t.status || '—'}
)} {failedTasks > 0 && (
{failedTasks} fehlgeschlagene Backup(s) in den letzten 30 Tagen
)}
) } function formatBytes(bytes) { if (bytes === 0) return '0 B' const k = 1024 const sizes = ['B', 'KB', 'MB', 'GB', 'TB'] const i = Math.floor(Math.log(bytes) / Math.log(k)) return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i] } 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` } function PBSTab({ pbs, agentId, sys }) { const [loading, setLoading] = useState({}) const [feedback, setFeedback] = useState({}) if (!pbs?.available) return
Keine PBS-Daten verfuegbar
const formatBytes = (b) => { if (!b) return '—' if (b >= 1099511627776) return `${(b / 1099511627776).toFixed(1)} TB` if (b >= 1073741824) return `${(b / 1073741824).toFixed(1)} GB` if (b >= 1048576) return `${(b / 1048576).toFixed(1)} MB` return `${b} B` } const formatTs = (ts) => ts ? new Date(ts * 1000).toLocaleString('de-DE') : '—' const formatDuration = (s) => { if (!s) return '—' if (s < 60) return `${s}s` if (s < 3600) return `${Math.floor(s/60)}m ${s%60}s` return `${Math.floor(s/3600)}h ${Math.floor((s%3600)/60)}m` } const taskStatusColor = (s) => { if (!s) return 'text-gray-500' if (s === 'OK' || s.startsWith('OK')) return 'text-green-400' if (s.toLowerCase().includes('err') || s.toLowerCase().includes('fail')) return 'text-red-400' if (s === 'running') return 'text-blue-400' return 'text-yellow-400' } const pbsAction = async (datastore, action) => { const key = `${datastore}-${action}` const cmds = { gc: `/usr/sbin/proxmox-backup-manager garbage-collection start ${datastore}`, verify: `/usr/sbin/proxmox-backup-manager verify ${datastore}`, prune: `/usr/sbin/proxmox-backup-manager prune-datastore ${datastore}`, } setLoading(l => ({ ...l, [key]: true })) setFeedback(f => ({ ...f, [key]: null })) try { await api.execCommand(agentId, cmds[action], 60) setFeedback(f => ({ ...f, [key]: 'ok' })) } catch (e) { setFeedback(f => ({ ...f, [key]: 'err' })) } finally { setLoading(l => ({ ...l, [key]: false })) setTimeout(() => setFeedback(f => ({ ...f, [key]: null })), 4000) } } const totalBackups = pbs.datastores?.reduce((s, d) => s + (d.backup_count || 0), 0) || 0 const runningTasks = pbs.tasks?.filter(t => t.running)?.length || 0 const StatCard = ({ icon, value, label, iconColor, highlight }) => (
{icon}
{value}
{label}
) return (
{/* Titel */}
Backup Server
{pbs.version &&
PBS {pbs.version}
}
{/* Stat-Karten */}
} value={pbs.datastores?.length || 0} label="Datastores" iconColor="text-orange-400" /> } value={totalBackups} label="Backups" iconColor="text-blue-400" /> } value={pbs.sync_jobs ?? 0} label="Sync Jobs" iconColor="text-green-400" /> } value={runningTasks > 0 ? runningTasks : '—'} label="Laufend" iconColor={runningTasks > 0 ? "text-blue-400" : "text-gray-600"} highlight={runningTasks > 0} />
{/* Datastores */} {pbs.datastores?.length > 0 && (

Datastores

{pbs.datastores.map((ds) => { const usedPct = ds.total > 0 ? (ds.used / ds.total * 100) : 0 const barColor = usedPct > 90 ? 'bg-red-500' : usedPct > 70 ? 'bg-yellow-500' : 'bg-green-500' const gc = pbs.gc?.find(g => g.datastore === ds.name) const mkKey = (a) => `${ds.name}-${a}` const ActionBtn = ({ action, label, cls }) => { const k = mkKey(action) const fb = feedback[k] return ( ) } return (
{ds.name}
{ds.backup_count > 0 && {ds.backup_count} Backups}
{formatBytes(ds.used)} / {formatBytes(ds.total)} {usedPct.toFixed(1)}%
{gc?.status && (
GC: {gc.status} {gc.last_run > 0 && Letzter Lauf: {formatTs(gc.last_run)}} {gc.duration > 0 && ({formatDuration(gc.duration)})}
)}
) })}
)} {/* Tasks */} {pbs.tasks?.length > 0 && (

Letzte Tasks

{pbs.tasks.map((t, i) => ( ))}
Typ Start Dauer Status
{t.type || '—'} {t.running && RUNNING} {formatTs(t.start_time)} {t.running ? läuft... : formatDuration(t.duration)} {t.status || '—'}
)}
) }