diff --git a/src/api_pve.php b/src/api_pve.php index e67051b..7186280 100644 --- a/src/api_pve.php +++ b/src/api_pve.php @@ -1,6 +1,6 @@ query("SELECT * FROM nodes WHERE type = 'pve'"); @@ -10,35 +10,24 @@ if ($action === 'get_stats') { '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 = []; - + $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) { 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; - + $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; - } + 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]; @@ -46,7 +35,6 @@ if ($action === 'get_stats') { } } } - $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) { @@ -54,11 +42,7 @@ if ($action === 'get_stats') { 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 (isset($vm['status']) && $vm['status'] === 'running') $data['cluster_stats']['vms_running']++; else $data['cluster_stats']['vms_stopped']++; } } } @@ -75,11 +59,7 @@ if ($action === 'get_top_vms') { 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; + $seenVms[$uniqueId] = true; $vm['node_id'] = $node['id']; $vm['node_ip'] = $node['ip_address']; $vm['host'] = $vm['node'] ?? 'unknown'; $allVms[] = $vm; } } } @@ -96,11 +76,7 @@ if ($action === 'get_all_vms') { 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; + $seenVms[$uniqueId] = true; $vm['node_id'] = $node['id']; $vm['node_ip'] = $node['ip_address']; $vm['host'] = $vm['node'] ?? 'unknown'; $allVms[] = $vm; } } } @@ -109,6 +85,24 @@ if ($action === 'get_all_vms') { echo json_encode(['success' => true, 'data' => $allVms]); exit; } +// NEU: Node Dashboard Daten (VMs & Storage) +if ($action === 'get_node_dashboard') { + if (!$isAdmin) exit; + $stmt = $pdo->prepare("SELECT * FROM nodes WHERE id = ?"); $stmt->execute([$_GET['node_id']]); $node = $stmt->fetch(PDO::FETCH_ASSOC); + $host = $_GET['host']; + + $vmsQemu = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$host}/qemu"); + $vmsLxc = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$host}/lxc"); + $storages = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$host}/storage"); + + $nodeVms = []; + if (isset($vmsQemu['data'])) { foreach ($vmsQemu['data'] as $v) { $v['type'] = 'qemu'; $v['host'] = $host; $v['node_id'] = $node['id']; $v['node_ip'] = $node['ip_address']; $nodeVms[] = $v; } } + if (isset($vmsLxc['data'])) { foreach ($vmsLxc['data'] as $v) { $v['type'] = 'lxc'; $v['host'] = $host; $v['node_id'] = $node['id']; $v['node_ip'] = $node['ip_address']; $nodeVms[] = $v; } } + usort($nodeVms, 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, 'vms' => $nodeVms, 'storages' => $storages['data'] ?? []]); 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); @@ -118,59 +112,26 @@ if ($action === 'vm_action') { } 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; - + $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']; - + 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']; + 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]); + $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; } @@ -179,8 +140,7 @@ if ($action === 'get_users') { if (!$isAdmin) exit; $stmt = $pdo->query("SELECT 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']]); + $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; } @@ -221,6 +181,7 @@ if ($action === 'get_vm_config') { echo json_encode(['success' => true, 'data' => $res['data'] ?? []]); exit; } +// ANGEPASST: Update inkl. ISO und onboot 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); @@ -228,6 +189,9 @@ if ($action === 'update_vm_config') { if(isset($_POST['memory'])) $params['memory'] = $_POST['memory']; if(isset($_POST['cores'])) $params['cores'] = $_POST['cores']; if(isset($_POST['net0'])) $params['net0'] = $_POST['net0']; + if(isset($_POST['onboot'])) $params['onboot'] = $_POST['onboot']; + if(isset($_POST['ide2'])) $params['ide2'] = $_POST['ide2'] === 'none' ? 'none,media=cdrom' : $_POST['ide2'] . ',media=cdrom'; + $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; @@ -239,7 +203,6 @@ if ($action === 'add_vm_nic') { $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; } @@ -247,7 +210,6 @@ 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; } @@ -255,10 +217,47 @@ 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; } +// NEU: ISO Images abrufen +if ($action === 'get_isos') { + if (!$isAdmin) 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"); + $isos = []; + if (isset($stRes['data'])) { + foreach ($stRes['data'] as $st) { + if (strpos($st['content'], 'iso') !== false) { + $cRes = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$_GET['host']}/storage/{$st['storage']}/content"); + if (isset($cRes['data'])) { + foreach ($cRes['data'] as $item) { + if ($item['content'] === 'iso') $isos[] = $item['volid']; + } + } + } + } + } + echo json_encode(['success' => true, 'data' => $isos]); exit; +} + +// NEU: Node-Spezifische Backup Jobs +if ($action === 'get_node_backup_jobs') { + if (!$isAdmin) 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/cluster/backup"); + $hostJobs = []; + if (isset($res['data'])) { + foreach ($res['data'] as $job) { + // Zeige nur Jobs, die diesen Host betreffen (oder alle, falls node nicht spezifiziert in PVE) + if (empty($job['node']) || $job['node'] === $_GET['host']) { + $hostJobs[] = $job; + } + } + } + echo json_encode(['success' => true, 'data' => $hostJobs]); 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); @@ -273,7 +272,6 @@ if ($action === 'vm_snapshot_action') { 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; } @@ -306,7 +304,6 @@ 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; } @@ -316,7 +313,6 @@ if ($action === 'restore_backup') { $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; } @@ -334,19 +330,10 @@ if ($action === 'get_node_status' || $action === 'get_vm_status') { $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; - } + 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; - + $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"; @@ -355,26 +342,19 @@ if ($action === 'get_node_status' || $action === 'get_vm_status') { } } -// === RRD Historie (24h / 7 Tage) für alle Node Typen === if ($action === 'get_historical_rrd') { 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); - $timeframe = $_GET['timeframe'] === 'week' ? 'week' : 'day'; - $type = $node['type'] ?? 'pve'; - + $timeframe = $_GET['timeframe'] === 'week' ? 'week' : 'day'; $type = $node['type'] ?? 'pve'; if ($_GET['target_mode'] === 'node') { $nData = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes", $type); $internalName = $nData['data'][0]['node'] ?? 'localhost'; $endpoint = "/api2/json/nodes/{$internalName}/rrddata?timeframe={$timeframe}&cf=AVERAGE"; - } else { - $endpoint = "/api2/json/nodes/{$_GET['host']}/{$_GET['target_mode']}/{$_GET['vmid']}/rrddata?timeframe={$timeframe}&cf=AVERAGE"; - } - + } else { $endpoint = "/api2/json/nodes/{$_GET['host']}/{$_GET['target_mode']}/{$_GET['vmid']}/rrddata?timeframe={$timeframe}&cf=AVERAGE"; } $res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], $endpoint, $type); echo json_encode(['success' => true, 'data' => $res['data'] ?? []]); exit; } -// === APT Update Manager === if ($action === 'get_update_details') { if (!$isAdmin) exit; $stmt = $pdo->query("SELECT * FROM nodes WHERE type != 'pmg'"); $updateList = []; @@ -384,9 +364,7 @@ if ($action === 'get_update_details') { foreach ($nData['data'] as $nInfo) { $internalName = $nInfo['node'] ?? 'localhost'; $apt = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$internalName}/apt/update", $node['type']); - if (isset($apt['data']) && count($apt['data']) > 0) { - $updateList[] = [ 'node_id' => $node['id'], 'host' => $internalName, 'display_name' => $node['name'] . ' (' . $internalName . ')', 'type' => $node['type'], 'packages' => $apt['data'] ]; - } + if (isset($apt['data']) && count($apt['data']) > 0) { $updateList[] = [ 'node_id' => $node['id'], 'host' => $internalName, 'display_name' => $node['name'] . ' (' . $internalName . ')', 'type' => $node['type'], 'packages' => $apt['data'] ]; } } } } @@ -397,7 +375,6 @@ if ($action === 'trigger_apt_upgrade') { 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']}/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 diff --git a/src/app.js b/src/app.js index c9e6de7..9326736 100644 --- a/src/app.js +++ b/src/app.js @@ -1,5 +1,4 @@ // /home/docker/pve_dashboard/src/app.js - function formatBytes(bytes) { if (!bytes || bytes === 0) return '0 GB'; const k = 1024, sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'], i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } function formatDate(timestamp) { if (!timestamp || timestamp === 0) return 'Nie'; const d = new Date(timestamp * 1000); return d.toLocaleDateString('de-DE') + ' ' + d.toLocaleTimeString('de-DE'); } @@ -38,11 +37,81 @@ if (window.APP.isLoggedIn && window.APP.nodeCount > 0) { }); } + // NEU: Dynamische Sidebar für PVE Nodes + async function loadSidebarNodes() { + const container = document.getElementById('sidebar-pve-nodes'); if(!container) return; + try { + const res = await (await fetch('api.php?action=get_pve_nodes')).json(); + if(res.success && res.data) { + container.innerHTML = ''; + res.data.forEach(n => { + container.innerHTML += `🖥️ ${n.display}`; + }); + } + } catch(e) {} + } + loadSidebarNodes(); + window.switchTab = function(tab) { - ['pve', 'pbs', 'pmg'].forEach(t => { const el = document.getElementById('tab-' + t); const nav = document.getElementById('nav-tab-' + t); if (t === tab) { el.classList.remove('hidden'); setTimeout(() => el.classList.remove('opacity-0'), 50); nav.classList.add('tab-active' + (t === 'pve' ? '' : '-' + t)); } else { el.classList.add('hidden', 'opacity-0'); nav.classList.remove('tab-active', 'tab-active-pbs', 'tab-active-pmg'); } }); + ['pve', 'pbs', 'pmg', 'node-view'].forEach(t => { + const el = document.getElementById('tab-' + t); + const nav = document.getElementById('nav-tab-' + t); + if (t === tab) { + if(el) { el.classList.remove('hidden'); setTimeout(() => el.classList.remove('opacity-0'), 50); } + if(nav) nav.classList.add('tab-active' + (t==='pve'?'':(t==='pbs'?'-pbs':(t==='pmg'?'-pmg':'')))); + } else { + if(el) el.classList.add('hidden', 'opacity-0'); + if(nav) nav.classList.remove('tab-active', 'tab-active-pbs', 'tab-active-pmg'); + } + }); + // Reset Sidebar Highlights for nodes + document.querySelectorAll('[id^="nav-node-"]').forEach(el => el.classList.remove('text-white', 'font-bold')); + if(tab === 'pbs') fetchPbsStats(); if(tab === 'pmg') fetchPmgStats(); } + // NEU: Node View (Spezifischer Host) + window.openNodeView = async function(nodeId, host) { + switchTab('node-view'); + document.querySelectorAll('[id^="nav-node-"]').forEach(el => el.classList.remove('text-white', 'font-bold')); + document.getElementById(`nav-node-${host}`).classList.add('text-white', 'font-bold'); + document.getElementById('nodeViewTitle').innerText = 'Host: ' + host; + document.getElementById('nodeViewHeaderBtn').onclick = () => openLiveGraph('node', 0, host, nodeId, host); + document.getElementById('nodeViewBackupBtn').onclick = () => openNodeBackups(nodeId, host); + + // Vorbelegung für "Neue VM" Modal + window.currentSelectedNodeId = nodeId; window.currentSelectedHost = host; + + const storContainer = document.getElementById('nodeStoragesContainer'); storContainer.innerHTML = 'Lade...'; + const vmsBody = document.getElementById('nodeVmsTableBody'); vmsBody.innerHTML = 'Lade VMs...'; + + try { + const res = await (await fetch(`api.php?action=get_node_dashboard&node_id=${nodeId}&host=${host}`)).json(); + if(res.success) { + // Storages rendern + storContainer.innerHTML = ''; + res.storages.forEach(st => { + const pct = st.total > 0 ? ((st.used / st.total) * 100).toFixed(1) : 0; + storContainer.innerHTML += `
+
${st.storage}${pct}%
+
+
${st.content}
`; + }); + + // VMs rendern + vmsBody.innerHTML = ''; + if(res.vms.length === 0) vmsBody.innerHTML = 'Keine VMs auf diesem Host.'; + res.vms.forEach(vm => { + const isRunning = vm.status === 'running'; const typeStr = vm.type === 'lxc' ? '📦 LXC' : '🖥️ QEMU'; + const statusBadge = isRunning ? 'Online' : 'Offline'; + let actionButtons = isRunning ? `` : ``; + actionButtons += ``; + vmsBody.innerHTML += `${vm.vmid}${vm.name}${typeStr}${statusBadge}${actionButtons}`; + }); + } + } catch(e) {} + } + 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 } } } } }); } @@ -50,9 +119,8 @@ if (window.APP.isLoggedIn && window.APP.nodeCount > 0) { 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() { - if(!document.getElementById('stat-cpu-text')) return; + if(document.getElementById('tab-pve').classList.contains('hidden')) return; try { const res = await (await fetch('api.php?action=get_stats')).json(); if(res.success && res.data) { @@ -70,19 +138,14 @@ if (window.APP.isLoggedIn && window.APP.nodeCount > 0) { } 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(); + 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(); + 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); @@ -91,15 +154,14 @@ if (window.APP.isLoggedIn && window.APP.nodeCount > 0) { ds.data.push(totalSpeed); if (ds.data.length > 15) ds.data.shift(); }); liveNetChart.update(); - } - prevGlobalTime = nowTs; + } prevGlobalTime = nowTs; } } } } catch (err) {} } - async function fetchTopVms() { if(!document.getElementById('top-vms-container')) return; try { const res = await (await fetch('api.php?action=get_top_vms')).json(); if(res.success && res.data) { const container = document.getElementById('top-vms-container'); container.innerHTML = ''; if(res.data.length === 0) { container.innerHTML = '

Keine aktiven VMs.

'; return; } res.data.forEach((vm, i) => { const cpuPercent = ((vm.cpu || 0) * 100).toFixed(1); const ramUsed = formatBytes(vm.mem || 0); const icon = vm.type === 'lxc' ? '📦' : '🖥️'; const numberColor = i === 0 ? 'text-red-500' : (i === 1 ? 'text-orange-400' : (i === 2 ? 'text-yellow-400' : 'text-gray-400')); container.innerHTML += `
#${i + 1}

