diff --git a/src/api_pve.php b/src/api_pve.php
index 994148a..f547c7e 100644
--- a/src/api_pve.php
+++ b/src/api_pve.php
@@ -1,358 +1,50 @@
-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') {
+// === NEU: 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);
- $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; } }
- }
+ $timeframe = $_GET['timeframe'] === 'week' ? 'week' : 'day';
+ $type = $node['type'] ?? 'pve';
+
+ if ($type === 'pbs') {
+ $endpoint = "/api2/json/nodes/localhost/rrddata?timeframe={$timeframe}&cf=AVERAGE";
+ } elseif ($type === 'pmg') {
+ $internalName = $_GET['host'] ?? 'localhost';
+ $endpoint = "/api2/json/nodes/{$internalName}/rrddata?timeframe={$timeframe}&cf=AVERAGE";
+ } else {
+ if ($_GET['target_mode'] === 'node') {
+ $endpoint = "/api2/json/nodes/{$_GET['host']}/rrddata?timeframe={$timeframe}&cf=AVERAGE";
+ } else {
+ $endpoint = "/api2/json/nodes/{$_GET['host']}/{$_GET['target_mode']}/{$_GET['vmid']}/rrddata?timeframe={$timeframe}&cf=AVERAGE";
}
}
- 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;
+ $res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], $endpoint, $type);
+ echo json_encode(['success' => true, 'data' => $res['data'] ?? []]); exit;
+}
+
+// === NEU: APT Update Manager ===
+if ($action === 'get_update_details') {
+ if (!$isAdmin) exit;
+ $stmt = $pdo->query("SELECT * FROM nodes WHERE type != 'pmg'"); $updateList = [];
+ foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $node) {
+ $nData = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes", $node['type']);
+ if (isset($nData['data'])) {
+ 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'] ];
}
}
}
-
- $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;
}
+ echo json_encode(['success' => true, 'data' => $updateList]); exit;
}
-?>
\ No newline at end of file
+
+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 0deb1be..db76801 100644
--- a/src/app.js
+++ b/src/app.js
@@ -11,10 +11,7 @@ if (!window.APP.isLoggedIn) {
if(setupForm) { setupForm.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('nodeName').value); fd.append('ip', document.getElementById('nodeIp').value); fd.append('user', document.getElementById('nodeUser').value); fd.append('pass', document.getElementById('nodePass').value); fd.append('type', 'pve'); try { const res = await (await fetch('api.php?action=add_node', { method: 'POST', body: fd })).json(); if(res.success) { btn.innerText = 'Erfolgreich!'; setTimeout(() => window.location.reload(), 1000); } else { alert(res.error); btn.innerText = oTxt; } } catch (e) { alert('Netzwerkfehler.'); btn.innerText = oTxt; } }); }
}
-async function logout() {
- await fetch('api.php?action=logout');
- window.location.href = window.location.pathname + '?t=' + Date.now();
-}
+async function logout() { await fetch('api.php?action=logout'); window.location.href = window.location.pathname + '?t=' + Date.now(); }
if (window.APP.isLoggedIn && window.APP.nodeCount > 0) {
@@ -33,23 +30,11 @@ if (window.APP.isLoggedIn && window.APP.nodeCount > 0) {
const pwdForm = document.getElementById('changePasswordForm');
if(pwdForm) {
pwdForm.addEventListener('submit', async function(e) {
- e.preventDefault();
- const oldP = document.getElementById('oldPassword').value;
- const newP = document.getElementById('newPassword').value;
- const confirmP = document.getElementById('newPasswordConfirm').value;
-
+ e.preventDefault(); const oldP = document.getElementById('oldPassword').value; const newP = document.getElementById('newPassword').value; const confirmP = document.getElementById('newPasswordConfirm').value;
if (newP !== confirmP) { alert('Die neuen Passwörter stimmen nicht überein!'); return; }
-
const btn = this.querySelector('button[type="submit"]'); const oTxt = btn.innerText; btn.innerText = 'Speichere...';
const fd = new FormData(); fd.append('old_password', oldP); fd.append('new_password', newP);
-
- try {
- const res = await (await fetch('api.php?action=change_password', {method: 'POST', body: fd})).json();
- if(res.success) {
- alert('Passwort erfolgreich geändert! Bitte neu anmelden.');
- closePasswordModal(); logout();
- } else { alert(res.error || 'Fehler beim Ändern des Passworts.'); }
- } catch(err) { alert('Netzwerkfehler.'); } finally { btn.innerText = oTxt; }
+ try { const res = await (await fetch('api.php?action=change_password', {method: 'POST', body: fd})).json(); if(res.success) { alert('Passwort erfolgreich geändert! Bitte neu anmelden.'); closePasswordModal(); logout(); } else { alert(res.error || 'Fehler beim Ändern des Passworts.'); } } catch(err) { alert('Netzwerkfehler.'); } finally { btn.innerText = oTxt; }
});
}
@@ -58,12 +43,6 @@ if (window.APP.isLoggedIn && window.APP.nodeCount > 0) {
if(tab === 'pbs') fetchPbsStats(); if(tab === 'pmg') fetchPmgStats();
}
- 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() {
@@ -72,78 +51,32 @@ if (window.APP.isLoggedIn && window.APP.nodeCount > 0) {
const res = await (await fetch('api.php?action=get_stats')).json();
if(res.success && res.data) {
const d = res.data;
- document.getElementById('stat-cpu-text').innerText = `${d.cpu_percent}% (${d.cpu_cores} Cores)`;
- document.getElementById('stat-cpu-bar').style.width = `${d.cpu_percent}%`;
- let ramPercent = (d.ram_used / d.ram_total) * 100 || 0;
- document.getElementById('stat-ram-text').innerText = `${formatBytes(d.ram_used)} / ${formatBytes(d.ram_total)}`;
- document.getElementById('stat-ram-bar').style.width = `${ramPercent}%`;
- let diskPercent = (d.disk_used / d.disk_total) * 100 || 0;
- document.getElementById('stat-disk-text').innerText = `${formatBytes(d.disk_used)} / ${formatBytes(d.disk_total)}`;
- document.getElementById('stat-disk-bar').style.width = `${diskPercent}%`;
-
- // Neue Übersichtskachel füttern
+ document.getElementById('stat-cpu-text').innerText = `${d.cpu_percent}% (${d.cpu_cores} Cores)`; document.getElementById('stat-cpu-bar').style.width = `${d.cpu_percent}%`;
+ let ramPercent = (d.ram_used / d.ram_total) * 100 || 0; document.getElementById('stat-ram-text').innerText = `${formatBytes(d.ram_used)} / ${formatBytes(d.ram_total)}`; document.getElementById('stat-ram-bar').style.width = `${ramPercent}%`;
+ let diskPercent = (d.disk_used / d.disk_total) * 100 || 0; document.getElementById('stat-disk-text').innerText = `${formatBytes(d.disk_used)} / ${formatBytes(d.disk_total)}`; document.getElementById('stat-disk-bar').style.width = `${diskPercent}%`;
if (d.cluster_stats) {
const elNodesOn = document.getElementById('stat-nodes-online');
- if (elNodesOn) {
- elNodesOn.innerText = d.cluster_stats.nodes_online;
- if (d.cluster_stats.nodes_online < d.cluster_stats.nodes_total) {
- elNodesOn.className = 'text-red-500';
- } else {
- elNodesOn.className = 'text-green-500';
- }
- }
+ if (elNodesOn) { elNodesOn.innerText = d.cluster_stats.nodes_online; elNodesOn.className = (d.cluster_stats.nodes_online < d.cluster_stats.nodes_total) ? 'text-red-500' : 'text-green-500'; }
if(document.getElementById('stat-nodes-total')) document.getElementById('stat-nodes-total').innerText = d.cluster_stats.nodes_total;
if(document.getElementById('stat-vms-total')) document.getElementById('stat-vms-total').innerText = d.cluster_stats.vms_total;
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) {}
}
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 += ``; }); } } 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) { console.error(err); } }
- async function fetchUpdates() { 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) {} }
- 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)}
⚙️ Verwaltung öffnen
`; }); } } 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 += `CPU${cpuPercent}%
RAM${ramPercent}%
System Disk${diskPercent}%
🛡️ Gateway öffnen
`; }); } } catch(e) {} }
+ 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) {} }
+
+ // APT UPDATES
+ 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) {} }
+ window.openUpdateManager = async function() { document.getElementById('updateManagerModal').classList.remove('hidden'); const content = document.getElementById('updateManagerContent'); content.innerHTML = 'Prüfe Updates...
'; try { const res = await (await fetch('api.php?action=get_update_details')).json(); if (res.success) { content.innerHTML = ''; if(res.data.length === 0) { content.innerHTML = '🎉 Alle Systeme sind auf dem neuesten Stand!
'; return; } res.data.forEach(node => { let pkgsHtml = ''; node.packages.forEach(p => { pkgsHtml += `${p.Title}${p.OldVersion} ➔ ${p.Version}
`; }); content.innerHTML += ` ${node.display_name} ${node.packages.length} Updates
${pkgsHtml}
`; }); } } catch(e) {} }
+ 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 += `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);
@@ -201,8 +134,7 @@ if (window.APP.isLoggedIn && window.APP.nodeCount > 0) {
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...
';
+ document.getElementById('snapshotListContainer').innerHTML = 'Lade Snapshots...
'; document.getElementById('backupListContainer').innerHTML = 'Lade Backups...
';
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'; }
@@ -219,439 +151,135 @@ if (window.APP.isLoggedIn && window.APP.nodeCount > 0) {
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) {} }
- 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 += ``;
- });
- }
- } catch(e) {}
- }
+ 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 += ``; }); } } 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) {}
- }
+ 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) {}
- }
+ 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) {}
- }
+ 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 += ``;
- });
- }
- container.innerHTML += `${node.name}
${node.ip_address}
${vmsHtml}
`;
- });
- }
- } catch(e) {}
- }
+ 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 += ``; }); } container.innerHTML += `${node.name}
${node.ip_address}
${vmsHtml}
`; }); } } catch(e) {} }
window.closeNodeTopology = function() { topologyModal.classList.add('hidden'); }
- let perfChartObj = null; let netChartObj = null; let liveInterval = null;
+ // RRD UND LIVE GRAPH ENGINE
+ let perfChartObj = null; let netChartObj = null; let liveInterval = null; let currentGraphParams = {};
window.openLiveGraph = function(targetMode, vmid, host, nodeId, name) {
- document.getElementById('liveGraphModal').classList.remove('hidden'); document.getElementById('graphModalTitle').innerText = targetMode === 'node' ? `Live Performance: Node ${name}` : `Live Performance: VM ${name} (${vmid})`;
- if(perfChartObj) perfChartObj.destroy(); if(netChartObj) netChartObj.destroy();
- const ctxPerf = document.getElementById('perfChart').getContext('2d'); perfChartObj = new Chart(ctxPerf, { 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: 0 }, scales: { x: { grid: { color: '#33334d' }, ticks: { color: '#9ca3af' } }, y: { min: 0, max: 100, grid: { color: '#33334d' }, ticks: { color: '#9ca3af' } } }, plugins: { legend: { labels: { color: '#e2e8f0' } } } } });
- const ctxNet = document.getElementById('netChart').getContext('2d'); netChartObj = new Chart(ctxNet, { type: 'line', data: { labels: [], datasets: [ { label: 'RX (MB/s)', borderColor: '#10b981', backgroundColor: 'rgba(16, 185, 129, 0.1)', borderWidth: 2, tension: 0.4, fill: true, data: [] }, { label: 'TX (MB/s)', borderColor: '#8b5cf6', backgroundColor: 'rgba(139, 92, 246, 0.1)', borderWidth: 2, tension: 0.4, fill: true, data: [] }]}, options: { responsive: true, maintainAspectRatio: false, animation: { duration: 0 }, scales: { x: { grid: { color: '#33334d' }, ticks: { color: '#9ca3af' } }, y: { min: 0, grid: { color: '#33334d' }, ticks: { color: '#9ca3af' } } }, plugins: { legend: { labels: { color: '#e2e8f0' } } } } });
+ document.getElementById('liveGraphModal').classList.remove('hidden');
+ currentGraphParams = { mode: targetMode, vmid: vmid, host: host, nodeId: nodeId, name: name };
+ switchGraphTab('live');
+ }
+
+ window.switchGraphTab = async function(tab) {
if(liveInterval) clearInterval(liveInterval);
- let prevNetIn = 0; let prevNetOut = 0; let prevTime = null;
- const fetchLiveData = async () => {
- try {
- const endpoint = targetMode === 'node' ? `api.php?action=get_node_status&host=${host}&node_id=${nodeId}` : `api.php?action=get_vm_status&vmid=${vmid}&host=${host}&type=${targetMode}&node_id=${nodeId}`;
- const res = await (await fetch(endpoint)).json();
- if(res.success && res.data) {
- const d = res.data; const now = Date.now();
- let cpuRaw = 0; if (d.cpu !== undefined) cpuRaw = d.cpu; else if (d.cpuinfo && d.cpuinfo.cpus) cpuRaw = 0; const cpu = (cpuRaw * 100).toFixed(1);
- let ram = 0; if (d.maxmem && d.maxmem > 0) { ram = ((d.mem / d.maxmem) * 100).toFixed(1); } else if (d.memory && d.memory.total > 0) { ram = ((d.memory.used / d.memory.total) * 100).toFixed(1); }
-
- let currentNetIn = d.netin || 0; let currentNetOut = d.netout || 0;
- let rxSpeed = 0; let txSpeed = 0;
-
- if (d.is_rrd_net) {
- rxSpeed = (currentNetIn / (1024 * 1024)).toFixed(2);
- txSpeed = (currentNetOut / (1024 * 1024)).toFixed(2);
- } else {
- if(prevTime !== null) {
- const timeSec = (now - prevTime) / 1000;
- if (timeSec > 0) {
- rxSpeed = Math.max(0, ((currentNetIn - prevNetIn) / timeSec / (1024 * 1024))).toFixed(2);
- txSpeed = Math.max(0, ((currentNetOut - prevNetOut) / timeSec / (1024 * 1024))).toFixed(2);
- }
- }
+ ['live', 'day', 'week'].forEach(t => {
+ const b = document.getElementById('btn-graph-' + t);
+ if(t === tab) { b.classList.add('border-proxmox', 'text-white'); b.classList.remove('border-transparent', 'text-gray-400'); }
+ else { b.classList.remove('border-proxmox', 'text-white'); b.classList.add('border-transparent', 'text-gray-400'); }
+ });
+
+ document.getElementById('graphModalTitle').innerText = `${tab === 'live' ? 'Live' : (tab === 'day' ? '24h' : '7 Tage')} Performance: ${currentGraphParams.name}`;
+ const dot = document.getElementById('graphStatusDot');
+ if(tab === 'live') { dot.classList.add('animate-pulse', 'bg-green-500'); dot.classList.remove('bg-blue-500'); document.getElementById('graphSubText').innerText = 'Metriken werden live abgefragt.'; }
+ else { dot.classList.remove('animate-pulse', 'bg-green-500'); dot.classList.add('bg-blue-500'); document.getElementById('graphSubText').innerText = 'Historische RRD-Daten via Proxmox API.'; }
+
+ if(perfChartObj) perfChartObj.destroy(); if(netChartObj) netChartObj.destroy();
+ const ctxPerf = document.getElementById('perfChart').getContext('2d'); perfChartObj = new Chart(ctxPerf, { type: 'line', data: { labels: [], datasets: [ { label: 'CPU (%)', borderColor: '#3b82f6', backgroundColor: 'rgba(59, 130, 246, 0.1)', borderWidth: 2, tension: 0.4, fill: true, pointRadius: tab==='live'?3:0, data: [] }, { label: 'RAM (%)', borderColor: '#E57000', backgroundColor: 'rgba(229, 112, 0, 0.1)', borderWidth: 2, tension: 0.4, fill: true, pointRadius: tab==='live'?3:0, data: [] }]}, options: { responsive: true, maintainAspectRatio: false, animation: { duration: tab==='live'?0:500 }, scales: { x: { grid: { color: '#33334d' }, ticks: { color: '#9ca3af' } }, y: { min: 0, max: 100, grid: { color: '#33334d' }, ticks: { color: '#9ca3af' } } }, plugins: { legend: { labels: { color: '#e2e8f0' } } } } });
+ const ctxNet = document.getElementById('netChart').getContext('2d'); netChartObj = new Chart(ctxNet, { type: 'line', data: { labels: [], datasets: [ { label: 'RX (MB/s)', borderColor: '#10b981', backgroundColor: 'rgba(16, 185, 129, 0.1)', borderWidth: 2, tension: 0.4, fill: true, pointRadius: tab==='live'?3:0, data: [] }, { label: 'TX (MB/s)', borderColor: '#8b5cf6', backgroundColor: 'rgba(139, 92, 246, 0.1)', borderWidth: 2, tension: 0.4, fill: true, pointRadius: tab==='live'?3:0, data: [] }]}, options: { responsive: true, maintainAspectRatio: false, animation: { duration: tab==='live'?0:500 }, scales: { x: { grid: { color: '#33334d' }, ticks: { color: '#9ca3af' } }, y: { min: 0, grid: { color: '#33334d' }, ticks: { color: '#9ca3af' } } }, plugins: { legend: { labels: { color: '#e2e8f0' } } } } });
+
+ if(tab === 'live') {
+ let prevNetIn = 0; let prevNetOut = 0; let prevTime = null;
+ const fetchLiveData = async () => {
+ try {
+ const endpoint = currentGraphParams.mode === 'node' ? `api.php?action=get_node_status&host=${currentGraphParams.host}&node_id=${currentGraphParams.nodeId}` : `api.php?action=get_vm_status&vmid=${currentGraphParams.vmid}&host=${currentGraphParams.host}&type=${currentGraphParams.mode}&node_id=${currentGraphParams.nodeId}`;
+ const res = await (await fetch(endpoint)).json();
+ if(res.success && res.data) {
+ const d = res.data; const now = Date.now();
+ let cpuRaw = 0; if (d.cpu !== undefined) cpuRaw = d.cpu; else if (d.cpuinfo && d.cpuinfo.cpus) cpuRaw = 0; const cpu = (cpuRaw * 100).toFixed(1);
+ let ram = 0; if (d.maxmem && d.maxmem > 0) { ram = ((d.mem / d.maxmem) * 100).toFixed(1); } else if (d.memory && d.memory.total > 0) { ram = ((d.memory.used / d.memory.total) * 100).toFixed(1); }
+
+ let currentNetIn = d.netin || 0; let currentNetOut = d.netout || 0;
+ let rxSpeed = 0; let txSpeed = 0;
+ if (d.is_rrd_net) { rxSpeed = (currentNetIn / (1024 * 1024)).toFixed(2); txSpeed = (currentNetOut / (1024 * 1024)).toFixed(2); } else { if(prevTime !== null) { const timeSec = (now - prevTime) / 1000; if (timeSec > 0) { rxSpeed = Math.max(0, ((currentNetIn - prevNetIn) / timeSec / (1024 * 1024))).toFixed(2); txSpeed = Math.max(0, ((currentNetOut - prevNetOut) / timeSec / (1024 * 1024))).toFixed(2); } } }
+ prevNetIn = currentNetIn; prevNetOut = currentNetOut; prevTime = now;
+ const timeStr = new Date().toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
+
+ perfChartObj.data.labels.push(timeStr); perfChartObj.data.datasets[0].data.push(cpu); perfChartObj.data.datasets[1].data.push(ram); if(perfChartObj.data.labels.length > 30) { perfChartObj.data.labels.shift(); perfChartObj.data.datasets[0].data.shift(); perfChartObj.data.datasets[1].data.shift(); } perfChartObj.update();
+ if(prevTime !== null || d.is_rrd_net) { netChartObj.data.labels.push(timeStr); netChartObj.data.datasets[0].data.push(rxSpeed); netChartObj.data.datasets[1].data.push(txSpeed); if(netChartObj.data.labels.length > 30) { netChartObj.data.labels.shift(); netChartObj.data.datasets[0].data.shift(); netChartObj.data.datasets[1].data.shift(); } netChartObj.update(); }
}
-
- prevNetIn = currentNetIn; prevNetOut = currentNetOut; prevTime = now;
- const timeStr = new Date().toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
-
- perfChartObj.data.labels.push(timeStr); perfChartObj.data.datasets[0].data.push(cpu); perfChartObj.data.datasets[1].data.push(ram); if(perfChartObj.data.labels.length > 30) { perfChartObj.data.labels.shift(); perfChartObj.data.datasets[0].data.shift(); perfChartObj.data.datasets[1].data.shift(); } perfChartObj.update();
- if(prevTime !== null || d.is_rrd_net) { netChartObj.data.labels.push(timeStr); netChartObj.data.datasets[0].data.push(rxSpeed); netChartObj.data.datasets[1].data.push(txSpeed); if(netChartObj.data.labels.length > 30) { netChartObj.data.labels.shift(); netChartObj.data.datasets[0].data.shift(); netChartObj.data.datasets[1].data.shift(); } netChartObj.update(); }
+ } catch(e) {}
+ };
+ fetchLiveData(); liveInterval = setInterval(fetchLiveData, 2000);
+ } else {
+ try {
+ const res = await (await fetch(`api.php?action=get_historical_rrd&timeframe=${tab}&target_mode=${currentGraphParams.mode}&vmid=${currentGraphParams.vmid}&host=${currentGraphParams.host}&node_id=${currentGraphParams.nodeId}`)).json();
+ if(res.success && res.data) {
+ res.data.forEach(d => {
+ const date = new Date(d.time * 1000); const l = tab === 'day' ? date.toLocaleTimeString('de-DE',{hour:'2-digit',minute:'2-digit'}) : date.toLocaleDateString('de-DE',{weekday:'short', hour:'2-digit'});
+ perfChartObj.data.labels.push(l); perfChartObj.data.datasets[0].data.push((d.cpu || 0) * 100);
+ let ram = 0; if (d.maxmem > 0) { ram = ((d.mem / d.maxmem) * 100).toFixed(1); } perfChartObj.data.datasets[1].data.push(ram);
+ netChartObj.data.labels.push(l); netChartObj.data.datasets[0].data.push((d.netin || 0) / (1024*1024)); netChartObj.data.datasets[1].data.push((d.netout || 0) / (1024*1024));
+ });
+ perfChartObj.update(); netChartObj.update();
}
} catch(e) {}
- };
- fetchLiveData(); liveInterval = setInterval(fetchLiveData, 2000);
+ }
}
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.closeVncConsole = function() {}
+ 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.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.'; } }
- 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.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.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.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.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}
`;
- 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; }
- }
+ 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}
`; 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.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) {}
- }
-
+ 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 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', document.getElementById('cronSchedule').value);
- try { const res = await (await fetch('api.php?action=add_cron_job', {method: 'POST', body: fd})).json(); if(res.success) { addCronForm.reset(); document.getElementById('cronSchedule').value = '0 3 * * *'; checkCronAction(); loadCronJobs(); } else alert('Fehler.'); } catch(e) {} finally { btn.innerText = oTxt; }
+
+ 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 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; }
});
}
@@ -659,23 +287,6 @@ if (window.APP.isLoggedIn && window.APP.nodeCount > 0) {
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.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'); }
}
\ No newline at end of file
diff --git a/src/cron.php b/src/cron.php
index a923eb6..a1a27e2 100644
--- a/src/cron.php
+++ b/src/cron.php
@@ -2,9 +2,14 @@
// /home/docker/pve_dashboard/src/cron.php
require_once __DIR__ . '/db.php';
+// FIX 1: Harte Zeitzone für die PHP-CLI (überschreibt Docker UTC)
+date_default_timezone_set('Europe/Berlin');
+
function isCronMatch($cron, $time = null) {
if ($time === null) $time = time();
- $cronParts = explode(' ', trim($cron));
+
+ // FIX 2: Ignoriert beliebig viele Leerzeichen zwischen den Werten
+ $cronParts = preg_split('/\s+/', trim($cron));
if (count($cronParts) !== 5) return false;
list($min, $hour, $day, $month, $weekday) = $cronParts;
@@ -18,7 +23,10 @@ function isCronMatch($cron, $time = null) {
function matchCronPart($part, $current) {
if ($part === '*') return true;
- if ($part === (string)(int)$current) return true;
+
+ // FIX 3: Sauberer Integer-Vergleich für führende Nullen ("00" wird zu 0)
+ if (is_numeric($part) && (int)$part === (int)$current) return true;
+
if (strpos($part, '*/') === 0) {
$step = (int)substr($part, 2);
return $step > 0 && ((int)$current % $step) === 0;
@@ -26,7 +34,7 @@ function matchCronPart($part, $current) {
return false;
}
-// Mini-Proxmox-Fetcher für das Cron-Skript (Mit Token-Auth für PVE!)
+// Mini-Proxmox-Fetcher für das Cron-Skript
function cronProxmoxData($ip, $tokenId, $tokenSecret, $endpoint, $method = "POST", $postData = null) {
$headers = ["Authorization: PVEAPIToken={$tokenId}={$tokenSecret}"];
$ch = curl_init("https://{$ip}:8006{$endpoint}");
diff --git a/src/index.php b/src/index.php
index 59505bc..40d1964 100644
--- a/src/index.php
+++ b/src/index.php
@@ -120,7 +120,7 @@ $nodeCount = $stmt->fetchColumn();
Datacenter Storage
Lade...
- System Updates (APT)
Lade...
Prüfe Updates...
+ System Updates (APT)
Lade...
Prüfe Updates...
diff --git a/src/modals.php b/src/modals.php
index d6976fe..6486637 100644
--- a/src/modals.php
+++ b/src/modals.php
@@ -11,15 +11,48 @@
-
Live Performance
Metriken werden live im 2-Sekunden-Takt abgefragt.
+
+
+
+
+
Performance
+
+
+
+
+
+
+
+
+
+
Metriken werden live abgefragt.
+
+
+
-
🗄️ Gespeicherte Backups
Lade...
🧹 Verify & Garbage Collection
Lade...
🔄 Eingerichtete Sync Jobs
Lade...
📋 Letzte 100 PBS Jobs (Historie)
| Start | Job Typ | Status | Dauer | Log |
|---|
+
Datastore
🗄️ Gespeicherte Backups
Lade...
🧹 Verify & Garbage Collection
Lade...
🔄 Eingerichtete Sync Jobs
Lade...
📋 Letzte 100 PBS Jobs (Historie)
| Start | Job Typ | Status | Dauer | Log |
|---|
-
Zurückgehaltene Mails
| Datum | Absender | Empfänger | Betreff | Score | Aktion |
|---|
| Zeit | Absender | Empfänger | Status |
|---|
Aktuelle Postfix Warteschlangen
| Queue | Zeit | Absender | Empfänger | Grund |
|---|
⚙️ Mail Proxy Konfiguration
🔒 SSL Zertifikate
Neues Zertifikat installieren
+
Mail Gateway
Zurückgehaltene Mails
| Datum | Absender | Empfänger | Betreff | Score | Aktion |
|---|
| Zeit | Absender | Empfänger | Status |
|---|
Aktuelle Postfix Warteschlangen
| Queue | Zeit | Absender | Empfänger | Grund |
|---|
⚙️ Mail Proxy Konfiguration
🔒 SSL Zertifikate
Neues Zertifikat installieren
-
+
+
+
+
+
📦 System Updates (APT)
+
+
+
+
Prüfe alle Nodes auf Paket-Updates...
+
+
+
+
+
@@ -36,7 +69,7 @@
-
+
-
-
-
Beispiel: "0 3 * * *" = Jeden Tag um 03:00 Uhr.
+
+
+
+
+
+
@@ -68,56 +109,9 @@
-
-
-
-
-
-
-
- Aktivitäten-Protokoll (Audit Log)
-
-
-
-
-
-
-
-
- | Datum / Uhrzeit | Benutzer | Aktion | Ziel / Details |
-
-
-
-
-
-
-
-
-
-
-
-
🔑 Passwort ändern
-
-
-
-
-
\ No newline at end of file
+
+ Aktivitäten-Protokoll (Audit Log)
| Datum / Uhrzeit | Benutzer | Aktion | Ziel / Details |
|---|
+
\ No newline at end of file