From 50140377e8ae93094570f935b4a0607af9742889 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 27 Aug 2026 08:29:59 +0200 Subject: [PATCH] Release v1.1: Hotfix JS Code Fehler. --- src/api_pve.php | 361 +++++++++++++++++++++++++++++++++++++++++++++++- src/app.js | 43 +++++- 2 files changed, 396 insertions(+), 8 deletions(-) diff --git a/src/api_pve.php b/src/api_pve.php index f547c7e..302ba27 100644 --- a/src/api_pve.php +++ b/src/api_pve.php @@ -1,3 +1,361 @@ +query("SELECT * FROM nodes WHERE type = 'pve'"); + $nodes = $stmt->fetchAll(PDO::FETCH_ASSOC); + $data = [ + 'cpu_cores' => 0, 'cpu_used' => 0, 'ram_total' => 0, 'ram_used' => 0, + 'disk_total' => 0, 'disk_used' => 0, 'cpu_percent' => 0, 'nodes_net' => [], + 'cluster_stats' => ['nodes_total' => count($nodes), 'nodes_online' => 0, 'vms_total' => 0, 'vms_running' => 0, 'vms_stopped' => 0] + ]; + + $seenNodes = []; + $seenVms = []; + + foreach ($nodes as $node) { + $nData = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes"); + if (isset($nData['data'])) { + foreach ($nData['data'] as $nInfo) { + // Duplikate (z.B. in Clustern) vermeiden + if (!isset($seenNodes[$nInfo['node']])) { + $seenNodes[$nInfo['node']] = true; + + if ($nInfo['status'] === 'online') { + $data['cluster_stats']['nodes_online']++; + $data['cpu_cores'] += $nInfo['maxcpu'] ?? 0; + $data['cpu_used'] += ($nInfo['cpu'] ?? 0) * ($nInfo['maxcpu'] ?? 0); + $data['ram_total'] += $nInfo['maxmem'] ?? 0; + $data['ram_used'] += $nInfo['mem'] ?? 0; + $data['disk_total'] += $nInfo['maxdisk'] ?? 0; + $data['disk_used'] += $nInfo['disk'] ?? 0; + + $nRrd = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$nInfo['node']}/rrddata?timeframe=hour&cf=AVERAGE"); + $netin = 0; $netout = 0; + if (isset($nRrd['data']) && is_array($nRrd['data'])) { + for ($i = count($nRrd['data']) - 1; $i >= 0; $i--) { + if (isset($nRrd['data'][$i]['netin']) && $nRrd['data'][$i]['netin'] !== null) { + $netin = $nRrd['data'][$i]['netin']; + $netout = $nRrd['data'][$i]['netout']; + break; + } + } + } + $data['nodes_net'][] = ['name' => $nInfo['node'], 'netin' => $netin, 'netout' => $netout]; + } + } + } + } + + // VMs von allen angebundenen Hosts ziehen und Duplikate filtern + $vms = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/cluster/resources?type=vm"); + if (isset($vms['data'])) { + foreach($vms['data'] as $vm) { + $uniqueId = $vm['vmid'] . '@' . ($vm['node'] ?? 'unknown'); + if (!isset($seenVms[$uniqueId])) { + $seenVms[$uniqueId] = true; + $data['cluster_stats']['vms_total']++; + if (isset($vm['status']) && $vm['status'] === 'running') { + $data['cluster_stats']['vms_running']++; + } else { + $data['cluster_stats']['vms_stopped']++; + } + } + } + } + } + if ($data['cpu_cores'] > 0) $data['cpu_percent'] = round(($data['cpu_used'] / $data['cpu_cores']) * 100, 1); + echo json_encode(['success' => true, 'data' => $data]); exit; +} + +if ($action === 'get_top_vms') { + $stmt = $pdo->query("SELECT * FROM nodes WHERE type = 'pve'"); $nodes = $stmt->fetchAll(PDO::FETCH_ASSOC); $allVms = []; $seenVms = []; + foreach ($nodes as $node) { + $vms = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/cluster/resources?type=vm"); + if (isset($vms['data'])) { + foreach ($vms['data'] as $vm) { + $uniqueId = $vm['vmid'] . '@' . ($vm['node'] ?? 'unknown'); + if (!isset($seenVms[$uniqueId]) && checkVmPermission($pdo, $vm['vmid'])) { + $seenVms[$uniqueId] = true; + $vm['node_id'] = $node['id']; + $vm['node_ip'] = $node['ip_address']; + $vm['host'] = $vm['node'] ?? 'unknown'; + $allVms[] = $vm; + } + } + } + } + usort($allVms, function($a, $b) { return ($b['cpu'] ?? 0) <=> ($a['cpu'] ?? 0); }); + echo json_encode(['success' => true, 'data' => array_slice($allVms, 0, 5)]); exit; +} + +if ($action === 'get_all_vms') { + $stmt = $pdo->query("SELECT * FROM nodes WHERE type = 'pve'"); $nodes = $stmt->fetchAll(PDO::FETCH_ASSOC); $allVms = []; $seenVms = []; + foreach ($nodes as $node) { + $vms = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/cluster/resources?type=vm"); + if (isset($vms['data'])) { + foreach ($vms['data'] as $vm) { + $uniqueId = $vm['vmid'] . '@' . ($vm['node'] ?? 'unknown'); + if (!isset($seenVms[$uniqueId]) && checkVmPermission($pdo, $vm['vmid'])) { + $seenVms[$uniqueId] = true; + $vm['node_id'] = $node['id']; + $vm['node_ip'] = $node['ip_address']; + $vm['host'] = $vm['node'] ?? 'unknown'; + $allVms[] = $vm; + } + } + } + } + usort($allVms, function($a, $b) { if ($a['status'] === 'running' && $b['status'] !== 'running') return -1; if ($a['status'] !== 'running' && $b['status'] === 'running') return 1; return $a['vmid'] <=> $b['vmid']; }); + echo json_encode(['success' => true, 'data' => $allVms]); exit; +} + +if ($action === 'vm_action') { + if (!checkVmPermission($pdo, $_POST['vmid'])) exit; + $stmt = $pdo->prepare("SELECT * FROM nodes WHERE id = ?"); $stmt->execute([$_POST['node_id']]); $node = $stmt->fetch(PDO::FETCH_ASSOC); + $res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$_POST['host']}/{$_POST['type']}/{$_POST['vmid']}/status/{$_POST['cmd']}", 'pve', 'POST'); + logAudit("VM {$_POST['cmd']}", "VMID: {$_POST['vmid']} auf Host: {$_POST['host']}"); + echo json_encode(['success' => isset($res['data']), 'error' => $res['errors'] ?? 'Aktion fehlgeschlagen.']); exit; +} + +if ($action === 'get_nodes') { if (!$isAdmin) exit; $stmt = $pdo->query("SELECT id, name, ip_address, type FROM nodes"); echo json_encode(['success' => true, 'data' => $stmt->fetchAll(PDO::FETCH_ASSOC)]); exit; } + +if ($action === 'delete_node') { if (!$isAdmin) exit; $stmt = $pdo->prepare("DELETE FROM nodes WHERE id = ?"); $stmt->execute([$_POST['id']]); logAudit('Server gelöscht', "Node ID: {$_POST['id']}"); echo json_encode(['success' => true]); exit; } + +if ($action === 'add_node') { + if (!$isAdmin) exit; + + $name = trim($_POST['name']); + $ip = trim($_POST['ip']); + $user = trim($_POST['user']); + $pass = trim($_POST['pass']); + $type = $_POST['type'] ?? 'pve'; + + $tokenIdToSave = $user; + $tokenSecretToSave = $pass; + + if ($type === 'pve') { + $chAuth = curl_init("https://{$ip}:8006/api2/json/access/ticket"); + curl_setopt_array($chAuth, [ + CURLOPT_RETURNTRANSFER => true, CURLOPT_SSL_VERIFYPEER => false, + CURLOPT_SSL_VERIFYHOST => false, CURLOPT_TIMEOUT => 5, + CURLOPT_POST => true, CURLOPT_POSTFIELDS => http_build_query(['username' => $user, 'password' => $pass]) + ]); + $authRes = json_decode(curl_exec($chAuth), true); + curl_close($chAuth); + + if(!isset($authRes['data']['ticket'])) { + echo json_encode(['success' => false, 'error' => 'Proxmox Login fehlgeschlagen. Passwort falsch?']); exit; + } + + $ticket = $authRes['data']['ticket']; + $csrf = $authRes['data']['CSRFPreventionToken']; + + $tokenName = 'pvedash' . rand(1000, 9999); + $chToken = curl_init("https://{$ip}:8006/api2/json/access/users/{$user}/token/{$tokenName}"); + curl_setopt_array($chToken, [ + CURLOPT_RETURNTRANSFER => true, CURLOPT_SSL_VERIFYPEER => false, + CURLOPT_SSL_VERIFYHOST => false, CURLOPT_TIMEOUT => 5, + CURLOPT_POST => true, CURLOPT_POSTFIELDS => http_build_query(['privsep' => 0]), + CURLOPT_HTTPHEADER => ["Cookie: PVEAuthCookie={$ticket}", "CSRFPreventionToken: {$csrf}"] + ]); + $tokenRes = json_decode(curl_exec($chToken), true); + curl_close($chToken); + + if(!isset($tokenRes['data']['value'])) { + echo json_encode(['success' => false, 'error' => 'Konnte API Token nicht erstellen. Admin-Rechte?']); exit; + } + + $tokenIdToSave = $user . '!' . $tokenName; + $tokenSecretToSave = $tokenRes['data']['value']; + } + + $stmt = $pdo->prepare("INSERT INTO nodes (name, ip_address, token_id, token_secret, type) VALUES (?, ?, ?, ?, ?)"); + $stmt->execute([$name, $ip, $tokenIdToSave, $tokenSecretToSave, $type]); + logAudit('Server hinzugefügt', "Node: {$name} ({$ip})"); + echo json_encode(['success' => true]); exit; +} + +if ($action === 'get_users') { if (!$isAdmin) exit; $stmt = $pdo->query("SELECT id, username, role FROM users"); echo json_encode(['success' => true, 'data' => $stmt->fetchAll(PDO::FETCH_ASSOC)]); exit; } +if ($action === 'delete_user') { if (!$isAdmin) exit; $stmt = $pdo->prepare("DELETE FROM users WHERE id = ?"); $stmt->execute([$_POST['id']]); logAudit('User gelöscht', "User ID: {$_POST['id']}"); echo json_encode(['success' => true]); exit; } +if ($action === 'create_user') { + if (!$isAdmin) exit; + $stmt = $pdo->prepare("INSERT INTO users (username, password_hash, role, permissions) VALUES (?, ?, ?, ?)"); + $stmt->execute([$_POST['username'], password_hash($_POST['password'], PASSWORD_DEFAULT), $_POST['role'], $_POST['permissions']]); + logAudit('User angelegt', "Username: {$_POST['username']} Rolle: {$_POST['role']}"); + echo json_encode(['success' => true]); exit; +} + +if ($action === 'get_pve_nodes') { + if (!$isAdmin) exit; + $stmt = $pdo->query("SELECT * FROM nodes WHERE type = 'pve'"); $resList = []; $seenNodes = []; + foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $node) { + $nData = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes"); + if(isset($nData['data'])) { + foreach($nData['data'] as $n) { + if($n['status'] === 'online' && !isset($seenNodes[$n['node']])) { + $seenNodes[$n['node']] = true; + $resList[] = ['node_id' => $node['id'], 'host' => $n['node'], 'display' => $node['name'] . ' -> ' . $n['node']]; + } + } + } + } + echo json_encode(['success' => true, 'data' => $resList]); exit; +} + +if ($action === 'create_vm') { + if (!$isAdmin) exit; + $stmt = $pdo->prepare("SELECT * FROM nodes WHERE id = ? AND type = 'pve'"); $stmt->execute([$_POST['node_id']]); $node = $stmt->fetch(PDO::FETCH_ASSOC); + $nextIdRes = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/cluster/nextid"); + if(!isset($nextIdRes['data'])) { echo json_encode(['success' => false, 'error' => 'Konnte keine freie VMID finden.']); exit; } + $vmid = $nextIdRes['data']; + $params = ['vmid' => $vmid, 'name' => $_POST['name'], 'memory' => $_POST['memory'], 'cores' => $_POST['cores'], 'net0' => 'virtio,bridge=vmbr0']; + $res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$_POST['host']}/qemu", 'pve', 'POST', $params); + logAudit('VM Erstellt', "VMID: {$vmid} Name: {$_POST['name']}"); + echo json_encode(['success' => isset($res['data']), 'vmid' => $vmid, 'error' => isset($res['data']) ? '' : 'Fehler bei Erstellung.']); exit; +} + +if ($action === 'get_vm_config') { + if (!checkVmPermission($pdo, $_GET['vmid'])) exit; + $stmt = $pdo->prepare("SELECT * FROM nodes WHERE id = ?"); $stmt->execute([$_GET['node_id']]); $node = $stmt->fetch(PDO::FETCH_ASSOC); + $res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$_GET['host']}/{$_GET['type']}/{$_GET['vmid']}/config"); + echo json_encode(['success' => true, 'data' => $res['data'] ?? []]); exit; +} + +if ($action === 'update_vm_config') { + if (!checkVmPermission($pdo, $_POST['vmid'])) exit; + $stmt = $pdo->prepare("SELECT * FROM nodes WHERE id = ?"); $stmt->execute([$_POST['node_id']]); $node = $stmt->fetch(PDO::FETCH_ASSOC); + $params = []; + if(isset($_POST['memory'])) $params['memory'] = $_POST['memory']; + if(isset($_POST['cores'])) $params['cores'] = $_POST['cores']; + if(isset($_POST['net0'])) $params['net0'] = $_POST['net0']; + $res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$_POST['host']}/{$_POST['type']}/{$_POST['vmid']}/config", 'pve', 'POST', $params); + logAudit('VM Config geändert', "VMID: {$_POST['vmid']}"); + echo json_encode(['success' => isset($res['data']), 'error' => 'API Fehler']); exit; +} + +if ($action === 'add_vm_nic') { + if (!checkVmPermission($pdo, $_POST['vmid'])) exit; + $stmt = $pdo->prepare("SELECT * FROM nodes WHERE id = ?"); $stmt->execute([$_POST['node_id']]); $node = $stmt->fetch(PDO::FETCH_ASSOC); + $cfg = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$_POST['host']}/{$_POST['type']}/{$_POST['vmid']}/config"); + $nextNic = 0; for ($i=0; $i<10; $i++) { if (!isset($cfg['data']["net{$i}"])) { $nextNic = $i; break; } } + $res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$_POST['host']}/{$_POST['type']}/{$_POST['vmid']}/config", 'pve', 'POST', ["net{$nextNic}" => "virtio,bridge={$_POST['bridge']}"]); + logAudit('VM NIC hinzugefügt', "VMID: {$_POST['vmid']} Bridge: {$_POST['bridge']}"); + echo json_encode(['success' => isset($res['data']), 'slot' => "net{$nextNic}", 'error' => 'API Fehler']); exit; +} + +if ($action === 'resize_vm_disk') { + if (!checkVmPermission($pdo, $_POST['vmid'])) exit; + $stmt = $pdo->prepare("SELECT * FROM nodes WHERE id = ?"); $stmt->execute([$_POST['node_id']]); $node = $stmt->fetch(PDO::FETCH_ASSOC); + $res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$_POST['host']}/{$_POST['type']}/{$_POST['vmid']}/resize", 'pve', 'PUT', ['disk' => $_POST['disk'], 'size' => $_POST['size']]); + logAudit('VM Disk erweitert', "VMID: {$_POST['vmid']} Disk: {$_POST['disk']} Size: {$_POST['size']}"); + echo json_encode(['success' => isset($res['data']), 'error' => 'API Fehler']); exit; +} + +if ($action === 'delete_vm') { + if (!$isAdmin) exit; + $stmt = $pdo->prepare("SELECT * FROM nodes WHERE id = ?"); $stmt->execute([$_POST['node_id']]); $node = $stmt->fetch(PDO::FETCH_ASSOC); + $res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$_POST['host']}/{$_POST['type']}/{$_POST['vmid']}", 'pve', 'DELETE'); + logAudit('VM Gelöscht', "VMID: {$_POST['vmid']}"); + echo json_encode(['success' => isset($res['data']), 'error' => 'API Fehler']); exit; +} + +if ($action === 'get_vm_snapshots') { + if (!checkVmPermission($pdo, $_GET['vmid'])) exit; + $stmt = $pdo->prepare("SELECT * FROM nodes WHERE id = ?"); $stmt->execute([$_GET['node_id']]); $node = $stmt->fetch(PDO::FETCH_ASSOC); + $res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$_GET['host']}/{$_GET['type']}/{$_GET['vmid']}/snapshot"); + echo json_encode(['success' => true, 'data' => $res['data'] ?? []]); exit; +} + +if ($action === 'vm_snapshot_action') { + if (!checkVmPermission($pdo, $_POST['vmid'])) exit; + $stmt = $pdo->prepare("SELECT * FROM nodes WHERE id = ?"); $stmt->execute([$_POST['node_id']]); $node = $stmt->fetch(PDO::FETCH_ASSOC); + $cmd = $_POST['cmd']; $snapname = $_POST['snapname']; + if ($cmd === 'create') { $res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$_POST['host']}/{$_POST['type']}/{$_POST['vmid']}/snapshot", 'pve', 'POST', ['snapname' => $snapname]); } + elseif ($cmd === 'delete') { $res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$_POST['host']}/{$_POST['type']}/{$_POST['vmid']}/snapshot/{$snapname}", 'pve', 'DELETE'); } + elseif ($cmd === 'rollback') { $res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$_POST['host']}/{$_POST['type']}/{$_POST['vmid']}/snapshot/{$snapname}/rollback", 'pve', 'POST'); } + logAudit('Snapshot ' . $cmd, "VMID: {$_POST['vmid']} Snap: {$snapname}"); + echo json_encode(['success' => isset($res['data']), 'error' => 'API Fehler']); exit; +} + +if ($action === 'get_backup_storages') { + if (!checkVmPermission($pdo, $_GET['vmid'] ?? 0)) exit; + $stmt = $pdo->prepare("SELECT * FROM nodes WHERE id = ?"); $stmt->execute([$_GET['node_id']]); $node = $stmt->fetch(PDO::FETCH_ASSOC); + $stRes = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$_GET['host']}/storage"); + $storages = []; if (isset($stRes['data'])) { foreach ($stRes['data'] as $st) { if (strpos($st['content'], 'backup') !== false) { $storages[] = $st['storage']; } } } + echo json_encode(['success' => true, 'data' => $storages]); exit; +} + +if ($action === 'get_vm_backups') { + if (!checkVmPermission($pdo, $_GET['vmid'])) exit; + $stmt = $pdo->prepare("SELECT * FROM nodes WHERE id = ?"); $stmt->execute([$_GET['node_id']]); $node = $stmt->fetch(PDO::FETCH_ASSOC); + $stRes = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$_GET['host']}/storage"); + $backups = []; + if (isset($stRes['data'])) { + foreach ($stRes['data'] as $st) { + if (strpos($st['content'], 'backup') !== false) { + $bRes = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$_GET['host']}/storage/{$st['storage']}/content?vmid={$_GET['vmid']}"); + if (isset($bRes['data'])) { foreach ($bRes['data'] as $b) { $b['storage'] = $st['storage']; $backups[] = $b; } } + } + } + } + usort($backups, function($a, $b) { return $b['ctime'] <=> $a['ctime']; }); + echo json_encode(['success' => true, 'data' => $backups]); exit; +} + +if ($action === 'create_backup') { + if (!checkVmPermission($pdo, $_POST['vmid'])) exit; + $stmt = $pdo->prepare("SELECT * FROM nodes WHERE id = ?"); $stmt->execute([$_POST['node_id']]); $node = $stmt->fetch(PDO::FETCH_ASSOC); + $res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$_POST['host']}/vzdump", 'pve', 'POST', ['vmid' => $_POST['vmid'], 'storage' => $_POST['storage'], 'mode' => 'snapshot']); + logAudit('Manuelles Backup', "VMID: {$_POST['vmid']} Storage: {$_POST['storage']}"); + echo json_encode(['success' => isset($res['data']), 'error' => 'API Fehler']); exit; +} + +if ($action === 'restore_backup') { + if (!checkVmPermission($pdo, $_POST['vmid'])) exit; + $stmt = $pdo->prepare("SELECT * FROM nodes WHERE id = ?"); $stmt->execute([$_POST['node_id']]); $node = $stmt->fetch(PDO::FETCH_ASSOC); + $res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$_POST['host']}/{$_POST['type']}/{$_POST['vmid']}/status/current"); + if (isset($res['data']) && $res['data']['status'] === 'running') { echo json_encode(['success' => false, 'error' => 'VM gestoppt?']); exit; } + $resRestore = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$_POST['host']}/{$_POST['type']}", 'pve', 'POST', ['vmid' => $_POST['vmid'], 'archive' => $_POST['archive'], 'force' => 1]); + logAudit('Backup Restore', "VMID: {$_POST['vmid']} Archive: {$_POST['archive']}"); + echo json_encode(['success' => isset($resRestore['data']), 'error' => 'API Fehler']); exit; +} + +if ($action === 'get_node_status' || $action === 'get_vm_status') { + $stmt = $pdo->prepare("SELECT * FROM nodes WHERE id = ?"); $stmt->execute([$_GET['node_id']]); $node = $stmt->fetch(PDO::FETCH_ASSOC); + + if ($action === 'get_node_status') { + $nData = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes"); + $internalName = $nData['data'][0]['node'] ?? 'pve'; + + $res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$internalName}/status"); + $rrd = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$internalName}/rrddata?timeframe=hour&cf=AVERAGE"); + + $netin = 0; $netout = 0; + if (isset($rrd['data']) && is_array($rrd['data'])) { + for ($i = count($rrd['data']) - 1; $i >= 0; $i--) { + if (isset($rrd['data'][$i]['netin']) && $rrd['data'][$i]['netin'] !== null) { + $netin = $rrd['data'][$i]['netin']; + $netout = $rrd['data'][$i]['netout']; + break; + } + } + } + + $data = $res['data'] ?? []; + $data['netin'] = $netin; + $data['netout'] = $netout; + $data['is_rrd_net'] = true; + + echo json_encode(['success' => true, 'data' => $data]); exit; + } else { + $endpoint = "/api2/json/nodes/{$_GET['host']}/{$_GET['type']}/{$_GET['vmid']}/status/current"; + $res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], $endpoint); + echo json_encode(['success' => true, 'data' => $res['data'] ?? []]); exit; + } +} + // === NEU: RRD Historie (24h / 7 Tage) für alle Node Typen === if ($action === 'get_historical_rrd') { if (!checkVmPermission($pdo, $_GET['vmid'] ?? 0)) exit; @@ -47,4 +405,5 @@ if ($action === 'trigger_apt_upgrade') { $res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$_POST['host']}/apt/upgrade", $node['type'], 'POST'); logAudit('APT Upgrade gestartet', "Node: {$_POST['host']}"); echo json_encode(['success' => isset($res['data']), 'upid' => $res['data'] ?? '', 'error' => $res['errors'] ?? 'API Fehler']); exit; -} \ No newline at end of file +} +?> \ No newline at end of file diff --git a/src/app.js b/src/app.js index db76801..ff86cb4 100644 --- a/src/app.js +++ b/src/app.js @@ -43,6 +43,13 @@ if (window.APP.isLoggedIn && window.APP.nodeCount > 0) { if(tab === 'pbs') fetchPbsStats(); if(tab === 'pmg') fetchPmgStats(); } + // HAUPT-GRAPHEN WIEDER HERGESTELLT + const ctx = document.getElementById('liveChart')?.getContext('2d'); let liveChart; + if(ctx) { liveChart = new Chart(ctx, { type: 'line', data: { labels: [], datasets: [{ label: 'CPU (%)', borderColor: '#3b82f6', backgroundColor: 'rgba(59, 130, 246, 0.1)', borderWidth: 2, tension: 0.4, fill: true, data: [] }, { label: 'RAM (%)', borderColor: '#E57000', backgroundColor: 'rgba(229, 112, 0, 0.1)', borderWidth: 2, tension: 0.4, fill: true, data: [] }] }, options: { responsive: true, maintainAspectRatio: false, animation: { duration: 500 }, scales: { x: { ticks: { color: '#9ca3af' }, grid: { color: '#33334d' } }, y: { min: 0, max: 100, ticks: { color: '#9ca3af', callback: v => v + '%' }, grid: { color: '#33334d' } } }, plugins: { legend: { labels: { color: '#e2e8f0', usePointStyle: true } } } } }); } + + const ctxNet = document.getElementById('liveNetChart')?.getContext('2d'); let liveNetChart; + if(ctxNet) { liveNetChart = new Chart(ctxNet, { type: 'line', data: { labels: [], datasets: [] }, options: { responsive: true, maintainAspectRatio: false, animation: { duration: 500 }, scales: { x: { ticks: { color: '#9ca3af' }, grid: { color: '#33334d' } }, y: { min: 0, ticks: { color: '#9ca3af' }, grid: { color: '#33334d' } } }, plugins: { legend: { labels: { color: '#e2e8f0', usePointStyle: true } } } } }); } + let prevGlobalTime = null; async function fetchGlobalStats() { @@ -62,6 +69,33 @@ if (window.APP.isLoggedIn && window.APP.nodeCount > 0) { if(document.getElementById('stat-vms-run')) document.getElementById('stat-vms-run').innerText = d.cluster_stats.vms_running; if(document.getElementById('stat-vms-stop')) document.getElementById('stat-vms-stop').innerText = d.cluster_stats.vms_stopped; } + + if(liveChart) { + const now = new Date(); + const timeStr = now.getHours().toString().padStart(2, '0') + ':' + now.getMinutes().toString().padStart(2, '0') + ':' + now.getSeconds().toString().padStart(2, '0'); + liveChart.data.labels.push(timeStr); + liveChart.data.datasets[0].data.push(d.cpu_percent); + liveChart.data.datasets[1].data.push(ramPercent.toFixed(1)); + if (liveChart.data.labels.length > 15) { liveChart.data.labels.shift(); liveChart.data.datasets[0].data.shift(); liveChart.data.datasets[1].data.shift(); } + liveChart.update(); + + if(liveNetChart && d.nodes_net) { + const nowTs = Date.now(); + if (prevGlobalTime !== null) { + liveNetChart.data.labels.push(timeStr); + if (liveNetChart.data.labels.length > 15) liveNetChart.data.labels.shift(); + const colors = ['#10b981', '#8b5cf6', '#f59e0b', '#ef4444', '#06b6d4']; + d.nodes_net.forEach((n, idx) => { + let rxSpeed = n.netin / (1024 * 1024); let txSpeed = n.netout / (1024 * 1024); let totalSpeed = (rxSpeed + txSpeed).toFixed(2); + let ds = liveNetChart.data.datasets.find(ds => ds.label === n.name); + if (!ds) { const c = colors[idx % colors.length]; ds = { label: n.name, borderColor: c, backgroundColor: c + '1a', borderWidth: 2, tension: 0.4, fill: true, data: new Array(Math.max(0, liveNetChart.data.labels.length - 1)).fill(0) }; liveNetChart.data.datasets.push(ds); } + ds.data.push(totalSpeed); if (ds.data.length > 15) ds.data.shift(); + }); + liveNetChart.update(); + } + prevGlobalTime = nowTs; + } + } } } catch (err) {} } @@ -267,17 +301,12 @@ if (window.APP.isLoggedIn && window.APP.nodeCount > 0) { window.checkCronAction = function() { const action = document.getElementById('cronAction').value; const vmBlock = document.getElementById('cronVmBlock'); if (action.includes('_vm')) vmBlock.classList.remove('hidden'); else vmBlock.classList.add('hidden'); } async function loadCronJobs() { const tbody = document.getElementById('cronTableBody'); tbody.innerHTML = 'Lade Jobs...'; try { const res = await (await fetch('api.php?action=get_cron_jobs')).json(); if (res.success) { tbody.innerHTML = ''; if(res.data.length === 0) { tbody.innerHTML = 'Keine geplanten Jobs.'; return; } res.data.forEach(job => { const lastRun = formatDate(job.last_run); const isActive = parseInt(job.is_active) === 1; const statusDot = isActive ? ' Aktiv' : ' Pausiert'; const targetStr = job.action_type.includes('_vm') ? `VM ${job.target_vmid}` : 'Gesamter Host'; let actionStr = job.action_type; if(actionStr === 'reboot_node') actionStr = 'Host Reboot'; if(actionStr === 'start_vm') actionStr = 'VM Start'; if(actionStr === 'stop_vm') actionStr = 'VM Stop'; if(actionStr === 'reboot_vm') actionStr = 'VM Reboot'; tbody.innerHTML += `
${statusDot}