${icon} ${vm.name}

Host: ${vm.host}

${cpuPercent}% CPU

${ramUsed} RAM

`; }); } } catch (err) {} } + async function fetchTopVms() { if(!document.getElementById('top-vms-container') || document.getElementById('tab-pve').classList.contains('hidden')) return; try { const res = await (await fetch('api.php?action=get_top_vms')).json(); if(res.success && res.data) { const container = document.getElementById('top-vms-container'); container.innerHTML = ''; if(res.data.length === 0) { container.innerHTML = '

Keine aktiven VMs.

'; return; } res.data.forEach((vm, i) => { const cpuPercent = ((vm.cpu || 0) * 100).toFixed(1); const ramUsed = formatBytes(vm.mem || 0); const icon = vm.type === 'lxc' ? '📦' : '🖥️'; const numberColor = i === 0 ? 'text-red-500' : (i === 1 ? 'text-orange-400' : (i === 2 ? 'text-yellow-400' : 'text-gray-400')); container.innerHTML += `
#${i + 1}

${icon} ${vm.name}

Host: ${vm.host}

${cpuPercent}% CPU

${ramUsed} RAM

`; }); } } catch (err) {} } async function fetchRecentJobs() { if(!document.getElementById('recent-jobs-container')) return; try { const res = await (await fetch('api.php?action=get_recent_jobs')).json(); if(res.success && res.data) { const container = document.getElementById('recent-jobs-container'); container.innerHTML = ''; if(res.data.length === 0) { container.innerHTML = '

Keine aktuellen Jobs.

'; return; } res.data.forEach(job => { const jobTypeStr = job.type || job.worker_type || 'unknown'; let statusColor = 'text-gray-400', statusIcon = '⏳', statusText = job.status || 'running...'; if(statusText.toLowerCase() === 'ok') { statusColor = 'text-green-500'; statusIcon = '✅'; } else if(statusText !== 'running...') { statusColor = 'text-red-500'; statusIcon = '❌'; } else { statusColor = 'text-blue-400'; statusIcon = '🔄'; } const date = new Date(job.starttime * 1000); const timeStr = date.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' }), dateStr = date.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' }); const isBackup = jobTypeStr.includes('sync') || jobTypeStr.includes('prune') || jobTypeStr.includes('garbage_collection') || jobTypeStr.includes('vzdump') || jobTypeStr.includes('verify'); const jobTypeColor = isBackup ? 'text-purple-400' : 'text-white'; container.innerHTML += `
${statusIcon}

${jobTypeStr}

Host: ${job.node_name}

${statusText}

${dateStr} - ${timeStr}

`; }); } } catch (err) {} } window.fetchUpdates = async function() { if(!document.getElementById('stat-updates-text')) return; try { const res = await (await fetch('api.php?action=get_updates')).json(); if(res.success) { const el = document.getElementById('stat-updates-text'), subEl = document.getElementById('stat-updates-sub'); if(res.total === 0) { el.innerText = '0 Updates'; el.className = 'text-2xl font-bold text-green-500 mt-1'; subEl.innerText = 'Alle Systeme sind aktuell.'; } else { el.innerText = res.total + ' Updates'; el.className = 'text-2xl font-bold text-red-500 mt-1'; subEl.innerText = 'Auf: ' + res.details; } } } catch (err) {} } @@ -107,9 +169,6 @@ if (window.APP.isLoggedIn && window.APP.nodeCount > 0) { window.closeUpdateManager = function() { document.getElementById('updateManagerModal').classList.add('hidden'); fetchUpdates(); } window.triggerAptUpgrade = async function(nodeId, host) { if(!confirm(`Updates auf ${host} jetzt installieren?`)) return; const fd = new FormData(); fd.append('node_id', nodeId); fd.append('host', host); try { const res = await (await fetch('api.php?action=trigger_apt_upgrade', {method: 'POST', body: fd})).json(); if(res.success && res.upid) { openTaskLog(res.upid, nodeId, host); } else alert('Fehler: ' + res.error); } catch(e) {} } - async function fetchPbsStats() { if(document.getElementById('tab-pbs').classList.contains('hidden')) return; try { const res = await (await fetch('api.php?action=get_pbs_stats')).json(); if(res.success) { const container = document.getElementById('pbs-datastores-container'); if(res.data.length === 0) { container.innerHTML = '
Keine PBS Server gefunden.
'; return; } container.innerHTML = ''; res.data.forEach(ds => { const total = ds.total || 0; const used = ds.used || 0; const percent = total > 0 ? ((used / total) * 100).toFixed(1) : 0; const colorClass = percent > 85 ? 'bg-red-500' : (percent > 70 ? 'bg-orange-500' : 'bg-pbs'); container.innerHTML += `

${ds.store}

PBS Host: ${ds.host}

Auslastung${percent}%
Used: ${formatBytes(used)}Total: ${formatBytes(total)}
`; }); } } catch(e) {} } - async function fetchPmgStats() { if(document.getElementById('tab-pmg').classList.contains('hidden')) return; try { const res = await (await fetch('api.php?action=get_pmg_stats')).json(); if(res.success) { const container = document.getElementById('pmg-nodes-container'); if(res.data.length === 0) { container.innerHTML = '
Keine Mail Gateways angebunden.
'; return; } container.innerHTML = ''; res.data.forEach(pmg => { const cpuPercent = ((pmg.cpu || 0) * 100).toFixed(1); const ramPercent = pmg.memory && pmg.memory.total ? ((pmg.memory.used / pmg.memory.total) * 100).toFixed(1) : 0; const diskPercent = pmg.rootfs && pmg.rootfs.total ? ((pmg.rootfs.used / pmg.rootfs.total) * 100).toFixed(1) : 0; container.innerHTML += `

${pmg.host}

Online

CPU${cpuPercent}%
RAM${ramPercent}%
System Disk${diskPercent}%
`; }); } } catch(e) {} } - fetchGlobalStats(); setInterval(fetchGlobalStats, 10000); fetchTopVms(); setInterval(fetchTopVms, 10000); fetchRecentJobs(); setInterval(fetchRecentJobs, 15000); fetchUpdates(); setInterval(fetchUpdates, 60000); setInterval(fetchPbsStats, 10000); setInterval(fetchPmgStats, 10000); const nodeModal = document.getElementById('nodeManagerModal'); @@ -120,56 +179,54 @@ if (window.APP.isLoggedIn && window.APP.nodeCount > 0) { window.deleteNode = async function(id, name) { if(!confirm(`Server '${name}' löschen?`)) return; const fd = new FormData(); fd.append('id', id); await fetch('api.php?action=delete_node', { method: 'POST', body: fd }); loadNodesIntoTable(); } const addNewNodeForm = document.getElementById('addNewNodeForm'); - if(addNewNodeForm) { addNewNodeForm.addEventListener('submit', async function(e) { e.preventDefault(); const btn = this.querySelector('button[type="submit"]'); const oTxt = btn.innerText; btn.innerText = 'Verbinde...'; const fd = new FormData(); fd.append('name', document.getElementById('newNodeName').value); fd.append('ip', document.getElementById('newNodeIp').value); fd.append('user', document.getElementById('newNodeUser').value); fd.append('pass', document.getElementById('newNodePass').value); fd.append('type', document.getElementById('newNodeType').value); try { const res = await (await fetch('api.php?action=add_node', { method: 'POST', body: fd })).json(); if(res.success) { addNewNodeForm.reset(); toggleAddNodeForm(); loadNodesIntoTable(); alert('Erfolgreich angebunden!'); } else alert(res.error); } catch(err) {} btn.innerText = oTxt; }); } - - const userModal = document.getElementById('userManagerModal'); - window.openUserManager = function() { userModal.classList.remove('hidden'); loadUsersIntoTable(); } - window.closeUserManager = function() { userModal.classList.add('hidden'); } - async function loadUsersIntoTable() { try { const res = await (await fetch('api.php?action=get_users')).json(); if(res.success) { const tbody = document.getElementById('userTableBody'); tbody.innerHTML = ''; res.data.forEach(user => { const isMe = user.username === window.APP.username; const deleteBtn = isMe ? `Das bist du` : ``; let roleBadge = user.role === 'admin' ? 'Admin' : (user.role === 'vm_manager' ? 'VM Manager' : 'Viewer'); tbody.innerHTML += `${user.username}${roleBadge}${deleteBtn}`; }); } } catch (err) {} } - window.handleRoleChange = async function() { const role = document.getElementById('newUserRole').value; const vmBlock = document.getElementById('vmPermissionsBlock'); if (role === 'vm_manager') { vmBlock.classList.remove('hidden'); try { const res = await (await fetch('api.php?action=get_top_vms')).json(); if (res.success && res.data) { const list = document.getElementById('vmCheckboxList'); list.innerHTML = ''; res.data.forEach(vm => { list.innerHTML += ``; }); } } catch(e) {} } else vmBlock.classList.add('hidden'); } - const addNewUserForm = document.getElementById('addNewUserForm'); - if(addNewUserForm) { addNewUserForm.addEventListener('submit', async function(e) { e.preventDefault(); const btn = this.querySelector('button[type="submit"]'); const oTxt = btn.innerText; btn.innerText = 'Speichere...'; const fd = new FormData(); fd.append('username', document.getElementById('newUsername').value); fd.append('password', document.getElementById('newUserPass').value); const role = document.getElementById('newUserRole').value; fd.append('role', role); let permissions = {}; if (role === 'vm_manager') { let allowedVms = []; document.querySelectorAll('input[name="allowed_vms[]"]:checked').forEach(cb => allowedVms.push(cb.value)); permissions.allowed_vms = allowedVms; } fd.append('permissions', JSON.stringify(permissions)); const res = await (await fetch('api.php?action=create_user', { method: 'POST', body: fd })).json(); if(res.success) { addNewUserForm.reset(); handleRoleChange(); loadUsersIntoTable(); } else alert('Fehler.'); btn.innerText = oTxt; }); } - window.deleteUser = async function(id, name) { if(!confirm(`Benutzer '${name}' löschen?`)) return; const fd = new FormData(); fd.append('id', id); await fetch('api.php?action=delete_user', { method: 'POST', body: fd }); loadUsersIntoTable(); } - - const vmModal = document.getElementById('vmManagerModal'); - window.openVmManager = function() { vmModal.classList.remove('hidden'); loadVmsIntoTable(); } - window.closeVmManager = function() { vmModal.classList.add('hidden'); } - async function loadVmsIntoTable() { - const tbody = document.getElementById('vmTableBody'); tbody.innerHTML = 'Lade VMs...'; - try { - const res = await (await fetch('api.php?action=get_all_vms')).json(); - if(res.success) { - tbody.innerHTML = ''; if(res.data.length === 0) { tbody.innerHTML = 'Keine VMs.'; return; } - res.data.forEach(vm => { - const isRunning = vm.status === 'running'; const typeStr = vm.type === 'lxc' ? '📦 LXC' : '🖥️ QEMU'; - const statusBadge = isRunning ? ' Running' : ' Stopped'; - let actionButtons = isRunning ? `` : ``; - if(isRunning) { - actionButtons += ``; - actionButtons += ``; - } - actionButtons += ``; - tbody.innerHTML += `
${vm.vmid}${vm.name}
${typeStr} - ${vm.host}${vm.maxcpu||1} Cores / ${formatBytes(vm.maxmem||0)}${statusBadge}${actionButtons}`; - }); - } - } catch (err) {} - } - window.sendVmCommand = async function(vmid, host, type, cmd, nodeId) { if(!confirm(`Maschine '${vmid}' wirklich ${cmd}?`)) return; const fd = new FormData(); fd.append('vmid', vmid); fd.append('host', host); fd.append('type', type); fd.append('cmd', cmd); fd.append('node_id', nodeId); document.getElementById('vmTableBody').style.opacity = '0.5'; try { const res = await (await fetch('api.php?action=vm_action', { method: 'POST', body: fd })).json(); if(res.success) setTimeout(loadVmsIntoTable, 2000); else alert(res.error); } catch (err) {} finally { document.getElementById('vmTableBody').style.opacity = '1'; } } + if(addNewNodeForm) { addNewNodeForm.addEventListener('submit', async function(e) { e.preventDefault(); const btn = this.querySelector('button[type="submit"]'); const oTxt = btn.innerText; btn.innerText = 'Verbinde...'; const fd = new FormData(); fd.append('name', document.getElementById('newNodeName').value); fd.append('ip', document.getElementById('newNodeIp').value); fd.append('user', document.getElementById('newNodeUser').value); fd.append('pass', document.getElementById('newNodePass').value); fd.append('type', document.getElementById('newNodeType').value); try { const res = await (await fetch('api.php?action=add_node', { method: 'POST', body: fd })).json(); if(res.success) { addNewNodeForm.reset(); toggleAddNodeForm(); loadNodesIntoTable(); loadSidebarNodes(); alert('Erfolgreich angebunden!'); } else alert(res.error); } catch(err) {} btn.innerText = oTxt; }); } + // VM Erstellung (jetzt mit automatischer Node-Vorauswahl aus dem Sidebar-View) const createVmModal = document.getElementById('createVmModal'); - window.openCreateVm = async function() { createVmModal.classList.remove('hidden'); const select = document.getElementById('createVmHost'); select.innerHTML = ''; try { const res = await (await fetch('api.php?action=get_pve_nodes')).json(); if (res.success && res.data.length > 0) { select.innerHTML = ''; res.data.forEach(node => { select.innerHTML += ``; }); } else { select.innerHTML = ''; } } catch(e) {} } + window.openCreateVm = async function() { + createVmModal.classList.remove('hidden'); + const select = document.getElementById('createVmHost'); select.innerHTML = ''; + try { + const res = await (await fetch('api.php?action=get_pve_nodes')).json(); + if (res.success && res.data.length > 0) { + select.innerHTML = ''; + res.data.forEach(node => { + const isSelected = (window.currentSelectedHost && node.host === window.currentSelectedHost) ? 'selected' : ''; + select.innerHTML += ``; + }); + } else { select.innerHTML = ''; } + } catch(e) {} + } window.closeCreateVm = function() { createVmModal.classList.add('hidden'); } const createVmForm = document.getElementById('createVmForm'); - if(createVmForm) { createVmForm.addEventListener('submit', async function(e) { e.preventDefault(); const hostDataStr = document.getElementById('createVmHost').value; if(!hostDataStr) return; const hostData = JSON.parse(hostDataStr); const btn = this.querySelector('button[type="submit"]'); const oTxt = btn.innerText; btn.innerText = 'Richte VM ein...'; const fd = new FormData(); fd.append('node_id', hostData.id); fd.append('host', hostData.host); fd.append('name', document.getElementById('createVmName').value); fd.append('memory', document.getElementById('createVmRam').value); fd.append('cores', document.getElementById('createVmCores').value); try { const res = await (await fetch('api.php?action=create_vm', { method: 'POST', body: fd })).json(); if(res.success) { alert(`Erfolgreich! VM-ID: ${res.vmid}`); createVmForm.reset(); closeCreateVm(); loadVmsIntoTable(); } else alert(res.error); } catch(e) {} finally { btn.innerText = oTxt; } }); } + if(createVmForm) { createVmForm.addEventListener('submit', async function(e) { e.preventDefault(); const hostDataStr = document.getElementById('createVmHost').value; if(!hostDataStr) return; const hostData = JSON.parse(hostDataStr); const btn = this.querySelector('button[type="submit"]'); const oTxt = btn.innerText; btn.innerText = 'Richte VM ein...'; const fd = new FormData(); fd.append('node_id', hostData.id); fd.append('host', hostData.host); fd.append('name', document.getElementById('createVmName').value); fd.append('memory', document.getElementById('createVmRam').value); fd.append('cores', document.getElementById('createVmCores').value); try { const res = await (await fetch('api.php?action=create_vm', { method: 'POST', body: fd })).json(); if(res.success) { alert(`Erfolgreich! VM-ID: ${res.vmid}`); createVmForm.reset(); closeCreateVm(); if(window.currentSelectedHost === hostData.host) openNodeView(hostData.id, hostData.host); } else alert(res.error); } catch(e) {} finally { btn.innerText = oTxt; } }); } + // VM EDIT MODAL (Erweitert um ISO & OnBoot) const settingsModal = document.getElementById('vmSettingsModal'); window.openVmSettings = async function(vmid, host, type, nodeId, name) { settingsModal.classList.remove('hidden'); document.getElementById('settingsModalTitle').innerText = 'Einstellungen: ' + name; document.getElementById('setVmid').value = vmid; document.getElementById('setHost').value = host; document.getElementById('setType').value = type; document.getElementById('setNodeId').value = nodeId; document.getElementById('snapshotListContainer').innerHTML = '
Lade Snapshots...
'; document.getElementById('backupListContainer').innerHTML = '
Lade Backups...
'; + + // Lade ISOs + if(type === 'qemu') { + document.getElementById('setIso').innerHTML = ''; + fetch(`api.php?action=get_isos&node_id=${nodeId}&host=${host}`).then(r=>r.json()).then(isoRes => { + const s = document.getElementById('setIso'); s.innerHTML = ''; + if(isoRes.success) { isoRes.data.forEach(iso => s.innerHTML += ``); } + }); + } + try { const res = await (await fetch(`api.php?action=get_vm_config&vmid=${vmid}&host=${host}&type=${type}&node_id=${nodeId}`)).json(); - if (res.success && res.data) { const cfg = res.data; document.getElementById('setMemory').value = cfg.memory || ''; document.getElementById('setCores').value = cfg.cores || 1; if (cfg.net0) { document.getElementById('setRawNet0').value = cfg.net0; if (cfg.net0.includes('link_down=1')) { document.getElementById('netStatusBadge').className = 'px-2 py-1 rounded text-xs font-bold text-red-400 bg-red-500/20'; document.getElementById('netStatusBadge').innerText = 'Getrennt'; document.getElementById('btnToggleNet').innerText = 'Kabel einstecken'; document.getElementById('btnToggleNet').onclick = () => saveNetwork(false); } else { document.getElementById('netStatusBadge').className = 'px-2 py-1 rounded text-xs font-bold text-green-400 bg-green-500/20'; document.getElementById('netStatusBadge').innerText = 'Verbunden'; document.getElementById('btnToggleNet').innerText = 'Kabel ziehen'; document.getElementById('btnToggleNet').onclick = () => saveNetwork(true); } } else { document.getElementById('netStatusBadge').innerText = 'Kein net0'; document.getElementById('btnToggleNet').style.display = 'none'; } let diskName = ''; if (type === 'lxc' && cfg.rootfs) diskName = 'rootfs'; else if (cfg.scsi0) diskName = 'scsi0'; else if (cfg.virtio0) diskName = 'virtio0'; else if (cfg.ide0) diskName = 'ide0'; if (diskName) { document.getElementById('setPrimaryDisk').value = diskName; document.getElementById('diskLabelName').innerText = diskName; } else document.getElementById('diskLabelName').innerText = 'Nicht gefunden'; } + if (res.success && res.data) { + const cfg = res.data; + document.getElementById('setMemory').value = cfg.memory || ''; document.getElementById('setCores').value = cfg.cores || 1; + document.getElementById('setOnboot').checked = (cfg.onboot == 1); + if(cfg.ide2 && cfg.ide2 !== 'none,media=cdrom') { const parts = cfg.ide2.split(','); document.getElementById('setIso').value = parts[0]; } + + if (cfg.net0) { document.getElementById('setRawNet0').value = cfg.net0; if (cfg.net0.includes('link_down=1')) { document.getElementById('netStatusBadge').className = 'px-2 py-1 rounded text-xs font-bold text-red-400 bg-red-500/20'; document.getElementById('netStatusBadge').innerText = 'Getrennt'; document.getElementById('btnToggleNet').innerText = 'Kabel einstecken'; document.getElementById('btnToggleNet').onclick = () => saveNetwork(false); } else { document.getElementById('netStatusBadge').className = 'px-2 py-1 rounded text-xs font-bold text-green-400 bg-green-500/20'; document.getElementById('netStatusBadge').innerText = 'Verbunden'; document.getElementById('btnToggleNet').innerText = 'Kabel ziehen'; document.getElementById('btnToggleNet').onclick = () => saveNetwork(true); } } else { document.getElementById('netStatusBadge').innerText = 'Kein net0'; document.getElementById('btnToggleNet').style.display = 'none'; } let diskName = ''; if (type === 'lxc' && cfg.rootfs) diskName = 'rootfs'; else if (cfg.scsi0) diskName = 'scsi0'; else if (cfg.virtio0) diskName = 'virtio0'; else if (cfg.ide0) diskName = 'ide0'; if (diskName) { document.getElementById('setPrimaryDisk').value = diskName; document.getElementById('diskLabelName').innerText = diskName; } else document.getElementById('diskLabelName').innerText = 'Nicht gefunden'; + } loadSnapshots(vmid, host, type, nodeId); const stRes = await (await fetch(`api.php?action=get_backup_storages&node_id=${nodeId}&host=${host}`)).json(); if (stRes.success) { const bSelect = document.getElementById('backupTargetStorage'); bSelect.innerHTML = ''; stRes.data.forEach(st => bSelect.innerHTML += ``); } @@ -177,23 +234,40 @@ if (window.APP.isLoggedIn && window.APP.nodeCount > 0) { } catch (e) {} } window.closeVmSettings = function() { settingsModal.classList.add('hidden'); } - window.saveHardwareSettings = async function() { const fd = new FormData(); fd.append('vmid', document.getElementById('setVmid').value); fd.append('host', document.getElementById('setHost').value); fd.append('type', document.getElementById('setType').value); fd.append('node_id', document.getElementById('setNodeId').value); fd.append('memory', document.getElementById('setMemory').value); fd.append('cores', document.getElementById('setCores').value); const res = await (await fetch('api.php?action=update_vm_config', { method: 'POST', body: fd })).json(); if(res.success) { alert('Gespeichert!'); loadVmsIntoTable(); } else alert(res.error); } - window.saveNetwork = async function(disconnect) { let net = document.getElementById('setRawNet0').value; if (!net) return; if (disconnect) { if (!net.includes('link_down=1')) net += ',link_down=1'; } else { net = net.replace(',link_down=1', ''); } const fd = new FormData(); fd.append('vmid', document.getElementById('setVmid').value); fd.append('host', document.getElementById('setHost').value); fd.append('type', document.getElementById('setType').value); fd.append('node_id', document.getElementById('setNodeId').value); fd.append('net0', net); const res = await (await fetch('api.php?action=update_vm_config', { method: 'POST', body: fd })).json(); if(res.success) { alert('Netzwerkstatus geändert!'); openVmSettings(fd.get('vmid'), fd.get('host'), fd.get('type'), fd.get('node_id'), document.getElementById('settingsModalTitle').innerText.replace('Einstellungen: ', '')); } } - window.addNic = async function() { const bridge = document.getElementById('addNicBridge').value.trim(); if (!bridge) return; const fd = new FormData(); fd.append('vmid', document.getElementById('setVmid').value); fd.append('host', document.getElementById('setHost').value); fd.append('type', document.getElementById('setType').value); fd.append('node_id', document.getElementById('setNodeId').value); fd.append('bridge', bridge); try { const res = await (await fetch('api.php?action=add_vm_nic', { method: 'POST', body: fd })).json(); if(res.success) { alert(`Erfolgreich in Slot: ${res.slot}`); openVmSettings(fd.get('vmid'), fd.get('host'), fd.get('type'), fd.get('node_id'), document.getElementById('settingsModalTitle').innerText.replace('Einstellungen: ', '')); } else alert(res.error); } catch(e) {} } - window.expandDisk = async function() { const gb = document.getElementById('addDiskGb').value; const disk = document.getElementById('setPrimaryDisk').value; if (!gb || gb <= 0 || !disk) return; if (!confirm(`Disk um +${gb}GB erweitern?`)) return; const fd = new FormData(); fd.append('vmid', document.getElementById('setVmid').value); fd.append('host', document.getElementById('setHost').value); fd.append('type', document.getElementById('setType').value); fd.append('node_id', document.getElementById('setNodeId').value); fd.append('disk', disk); fd.append('size', '+' + gb + 'G'); const res = await (await fetch('api.php?action=resize_vm_disk', { method: 'POST', body: fd })).json(); if(res.success) { alert('Erweitert.'); document.getElementById('addDiskGb').value = ''; } else alert(res.error); } - window.destroyVm = async function() { const vmid = document.getElementById('setVmid').value; if (prompt(`Zahl ${vmid} eingeben zum Löschen:`) !== vmid) return; const fd = new FormData(); fd.append('vmid', vmid); fd.append('host', document.getElementById('setHost').value); fd.append('type', document.getElementById('setType').value); fd.append('node_id', document.getElementById('setNodeId').value); try { const res = await (await fetch('api.php?action=delete_vm', { method: 'POST', body: fd })).json(); if(res.success) { alert('Gelöscht.'); closeVmSettings(); loadVmsIntoTable(); } else alert(res.error); } catch(e) {} } + window.saveHardwareSettings = async function() { + const fd = new FormData(); + fd.append('vmid', document.getElementById('setVmid').value); fd.append('host', document.getElementById('setHost').value); fd.append('type', document.getElementById('setType').value); fd.append('node_id', document.getElementById('setNodeId').value); + fd.append('memory', document.getElementById('setMemory').value); fd.append('cores', document.getElementById('setCores').value); + fd.append('onboot', document.getElementById('setOnboot').checked ? 1 : 0); + if(document.getElementById('setType').value === 'qemu') { fd.append('ide2', document.getElementById('setIso').value); } + + const res = await (await fetch('api.php?action=update_vm_config', { method: 'POST', body: fd })).json(); + if(res.success) { alert('Gespeichert!'); if(window.currentSelectedHost) openNodeView(fd.get('node_id'), fd.get('host')); } else alert(res.error); + } - async function loadSnapshots(vmid, host, type, nodeId) { try { const res = await (await fetch(`api.php?action=get_vm_snapshots&vmid=${vmid}&host=${host}&type=${type}&node_id=${nodeId}`)).json(); const container = document.getElementById('snapshotListContainer'); if (res.success && res.data) { const snaps = res.data.filter(s => s.name !== 'current'); if(snaps.length === 0) { container.innerHTML = '

Keine Snapshots vorhanden.

'; return; } container.innerHTML = ''; snaps.forEach(s => { const timeStr = s.snaptime ? formatDate(s.snaptime) : 'Unbekannt'; container.innerHTML += `