${job.name}

${actionStr}

${job.node_name}

Ziel: ${targetStr}

${job.cron_schedule}${lastRun}`; }); } } catch(e) {} } - // NEU: Automatischer Cron-Builder im Hintergrund! const addCronForm = document.getElementById('addCronJobForm'); if(addCronForm) { addCronForm.addEventListener('submit', async function(e) { e.preventDefault(); const btn = this.querySelector('button[type="submit"]'); const oTxt = btn.innerText; btn.innerText = 'Speichere...'; - - const timeVal = document.getElementById('cronTime').value; // z.B. "03:00" - const daysVal = document.getElementById('cronDays').value; // z.B. "*" - const [hour, minute] = timeVal.split(':'); - const generatedCronStr = `${parseInt(minute, 10)} ${parseInt(hour, 10)} * * ${daysVal}`; - + const timeVal = document.getElementById('cronTime').value; const daysVal = document.getElementById('cronDays').value; + const [hour, minute] = timeVal.split(':'); const generatedCronStr = `${parseInt(minute, 10)} ${parseInt(hour, 10)} * * ${daysVal}`; const fd = new FormData(); fd.append('name', document.getElementById('cronName').value); fd.append('node_id', document.getElementById('cronNodeId').value); fd.append('action_type', document.getElementById('cronAction').value); fd.append('target_vmid', document.getElementById('cronTargetVmid').value); fd.append('cron_schedule', generatedCronStr); try { const res = await (await fetch('api.php?action=add_cron_job', {method: 'POST', body: fd})).json(); if(res.success) { addCronForm.reset(); document.getElementById('cronTime').value = '03:00'; checkCronAction(); loadCronJobs(); } else alert('Fehler.'); } catch(e) {} finally { btn.innerText = oTxt; } });