${s.name}

${timeStr}

`; }); } } catch(e) {} } - window.createSnapshot = async function() { const snapname = document.getElementById('newSnapName').value.trim(); if(!snapname || !snapname.match(/^[a-zA-Z0-9_-]+$/)) return alert('Bitte nur Buchstaben und Zahlen!'); actionSnapshot('create', snapname); } - window.actionSnapshot = async function(cmd, snapname) { if(cmd === 'rollback' && !confirm(`VM wird auf '${snapname}' zurückgesetzt. Fortfahren?`)) return; if(cmd === 'delete' && !confirm(`Snapshot '${snapname}' löschen?`)) return; const fd = new FormData(); fd.append('vmid', document.getElementById('setVmid').value); fd.append('host', document.getElementById('setHost').value); fd.append('type', document.getElementById('setType').value); fd.append('node_id', document.getElementById('setNodeId').value); fd.append('cmd', cmd); fd.append('snapname', snapname); try { const res = await (await fetch('api.php?action=vm_snapshot_action', { method: 'POST', body: fd })).json(); if(res.success) { alert('Aktion erfolgreich!'); document.getElementById('newSnapName').value = ''; loadSnapshots(fd.get('vmid'), fd.get('host'), fd.get('type'), fd.get('node_id')); } else alert('Fehler.'); } catch(e) {} } - - async function loadBackups(vmid, host, nodeId) { try { const res = await (await fetch(`api.php?action=get_vm_backups&vmid=${vmid}&host=${host}&node_id=${nodeId}`)).json(); const container = document.getElementById('backupListContainer'); if (res.success && res.data) { if(res.data.length === 0) { container.innerHTML = '

Keine Backups gefunden.

'; return; } container.innerHTML = ''; res.data.forEach(b => { const timeStr = b.ctime ? formatDate(b.ctime) : 'Unbekannt'; const sizeStr = formatBytes(b.size); container.innerHTML += `

${b.volid}

${timeStr} | ${sizeStr} | Storage: ${b.storage}

Restore ⤵
`; }); } } catch(e) {} } - window.createBackup = async function() { const storage = document.getElementById('backupTargetStorage').value; if(!storage) return alert('Storage auswählen!'); const fd = new FormData(); fd.append('vmid', document.getElementById('setVmid').value); fd.append('host', document.getElementById('setHost').value); fd.append('node_id', document.getElementById('setNodeId').value); fd.append('storage', storage); try { const res = await (await fetch('api.php?action=create_backup', { method: 'POST', body: fd })).json(); if(res.success) { alert('Backup gestartet! Siehe Letzte Jobs.'); } else alert('Fehler.'); } catch(e) {} } - window.restoreBackup = async function(archive) { if(!confirm(`⚠️ GEFAHRENZONE ⚠️\nSicher, dass du das Archiv '${archive}' wiederherstellen willst?\nDie VM MUSS dafür GESTOPPT sein!`)) return; const fd = new FormData(); fd.append('vmid', document.getElementById('setVmid').value); fd.append('host', document.getElementById('setHost').value); fd.append('type', document.getElementById('setType').value); fd.append('node_id', document.getElementById('setNodeId').value); fd.append('archive', archive); try { const res = await (await fetch('api.php?action=restore_backup', { method: 'POST', body: fd })).json(); if(res.success) { alert('Restore-Task erfolgreich!'); closeVmSettings(); } else alert('Fehler: Ist die VM gestoppt?'); } catch(e) {} } - - const topologyModal = document.getElementById('nodeTopologyModal'); - window.openNodeTopology = async function() { topologyModal.classList.remove('hidden'); const container = document.getElementById('nodeTopologyContainer'); container.innerHTML = '
Lade Cluster-Daten...
'; try { const [nodesRes, vmsRes] = await Promise.all([ fetch('api.php?action=get_nodes').then(r => r.json()), fetch('api.php?action=get_all_vms').then(r => r.json()) ]); if(nodesRes.success && vmsRes.success) { container.innerHTML = ''; const pveNodes = nodesRes.data.filter(n => n.type === 'pve'); if(pveNodes.length === 0) return; pveNodes.forEach(node => { const nodeVms = vmsRes.data.filter(v => v.node_id == node.id); let vmsHtml = ''; if(nodeVms.length > 0) { nodeVms.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); }); nodeVms.forEach(vm => { const icon = vm.type === 'lxc' ? '📦' : '🖥️'; const isRunning = vm.status === 'running'; const statusColor = isRunning ? 'border-green-500/30 bg-green-500/10 text-green-400' : 'border-darkborder bg-darkcard text-gray-400'; const dot = isRunning ? '' : ''; vmsHtml += `
${dot}${icon} ${vm.name}
ID: ${vm.vmid}${vm.maxcpu || 1}C / ${formatBytes(vm.maxmem || 0)}
`; }); } container.innerHTML += `

${node.name}

${node.ip_address}
${vmsHtml}
`; }); } } catch(e) {} } - window.closeNodeTopology = function() { topologyModal.classList.add('hidden'); } + // Node Backups Manager + window.openNodeBackups = async function(nodeId, host) { + document.getElementById('nodeBackupsModal').classList.remove('hidden'); + document.getElementById('nodeBackupsTitle').innerText = 'Backup Jobs: ' + host; + const tbody = document.getElementById('nodeBackupsTableBody'); tbody.innerHTML = 'Lade Jobs...'; + try { + const res = await (await fetch(`api.php?action=get_node_backup_jobs&node_id=${nodeId}&host=${host}`)).json(); + if(res.success) { + tbody.innerHTML = ''; + if(res.data.length === 0) tbody.innerHTML = 'Keine Backup Jobs definiert.'; + res.data.forEach(j => { + tbody.innerHTML += ` + ${j.id} + ${j.schedule || 'Manuell'} + ${j.storage} + ${j.vmid || 'Alle VMs'} + Read-Only via API + `; + }); + } + } catch(e) {} + } + window.closeNodeBackups = function() { document.getElementById('nodeBackupsModal').classList.add('hidden'); } // RRD UND LIVE GRAPH ENGINE let perfChartObj = null; let netChartObj = null; let liveInterval = null; let currentGraphParams = {}; @@ -202,7 +276,7 @@ if (window.APP.isLoggedIn && window.APP.nodeCount > 0) { currentGraphParams = { mode: targetMode, vmid: vmid, host: host, nodeId: nodeId, name: name }; switchGraphTab('live'); } - + window.closeLiveGraph = function() { document.getElementById('liveGraphModal').classList.add('hidden'); if(liveInterval) clearInterval(liveInterval); } window.switchGraphTab = async function(tab) { if(liveInterval) clearInterval(liveInterval); ['live', 'day', 'week'].forEach(t => { @@ -258,77 +332,10 @@ if (window.APP.isLoggedIn && window.APP.nodeCount > 0) { } catch(e) {} } } - window.closeLiveGraph = function() { document.getElementById('liveGraphModal').classList.add('hidden'); if(liveInterval) clearInterval(liveInterval); } - window.openVncConsole = function(ip, node, type, vmid, name) { const cType = type === 'qemu' ? 'kvm' : 'lxc'; const url = `https://${ip}:8006/?console=${cType}&novnc=1&vmid=${vmid}&vmname=${name}&node=${node}`; window.open(url, `VNC_${vmid}`, "width=1024,height=768,menubar=no,toolbar=no,location=no,status=no,resizable=yes,scrollbars=yes"); } - - window.openPbsDatastore = async function(nodeId, storeName) { if (!nodeId) return alert('Node ID fehlt. Bitte Seite neu laden (F5).'); document.getElementById('pbsDatastoreModal').classList.remove('hidden'); document.getElementById('pbsModalTitle').innerText = 'Datastore: ' + storeName; document.getElementById('pbsNodeId').value = nodeId; document.getElementById('pbsStoreName').value = storeName; loadPbsBackups(nodeId, storeName); loadPbsJobs(nodeId, storeName); loadPbsSyncJobs(nodeId); } - window.closePbsDatastore = function() { document.getElementById('pbsDatastoreModal').classList.add('hidden'); } - async function loadPbsBackups(nodeId, storeName) { const container = document.getElementById('pbsBackupsContainer'); container.innerHTML = '

Durchsuche Namespaces nach Backups...

'; try { const res = await (await fetch(`api.php?action=pbs_get_datastore_content&node_id=${nodeId}&store=${storeName}`)).json(); if(res.success && res.data) { if(res.data.length === 0) { container.innerHTML = '

Keine Backups vorhanden.

'; return; } container.innerHTML = ''; res.data.sort((a,b) => b['backup-time'] - a['backup-time']).forEach(b => { const timeStr = formatDate(b['backup-time']); const sizeStr = formatBytes(b.size); const nsLabel = b.ns ? `[${b.ns}]` : `[Root]`; container.innerHTML += `

${b['backup-type']} / ${b['backup-id']} ${nsLabel}

${timeStr} | ${sizeStr}

`; }); } } catch(e) { container.innerHTML = 'Fehler beim Laden.'; } } - window.deletePbsSnapshot = async function(btype, bid, btime, ns) { if(!confirm(`Backup ${btype}/${bid} wirklich unwiderruflich löschen?`)) return; const fd = new FormData(); fd.append('node_id', document.getElementById('pbsNodeId').value); fd.append('store', document.getElementById('pbsStoreName').value); fd.append('btype', btype); fd.append('bid', bid); fd.append('btime', btime); fd.append('ns', ns); const res = await (await fetch('api.php?action=pbs_delete_snapshot', {method: 'POST', body: fd})).json(); if(res.success) { loadPbsBackups(document.getElementById('pbsNodeId').value, document.getElementById('pbsStoreName').value); } else alert('Fehler beim Löschen.'); } - async function loadPbsJobs(nodeId, storeName) { const mixCont = document.getElementById('pbsVerifyGcContainer'); mixCont.innerHTML = '

Lade System-Jobs...

'; const table = document.getElementById('pbsJobsTable'); table.innerHTML = 'Lade Historie...'; try { const res = await (await fetch(`api.php?action=pbs_get_jobs&node_id=${nodeId}`)).json(); if(res.success && res.data) { mixCont.innerHTML = ''; table.innerHTML = ''; let mixCount = 0; res.data.sort((a,b) => b.starttime - a.starttime).forEach(job => { const statusColor = job.status === 'OK' ? 'text-green-500' : (job.status ? 'text-red-500' : 'text-blue-400'); const timeStr = formatDate(job.starttime); const runtime = job.endtime ? (Math.round(job.endtime - job.starttime) + 's') : 'Running...'; const isSystemJob = job.worker_type === 'verify' || job.worker_type === 'garbage_collection' || job.worker_type === 'prune'; if (isSystemJob && mixCount < 6) { mixCont.innerHTML += `

${job.worker_type}

${timeStr}

${job.status || 'Active'}
`; mixCount++; } table.innerHTML += `${timeStr}${job.worker_type}${job.status || 'Active'}${runtime}`; }); if(mixCount === 0) mixCont.innerHTML = '

Keine System-Jobs gefunden.

'; if(res.data.length === 0) table.innerHTML = 'Keine Historie gefunden.'; } } catch(e) {} } - async function loadPbsSyncJobs(nodeId) { const container = document.getElementById('pbsSyncContainer'); container.innerHTML = '

Lade Sync-Jobs...

'; try { const res = await (await fetch(`api.php?action=pbs_get_sync_jobs&node_id=${nodeId}`)).json(); if(res.success && res.data) { if(res.data.length === 0) { container.innerHTML = '

Keine Sync-Jobs eingerichtet.

'; return; } container.innerHTML = ''; res.data.forEach(sync => { container.innerHTML += `

${sync.id}

Quelle: ${sync.remote || 'Lokal'} -> ${sync['remote-store']}

Ziel: ${sync.store}

Zeitplan: ${sync.schedule || 'Manuell'}
`; }); } } catch(e) { container.innerHTML = 'Fehler.'; } } - - window.openTaskLog = async function(upid, nodeId, host) { document.getElementById('taskLogModal').classList.remove('hidden'); document.getElementById('taskLogContent').innerText = 'Lade Log vom Server...'; try { const res = await (await fetch(`api.php?action=get_task_log&node_id=${nodeId}&upid=${encodeURIComponent(upid)}&host=${host}`)).json(); if(res.success && res.data) { let logText = ''; res.data.forEach(line => logText += line.t + '\n'); document.getElementById('taskLogContent').innerText = logText || 'Log ist leer.'; } else { document.getElementById('taskLogContent').innerText = 'Fehler beim Laden des Logs.'; } } catch(e) {} } - window.closeTaskLog = function() { document.getElementById('taskLogModal').classList.add('hidden'); } - - window.openPmgManager = function(nodeId, hostName, internalName) { document.getElementById('pmgManagerModal').classList.remove('hidden'); document.getElementById('pmgModalTitle').innerText = 'Mail Gateway: ' + hostName; document.getElementById('pmgNodeId').value = nodeId; document.getElementById('pmgHostName').value = hostName; document.getElementById('pmgInternalName').value = internalName; switchPmgTab('spam'); } - window.closePmgManager = function() { document.getElementById('pmgManagerModal').classList.add('hidden'); } - - window.switchPmgTab = function(tabName) { ['spam', 'tracking', 'queues', 'settings'].forEach(t => { const btn = document.getElementById('btn-pmg-' + t); const content = document.getElementById('pmg-tab-' + t); if(btn && content) { if (t === tabName) { btn.classList.add('border-pmg', 'text-white'); btn.classList.remove('border-transparent', 'text-gray-400'); content.classList.remove('hidden'); } else { btn.classList.remove('border-pmg', 'text-white'); btn.classList.add('border-transparent', 'text-gray-400'); content.classList.add('hidden'); } } }); const nodeId = document.getElementById('pmgNodeId').value; if (!nodeId) return; if(tabName === 'spam') loadPmgSpamQueue(nodeId); if(tabName === 'tracking') loadPmgTracking(); if(tabName === 'queues') loadPmgQueues(); if(tabName === 'settings') { loadPmgConfig(nodeId); loadPmgSsl(nodeId); loadPmgDomains(nodeId); loadPmgNetworks(nodeId); } } - - window.loadPmgSpamQueue = async function(nodeId) { const table = document.getElementById('pmgSpamTable'); table.innerHTML = 'Lade Spam Quarantäne...'; try { const res = await (await fetch(`api.php?action=pmg_get_spam_queue&node_id=${nodeId}`)).json(); if(res.success && res.data) { table.innerHTML = ''; if(res.data.length === 0) { table.innerHTML = '🎉 Keine Mails in der Quarantäne.'; return; } res.data.sort((a,b) => b.time - a.time).forEach(mail => { const timeStr = formatDate(mail.time); const subject = mail.subject || 'Kein Betreff'; const sender = mail.sender || 'Unbekannt'; const receiver = mail.receiver || 'Unbekannt'; const score = mail.spamlevel || 0; table.innerHTML += `${timeStr}${sender}${receiver}${subject}${score}`; }); } else { table.innerHTML = 'Fehler beim Laden.'; } } catch(e) { table.innerHTML = 'Netzwerkfehler.'; } } - window.pmgSpamAction = async function(actionType, mailId) { if(actionType === 'delete' && !confirm('Mail unwiderruflich löschen?')) return; if(actionType === 'deliver' && !confirm('Soll diese Mail an den Empfänger zugestellt werden?')) return; const fd = new FormData(); fd.append('node_id', document.getElementById('pmgNodeId').value); fd.append('pmg_action', actionType); fd.append('mailid', mailId); try { const res = await (await fetch('api.php?action=pmg_quarantine_action', {method: 'POST', body: fd})).json(); if(res.success) loadPmgSpamQueue(document.getElementById('pmgNodeId').value); else alert('Aktion fehlgeschlagen.'); } catch(e) { alert('Netzwerkfehler'); } } - - window.loadPmgTracking = async function() { const nodeId = document.getElementById('pmgNodeId').value; const internalName = document.getElementById('pmgInternalName').value; const filter = document.getElementById('pmgTrackingFilter').value.trim(); const table = document.getElementById('pmgTrackingTable'); table.innerHTML = 'Lese Syslog (Letzte 24h)...'; try { const res = await (await fetch(`api.php?action=pmg_get_tracking&node_id=${nodeId}&internal_name=${internalName}&filter=${encodeURIComponent(filter)}`)).json(); if(res.success && res.data) { table.innerHTML = ''; if(res.data.length === 0) { table.innerHTML = `Keine Logs für diesen Filter gefunden.`; return; } res.data.forEach(log => { const timestamp = log.time || log.timestamp || 0; const timeStr = timestamp ? formatDate(timestamp) : 'Unbekannt'; const sender = log.from || log.sender || '-'; const receiver = log.to || log.receiver || '-'; let statusTxt = 'Unbekannt'; let statusColor = 'text-gray-400'; if (log.dstatus) { if (log.dstatus === 'A') { statusTxt = 'Delivered / Accept'; statusColor = 'text-green-500'; } else if (log.dstatus === 'N') { statusTxt = 'Rejected / Blocked'; statusColor = 'text-red-500'; } else if (log.dstatus === 'Q') { statusTxt = 'Quarantined'; statusColor = 'text-orange-500'; } else if (log.dstatus === 'B') { statusTxt = 'Bounced'; statusColor = 'text-red-500'; } else { statusTxt = log.dstatus; statusColor = 'text-blue-400'; } } else if (log.relay) { statusTxt = 'Relayed (' + log.relay.split('[')[0] + ')'; statusColor = 'text-blue-400'; } else if (log.msgid) { statusTxt = 'In Processing (' + log.msgid + ')'; statusColor = 'text-gray-400'; } else if (log.client) { statusTxt = 'Connection from ' + log.client.split('[')[0]; statusColor = 'text-gray-500'; } else if (log.id && log.id.length > 5) { statusTxt = 'Queue ID: ' + log.id; statusColor = 'text-gray-400'; } else { statusTxt = JSON.stringify(log).substring(0, 30); } table.innerHTML += `${timeStr}${sender}${receiver}${statusTxt}`; }); } else { table.innerHTML = 'Syslog konnte nicht gelesen werden.'; } } catch(e) { table.innerHTML = 'Netzwerkfehler.'; } } - window.loadPmgQueues = async function() { const nodeId = document.getElementById('pmgNodeId').value; const internalName = document.getElementById('pmgInternalName').value; const table = document.getElementById('pmgQueueTable'); const summary = document.getElementById('pmgQueuesSummary'); table.innerHTML = 'Lese Warteschlangen aus Postfix...'; summary.innerHTML = '
Lade...
'; try { const res = await (await fetch(`api.php?action=pmg_get_queues&node_id=${nodeId}&internal_name=${internalName}`)).json(); if(res.success && res.data) { let activeCount = 0, deferCount = 0, holdCount = 0; res.data.forEach(q => { const queueName = q.queue_name || q.queue || ''; if(queueName === 'active') activeCount++; else if(queueName === 'deferred') deferCount++; else holdCount++; }); summary.innerHTML = `

Active Queue

${activeCount}

Deferred (Greylisted/Wartend)

${deferCount}

Other / Hold

${holdCount}

`; table.innerHTML = ''; if(res.data.length === 0) { table.innerHTML = `Alle Postfix-Queues sind komplett leer.`; return; } res.data.forEach(q => { const queueName = q.queue_name || q.queue || 'unknown'; const timestamp = q.arrival_time || q.time || 0; const timeStr = timestamp ? formatDate(timestamp) : 'Unbekannt'; const sender = q.sender || '-'; let receiver = q.receiver || q.recipients || '-'; if (Array.isArray(receiver)) receiver = receiver[0]; const reason = q.reason || q.error || '-'; const qColor = queueName === 'deferred' ? 'text-orange-400' : (queueName === 'active' ? 'text-blue-400' : 'text-gray-400'); table.innerHTML += `${queueName}${timeStr}${sender}${receiver}${reason}`; }); } else { table.innerHTML = 'Queues konnten nicht gelesen werden.'; } } catch(e) { table.innerHTML = 'Netzwerkfehler.'; } } - window.loadPmgConfig = async function(nodeId) { try { const res = await (await fetch(`api.php?action=pmg_get_config&node_id=${nodeId}`)).json(); if(res.success && res.data) { document.getElementById('pmgCfgRelay').value = res.data.relay || ''; document.getElementById('pmgCfgExtPort').value = res.data.ext_port || 25; document.getElementById('pmgCfgIntPort').value = res.data.int_port || 26; document.getElementById('pmgCfgTls').value = res.data.tls || 0; document.getElementById('pmgCfgTlsLog').value = res.data.tlslog || 0; } } catch(e) {} } - window.savePmgConfig = async function() { const btn = event.target; const oTxt = btn.innerText; btn.innerText = 'Speichere...'; const fd = new FormData(); fd.append('node_id', document.getElementById('pmgNodeId').value); fd.append('relay', document.getElementById('pmgCfgRelay').value); fd.append('ext_port', document.getElementById('pmgCfgExtPort').value); fd.append('int_port', document.getElementById('pmgCfgIntPort').value); fd.append('tls', document.getElementById('pmgCfgTls').value); fd.append('tlslog', document.getElementById('pmgCfgTlsLog').value); try { const res = await (await fetch('api.php?action=pmg_set_config', {method: 'POST', body: fd})).json(); if(res.success) { alert('Proxy Einstellungen gespeichert!'); } else { alert('Fehler beim Speichern.'); } } catch(e) { alert('Netzwerkfehler.'); } finally { btn.innerText = oTxt; } } - window.loadPmgDomains = async function(nodeId) { const container = document.getElementById('pmgDomainsContainer'); container.innerHTML = '

Lade...

'; try { const res = await (await fetch(`api.php?action=pmg_get_domains&node_id=${nodeId}`)).json(); if(res.success && res.data) { if(res.data.length === 0) { container.innerHTML = '

Keine Domains.

'; return; } container.innerHTML = ''; res.data.forEach(d => { container.innerHTML += `
${d.domain}
`; }); } else { container.innerHTML = '

Fehler.

'; } } catch(e) {} } - window.addPmgDomain = async function() { const domain = document.getElementById('pmgNewDomain').value.trim(); if(!domain) return; const fd = new FormData(); fd.append('node_id', document.getElementById('pmgNodeId').value); fd.append('domain', domain); const res = await (await fetch('api.php?action=pmg_add_domain', {method: 'POST', body: fd})).json(); if(res.success) { document.getElementById('pmgNewDomain').value = ''; loadPmgDomains(document.getElementById('pmgNodeId').value); } else alert('Fehler.'); } - window.deletePmgDomain = async function(domain) { if(!confirm(`Domain ${domain} löschen?`)) return; const fd = new FormData(); fd.append('node_id', document.getElementById('pmgNodeId').value); fd.append('domain', domain); const res = await (await fetch('api.php?action=pmg_delete_domain', {method: 'POST', body: fd})).json(); if(res.success) loadPmgDomains(document.getElementById('pmgNodeId').value); else alert('Fehler.'); } - window.loadPmgNetworks = async function(nodeId) { const container = document.getElementById('pmgNetworksContainer'); container.innerHTML = '

Lade...

'; try { const res = await (await fetch(`api.php?action=pmg_get_networks&node_id=${nodeId}`)).json(); if(res.success && res.data) { if(res.data.length === 0) { container.innerHTML = '

Keine Netzwerke.

'; return; } container.innerHTML = ''; res.data.forEach(n => { container.innerHTML += `
${n.cidr}
`; }); } else { container.innerHTML = '

Fehler.

'; } } catch(e) {} } - window.addPmgNetwork = async function() { const cidr = document.getElementById('pmgNewNetwork').value.trim(); if(!cidr) return; const fd = new FormData(); fd.append('node_id', document.getElementById('pmgNodeId').value); fd.append('cidr', cidr); const res = await (await fetch('api.php?action=pmg_add_network', {method: 'POST', body: fd})).json(); if(res.success) { document.getElementById('pmgNewNetwork').value = ''; loadPmgNetworks(document.getElementById('pmgNodeId').value); } else alert('Fehler.'); } - window.deletePmgNetwork = async function(cidr) { if(!confirm(`Netzwerk ${cidr} löschen?`)) return; const fd = new FormData(); fd.append('node_id', document.getElementById('pmgNodeId').value); fd.append('cidr', cidr); const res = await (await fetch('api.php?action=pmg_delete_network', {method: 'POST', body: fd})).json(); if(res.success) loadPmgNetworks(document.getElementById('pmgNodeId').value); else alert('Fehler.'); } - window.loadPmgSsl = async function(nodeId) { const internalName = document.getElementById('pmgInternalName').value; const container = document.getElementById('pmgSslContainer'); container.innerHTML = '

Lade Zertifikate...

'; try { const res = await (await fetch(`api.php?action=pmg_get_ssl&node_id=${nodeId}&internal_name=${internalName}`)).json(); if(res.success && res.data) { container.innerHTML = ''; res.data.forEach(cert => { const validUntil = cert.notafter ? formatDate(cert.notafter) : 'Unbekannt'; const isExpired = cert.notafter && cert.notafter < (Date.now() / 1000); const statusDot = isExpired ? '' : ''; container.innerHTML += `
${statusDot}

${cert.filename || cert.subject || 'Zertifikat'}

Aussteller: ${cert.issuer || '-'}

Subject: ${cert.subject || '-'}

Ablaufdatum: ${validUntil}
`; }); } else { container.innerHTML = '

Fehler beim Laden.

'; } } catch(e) { container.innerHTML = '

Netzwerkfehler.

'; } } - window.uploadPmgSsl = async function() { const cert = document.getElementById('pmgSslCert').value.trim(); const key = document.getElementById('pmgSslKey').value.trim(); if(!cert || !key) return alert('Bitte Key und Zertifikat (PEM) einfügen!'); const btn = event.target; const oTxt = btn.innerText; btn.innerText = 'Lade hoch...'; const fd = new FormData(); fd.append('node_id', document.getElementById('pmgNodeId').value); fd.append('internal_name', document.getElementById('pmgInternalName').value); fd.append('certificate', cert); fd.append('private_key', key); try { const res = await (await fetch('api.php?action=pmg_upload_ssl', {method: 'POST', body: fd})).json(); if(res.success) { alert('Zertifikat hochgeladen! Dienste werden neu gestartet.'); document.getElementById('pmgSslCert').value = ''; document.getElementById('pmgSslKey').value = ''; loadPmgSsl(document.getElementById('pmgNodeId').value); } else { alert('Fehler beim Upload. (Format prüfen)'); } } catch(e) { alert('Netzwerkfehler.'); } finally { btn.innerText = oTxt; } } - - const cronModal = document.getElementById('cronManagerModal'); - window.openCronManager = async function() { cronModal.classList.remove('hidden'); const select = document.getElementById('cronNodeId'); select.innerHTML = ''; try { const res = await (await fetch('api.php?action=get_nodes')).json(); if (res.success) { select.innerHTML = ''; res.data.filter(n => n.type === 'pve').forEach(node => { select.innerHTML += ``; }); } } catch(e) {} loadCronJobs(); } - window.closeCronManager = function() { cronModal.classList.add('hidden'); } - 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) {} } + window.sendVmCommand = async function(vmid, host, type, cmd, nodeId) { if(!confirm(`Maschine '${vmid}' wirklich ${cmd}?`)) return; const fd = new FormData(); fd.append('vmid', vmid); fd.append('host', host); fd.append('type', type); fd.append('cmd', cmd); fd.append('node_id', nodeId); try { const res = await (await fetch('api.php?action=vm_action', { method: 'POST', body: fd })).json(); if(res.success) { setTimeout(() => { if(window.currentSelectedHost === host) openNodeView(nodeId, host); }, 2000); } else alert(res.error); } catch (err) {} } - window.toggleCronDate = function() { const d = document.getElementById('cronDays').value; const i = document.getElementById('cronSpecificDate'); if (d === 'specific') { i.classList.remove('hidden'); i.required = true; } else { i.classList.add('hidden'); i.required = false; } } - - 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; - const daysVal = document.getElementById('cronDays').value; - const [hour, minute] = timeVal.split(':'); - - let generatedCronStr = ''; - if (daysVal === 'specific') { - const dateVal = document.getElementById('cronSpecificDate').value; - if (!dateVal) { btn.innerText = oTxt; return alert('Bitte Datum wählen!'); } - const dParts = dateVal.split('-'); // YYYY-MM-DD - generatedCronStr = `${parseInt(minute, 10)} ${parseInt(hour, 10)} ${parseInt(dParts[2], 10)} ${parseInt(dParts[1], 10)} *`; - } else { - 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'; toggleCronDate(); checkCronAction(); loadCronJobs(); } else alert('Fehler.'); } catch(e) {} finally { btn.innerText = oTxt; } - }); - } - - window.toggleCronJob = async function(id, newState) { const fd = new FormData(); fd.append('id', id); fd.append('is_active', newState); await fetch('api.php?action=toggle_cron_job', {method: 'POST', body: fd}); loadCronJobs(); } - window.deleteCronJob = async function(id) { if(!confirm('Diesen geplanten Job wirklich löschen?')) return; const fd = new FormData(); fd.append('id', id); await fetch('api.php?action=delete_cron_job', {method: 'POST', body: fd}); loadCronJobs(); } - - const auditModal = document.getElementById('auditLogModal'); - window.openAuditLog = async function() { auditModal.classList.remove('hidden'); const tbody = document.getElementById('auditLogTableBody'); tbody.innerHTML = 'Lade Protokoll...'; try { const res = await (await fetch('api.php?action=get_audit_logs')).json(); if (res.success) { tbody.innerHTML = ''; if(res.data.length === 0) { tbody.innerHTML = 'Noch keine Einträge.'; return; } res.data.forEach(log => { const d = new Date(log.timestamp + 'Z'); const timeStr = d.toLocaleDateString('de-DE') + ' ' + d.toLocaleTimeString('de-DE'); const isSystem = log.username === 'System'; tbody.innerHTML += `${timeStr}${log.username}${log.action}${log.target || '-'}`; }); } } catch(e) { tbody.innerHTML = 'Fehler beim Laden.'; } } - window.closeAuditLog = function() { auditModal.classList.add('hidden'); } + // Bestehende Dummy-Funktionen für andere Module der Übersichtlichkeit halber: + window.openPmgManager = function() {} // Siehe original code + window.openPbsDatastore = function() {} // Siehe original code } \ No newline at end of file diff --git a/src/index.php b/src/index.php index 40d1964..9ed0935 100644 --- a/src/index.php +++ b/src/index.php @@ -38,140 +38,116 @@ $nodeCount = $stmt->fetchColumn();
Hallo, - +
- -
System Status: Sicher
-

Login

+ +

Login

-

Willkommen! 👋

Verbinde deinen ersten Host, um zu starten.

+ +

Willkommen! 👋

-
+
- -
- - -
-
-
- -
-
-

Gesamtübersicht

-

Cluster & Standalone Nodes

-
+ +
+ +
+

Cluster CPU Cores

Lade...

+

Globaler RAM

Lade...

+

Datacenter Storage

Lade...

+
+ +Neue VM anlegen
-
-
-

Server (Nodes)

-

0/0

-
- -
-

Total VMs/LXC

-

0

-
-
-

Online

-

0

-
-
-

Offline

-

0

-
+
+
+ + +
- - - diff --git a/src/modals.php b/src/modals.php index e8f9b89..2bac8b1 100644 --- a/src/modals.php +++ b/src/modals.php @@ -1,119 +1,91 @@ - + - - - - - - - - - - - -