14 Commits
8 changed files with 798 additions and 799 deletions

No files matched your search

+8 -5
View File
@@ -1,5 +1,7 @@
# Proxmox Unified Console (PUC) 🚀 # Proxmox Unified Console (PUC) 🚀
![PUC Dashboard Preview](assets/dashboard.png)
A lightning-fast, unified web dashboard to manage your **Proxmox Virtual Environment (PVE)**, **Proxmox Backup Server (PBS)**, and **Proxmox Mail Gateway (PMG)** from a single, clean interface. A lightning-fast, unified web dashboard to manage your **Proxmox Virtual Environment (PVE)**, **Proxmox Backup Server (PBS)**, and **Proxmox Mail Gateway (PMG)** from a single, clean interface.
Built entirely with native APIs—no slow iframes, no CORS issues. Built entirely with native APIs—no slow iframes, no CORS issues.
@@ -58,19 +60,20 @@ services:
- ./data:/var/www/data - ./data:/var/www/data
environment: environment:
- TZ=Europe/Berlin - TZ=Europe/Berlin
```
2. Starten: 2. Starten:
```
Bash Bash
docker compose up -d docker compose up -d
```
3. ⚠️ WICHTIG: Rechte für SQLite anpassen: 3. ⚠️ WICHTIG: Rechte für SQLite anpassen:
Da der Container als Benutzer www-data (UID 33) läuft, der neu erstellte Volume-Ordner auf dem Host aber oft root gehört, muss dem Ordner die Schreibberechtigung erteilt werden. Führe im Verzeichnis der Compose-Datei aus: Da der Container als Benutzer www-data (UID 33) läuft, der neu erstellte Volume-Ordner auf dem Host aber oft root gehört, muss dem Ordner die Schreibberechtigung erteilt werden. Führe im Verzeichnis der Compose-Datei aus:
Bash Bash
```
sudo chown -R 33:33 ./data sudo chown -R 33:33 ./data
sudo chmod -R 775 ./data sudo chmod -R 775 ./data
```
4. Login: 4. Login:
Rufe https://<DEINE-IP>:8443 in deinem Browser auf (Zertifikatswarnung ignorieren). Rufe https://<DEINE-IP>:8443 in deinem Browser auf (Zertifikatswarnung ignorieren).
@@ -93,4 +96,4 @@ Rufe https://<DEINE-IP>:8443 in deinem Browser auf (Zertifikatswarnung ignoriere
Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
📄 License 📄 License
MIT MIT
Binary file not shown.

After

Width:  |  Height:  |  Size: 280 KiB

+26 -35
View File
@@ -1,6 +1,6 @@
<?php <?php
// /home/docker/pve_dashboard/src/api.php // /home/docker/pve_dashboard/src/api.php
ini_set('display_errors', 0); // Verhindert, dass PHP-Warnungen das JSON zerstören ini_set('display_errors', 0);
error_reporting(E_ALL); error_reporting(E_ALL);
require_once 'db.php'; require_once 'db.php';
@@ -60,7 +60,6 @@ if (!isset($_SESSION['user_id'])) {
exit; exit;
} }
// === PASSWORT ÄNDERN ===
if ($action === 'change_password') { if ($action === 'change_password') {
$oldPass = $_POST['old_password'] ?? ''; $oldPass = $_POST['old_password'] ?? '';
$newPass = $_POST['new_password'] ?? ''; $newPass = $_POST['new_password'] ?? '';
@@ -86,44 +85,37 @@ if ($action === 'change_password') {
$isAdmin = ($_SESSION['role'] ?? '') === 'admin'; $isAdmin = ($_SESSION['role'] ?? '') === 'admin';
@session_write_close(); @session_write_close();
// === PROXMOX API FETCHER (JETZT MIT TICKET-AUTH FÜR ALLE SYSTEME) === // === PROXMOX API FETCHER ===
function getProxmoxData($ip, $tokenId, $tokenSecret, $endpoint, $type = 'pve', $method = "GET", $postData = null, $timeout = 6) { function getProxmoxData($ip, $tokenId, $tokenSecret, $endpoint, $type = 'pve', $method = "GET", $postData = null, $timeout = 6) {
$port = ($type === 'pbs') ? 8007 : 8006; $port = ($type === 'pbs') ? 8007 : 8006;
// 1. Ticket holen (Login) if ($type === 'pmg' || $type === 'pbs') {
$chAuth = curl_init("https://{$ip}:{$port}/api2/json/access/ticket"); // TICKET AUTH für PBS und PMG
curl_setopt_array($chAuth, [ $chAuth = curl_init("https://{$ip}:{$port}/api2/json/access/ticket");
CURLOPT_RETURNTRANSFER => true, curl_setopt_array($chAuth, [
CURLOPT_SSL_VERIFYPEER => false, CURLOPT_RETURNTRANSFER => true, CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_SSL_VERIFYHOST => false, CURLOPT_SSL_VERIFYHOST => false, CURLOPT_TIMEOUT => 4,
CURLOPT_TIMEOUT => 4, CURLOPT_POST => true,
CURLOPT_POST => true, CURLOPT_POSTFIELDS => http_build_query(['username' => $tokenId, 'password' => $tokenSecret])
CURLOPT_POSTFIELDS => http_build_query(['username' => $tokenId, 'password' => $tokenSecret]) ]);
]); $authRes = json_decode(curl_exec($chAuth), true); curl_close($chAuth);
$authRes = json_decode(curl_exec($chAuth), true);
curl_close($chAuth); if(!isset($authRes['data']['ticket'])) return ['data' => null, 'error' => 'Auth failed'];
if(!isset($authRes['data']['ticket'])) { $cookieName = ($type === 'pbs') ? 'PBSAuthCookie' : 'PMGAuthCookie';
return ['data' => null, 'error' => 'Authentifizierung fehlgeschlagen']; $headers = [
"Cookie: {$cookieName}=" . $authRes['data']['ticket'],
"CSRFPreventionToken: " . $authRes['data']['CSRFPreventionToken']
];
} else {
// NATIVE API TOKENS FÜR PVE
$headers = ["Authorization: PVEAPIToken={$tokenId}={$tokenSecret}"];
} }
// 2. Passendes Cookie für das System wählen
$cookieName = 'PVEAuthCookie';
if ($type === 'pbs') $cookieName = 'PBSAuthCookie';
if ($type === 'pmg') $cookieName = 'PMGAuthCookie';
$headers = [
"Cookie: {$cookieName}=" . $authRes['data']['ticket'],
"CSRFPreventionToken: " . $authRes['data']['CSRFPreventionToken']
];
// 3. Eigentlichen API-Call ausführen
$ch = curl_init("https://{$ip}:{$port}{$endpoint}"); $ch = curl_init("https://{$ip}:{$port}{$endpoint}");
$options = [ $options = [
CURLOPT_RETURNTRANSFER => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_SSL_VERIFYPEER => false, CURLOPT_SSL_VERIFYHOST => false, CURLOPT_TIMEOUT => $timeout,
CURLOPT_SSL_VERIFYHOST => false,
CURLOPT_TIMEOUT => $timeout,
CURLOPT_HTTPHEADER => $headers CURLOPT_HTTPHEADER => $headers
]; ];
@@ -135,8 +127,7 @@ function getProxmoxData($ip, $tokenId, $tokenSecret, $endpoint, $type = 'pve', $
} }
curl_setopt_array($ch, $options); curl_setopt_array($ch, $options);
$res = curl_exec($ch); $res = curl_exec($ch); curl_close($ch);
curl_close($ch);
return json_decode($res, true) ?: []; return json_decode($res, true) ?: [];
} }
+222 -49
View File
@@ -1,18 +1,48 @@
<?php <?php
// /home/docker/pve_dashboard/src/api_pve.php // /home/docker/pve_dashboard/src/api_pve.php
if (!defined('PDO::ATTR_DRIVER_NAME')) exit; // Schutz if (!defined('PDO::ATTR_DRIVER_NAME')) exit;
if ($action === 'get_stats') { if ($action === 'get_stats') {
$stmt = $pdo->query("SELECT * FROM nodes WHERE type = 'pve'"); $nodes = $stmt->fetchAll(PDO::FETCH_ASSOC); $stmt = $pdo->query("SELECT * FROM nodes WHERE type = 'pve'");
$data = ['cpu_cores' => 0, 'cpu_used' => 0, 'ram_total' => 0, 'ram_used' => 0, 'disk_total' => 0, 'disk_used' => 0, 'cpu_percent' => 0]; $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) { foreach ($nodes as $node) {
$nData = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes"); $nData = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes");
if (isset($nData['data'])) { if (isset($nData['data'])) {
foreach ($nData['data'] as $nInfo) { foreach ($nData['data'] as $nInfo) {
if ($nInfo['status'] === 'online') { if (!isset($seenNodes[$nInfo['node']])) {
$data['cpu_cores'] += $nInfo['maxcpu'] ?? 0; $data['cpu_used'] += ($nInfo['cpu'] ?? 0) * ($nInfo['maxcpu'] ?? 0); $seenNodes[$nInfo['node']] = true;
$data['ram_total'] += $nInfo['maxmem'] ?? 0; $data['ram_used'] += $nInfo['mem'] ?? 0; if ($nInfo['status'] === 'online') {
$data['disk_total'] += $nInfo['maxdisk'] ?? 0; $data['disk_used'] += $nInfo['disk'] ?? 0; $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 = 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']++;
} }
} }
} }
@@ -22,60 +52,112 @@ if ($action === 'get_stats') {
} }
if ($action === 'get_top_vms') { if ($action === 'get_top_vms') {
$stmt = $pdo->query("SELECT * FROM nodes WHERE type = 'pve'"); $nodes = $stmt->fetchAll(PDO::FETCH_ASSOC); $allVms = []; $stmt = $pdo->query("SELECT * FROM nodes WHERE type = 'pve'"); $nodes = $stmt->fetchAll(PDO::FETCH_ASSOC); $allVms = []; $seenVms = [];
foreach ($nodes as $node) { foreach ($nodes as $node) {
$vms = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/cluster/resources?type=vm"); $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) { if (checkVmPermission($pdo, $vm['vmid'])) { $vm['node_id'] = $node['id']; $vm['node_ip'] = $node['ip_address']; $allVms[] = $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); }); usort($allVms, function($a, $b) { return ($b['cpu'] ?? 0) <=> ($a['cpu'] ?? 0); });
echo json_encode(['success' => true, 'data' => array_slice($allVms, 0, 5)]); exit; echo json_encode(['success' => true, 'data' => array_slice($allVms, 0, 5)]); exit;
} }
if ($action === 'get_all_vms') { if ($action === 'get_all_vms') {
$stmt = $pdo->query("SELECT * FROM nodes WHERE type = 'pve'"); $nodes = $stmt->fetchAll(PDO::FETCH_ASSOC); $allVms = []; $stmt = $pdo->query("SELECT * FROM nodes WHERE type = 'pve'"); $nodes = $stmt->fetchAll(PDO::FETCH_ASSOC); $allVms = []; $seenVms = [];
foreach ($nodes as $node) { foreach ($nodes as $node) {
$vms = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/cluster/resources?type=vm"); $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) { if (checkVmPermission($pdo, $vm['vmid'])) { $vm['node_id'] = $node['id']; $vm['node_ip'] = $node['ip_address']; $allVms[] = $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']; }); 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; echo json_encode(['success' => true, 'data' => $allVms]); exit;
} }
// NEU: Node Dashboard Daten (VMs & Storage)
if ($action === 'get_node_dashboard') {
if (!$isAdmin) exit;
$stmt = $pdo->prepare("SELECT * FROM nodes WHERE id = ?"); $stmt->execute([$_GET['node_id']]); $node = $stmt->fetch(PDO::FETCH_ASSOC);
$host = $_GET['host'];
$vmsQemu = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$host}/qemu");
$vmsLxc = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$host}/lxc");
$storages = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$host}/storage");
$nodeVms = [];
if (isset($vmsQemu['data'])) { foreach ($vmsQemu['data'] as $v) { $v['type'] = 'qemu'; $v['host'] = $host; $v['node_id'] = $node['id']; $v['node_ip'] = $node['ip_address']; $nodeVms[] = $v; } }
if (isset($vmsLxc['data'])) { foreach ($vmsLxc['data'] as $v) { $v['type'] = 'lxc'; $v['host'] = $host; $v['node_id'] = $node['id']; $v['node_ip'] = $node['ip_address']; $nodeVms[] = $v; } }
usort($nodeVms, function($a, $b) { if ($a['status'] === 'running' && $b['status'] !== 'running') return -1; if ($a['status'] !== 'running' && $b['status'] === 'running') return 1; return $a['vmid'] <=> $b['vmid']; });
echo json_encode(['success' => true, 'vms' => $nodeVms, 'storages' => $storages['data'] ?? []]); exit;
}
if ($action === 'vm_action') { if ($action === 'vm_action') {
if (!checkVmPermission($pdo, $_POST['vmid'])) exit; 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); $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']}", "POST"); $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');
// AUDIT LOG
logAudit("VM {$_POST['cmd']}", "VMID: {$_POST['vmid']} auf Host: {$_POST['host']}"); logAudit("VM {$_POST['cmd']}", "VMID: {$_POST['vmid']} auf Host: {$_POST['host']}");
echo json_encode(['success' => isset($res['data']), 'error' => $res['errors'] ?? 'Fehler']); exit; 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 === '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 === '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 ($action === 'add_node') {
if (!$isAdmin) exit; if (!$isAdmin) exit;
$stmt = $pdo->prepare("INSERT INTO nodes (name, ip_address, token_id, token_secret, type) VALUES (?, ?, ?, ?, ?)"); $name = trim($_POST['name']); $ip = trim($_POST['ip']); $user = trim($_POST['user']); $pass = trim($_POST['pass']); $type = $_POST['type'] ?? 'pve';
$stmt->execute([$_POST['name'], $_POST['ip'], $_POST['user'], $_POST['pass'], $_POST['type'] ?? 'pve']); $tokenIdToSave = $user; $tokenSecretToSave = $pass;
logAudit('Server hinzugefügt', "Node: {$_POST['name']} ({$_POST['ip']})"); 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; 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 === '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 === '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 ($action === 'create_user') {
if (!$isAdmin) exit; if (!$isAdmin) exit;
$stmt = $pdo->prepare("INSERT INTO users (username, password_hash, role, permissions) VALUES (?, ?, ?, ?)"); $stmt = $pdo->prepare("INSERT INTO users (username, password_hash, role, permissions) VALUES (?, ?, ?, ?)"); $stmt->execute([$_POST['username'], password_hash($_POST['password'], PASSWORD_DEFAULT), $_POST['role'], $_POST['permissions']]);
$stmt->execute([$_POST['username'], password_hash($_POST['password'], PASSWORD_DEFAULT), $_POST['role'], $_POST['permissions']]);
logAudit('User angelegt', "Username: {$_POST['username']} Rolle: {$_POST['role']}"); logAudit('User angelegt', "Username: {$_POST['username']} Rolle: {$_POST['role']}");
echo json_encode(['success' => true]); exit; echo json_encode(['success' => true]); exit;
} }
if ($action === 'get_pve_nodes') { if ($action === 'get_pve_nodes') {
if (!$isAdmin) exit; if (!$isAdmin) exit;
$stmt = $pdo->query("SELECT * FROM nodes WHERE type = 'pve'"); $resList = []; $stmt = $pdo->query("SELECT * FROM nodes WHERE type = 'pve'"); $resList = []; $seenNodes = [];
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $node) { foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $node) {
$nData = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes"); $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') { $resList[] = ['node_id' => $node['id'], 'host' => $n['node'], 'display' => $node['name'] . ' -> ' . $n['node']]; } } } 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; echo json_encode(['success' => true, 'data' => $resList]); exit;
} }
@@ -87,9 +169,9 @@ if ($action === 'create_vm') {
if(!isset($nextIdRes['data'])) { echo json_encode(['success' => false, 'error' => 'Konnte keine freie VMID finden.']); exit; } if(!isset($nextIdRes['data'])) { echo json_encode(['success' => false, 'error' => 'Konnte keine freie VMID finden.']); exit; }
$vmid = $nextIdRes['data']; $vmid = $nextIdRes['data'];
$params = ['vmid' => $vmid, 'name' => $_POST['name'], 'memory' => $_POST['memory'], 'cores' => $_POST['cores'], 'net0' => 'virtio,bridge=vmbr0']; $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", "POST", $params); $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']}"); logAudit('VM Erstellt', "VMID: {$vmid} Name: {$_POST['name']}");
echo json_encode(['success' => isset($res['data']), 'vmid' => $vmid]); exit; echo json_encode(['success' => isset($res['data']), 'vmid' => $vmid, 'error' => isset($res['data']) ? '' : 'Fehler bei Erstellung.']); exit;
} }
if ($action === 'get_vm_config') { if ($action === 'get_vm_config') {
@@ -99,6 +181,7 @@ if ($action === 'get_vm_config') {
echo json_encode(['success' => true, 'data' => $res['data'] ?? []]); exit; echo json_encode(['success' => true, 'data' => $res['data'] ?? []]); exit;
} }
// ANGEPASST: Update inkl. ISO und onboot
if ($action === 'update_vm_config') { if ($action === 'update_vm_config') {
if (!checkVmPermission($pdo, $_POST['vmid'])) exit; 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); $stmt = $pdo->prepare("SELECT * FROM nodes WHERE id = ?"); $stmt->execute([$_POST['node_id']]); $node = $stmt->fetch(PDO::FETCH_ASSOC);
@@ -106,9 +189,12 @@ if ($action === 'update_vm_config') {
if(isset($_POST['memory'])) $params['memory'] = $_POST['memory']; if(isset($_POST['memory'])) $params['memory'] = $_POST['memory'];
if(isset($_POST['cores'])) $params['cores'] = $_POST['cores']; if(isset($_POST['cores'])) $params['cores'] = $_POST['cores'];
if(isset($_POST['net0'])) $params['net0'] = $_POST['net0']; 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", "POST", $params); if(isset($_POST['onboot'])) $params['onboot'] = $_POST['onboot'];
if(isset($_POST['ide2'])) $params['ide2'] = $_POST['ide2'] === 'none' ? 'none,media=cdrom' : $_POST['ide2'] . ',media=cdrom';
$res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$_POST['host']}/{$_POST['type']}/{$_POST['vmid']}/config", 'pve', 'POST', $params);
logAudit('VM Config geändert', "VMID: {$_POST['vmid']}"); logAudit('VM Config geändert', "VMID: {$_POST['vmid']}");
echo json_encode(['success' => isset($res['data'])]); exit; echo json_encode(['success' => isset($res['data']), 'error' => 'API Fehler']); exit;
} }
if ($action === 'add_vm_nic') { if ($action === 'add_vm_nic') {
@@ -116,25 +202,60 @@ if ($action === 'add_vm_nic') {
$stmt = $pdo->prepare("SELECT * FROM nodes WHERE id = ?"); $stmt->execute([$_POST['node_id']]); $node = $stmt->fetch(PDO::FETCH_ASSOC); $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"); $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; } } $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", "POST", ["net{$nextNic}" => "virtio,bridge={$_POST['bridge']}"]); $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;
echo json_encode(['success' => isset($res['data']), 'slot' => "net{$nextNic}"]); exit;
} }
if ($action === 'resize_vm_disk') { if ($action === 'resize_vm_disk') {
if (!checkVmPermission($pdo, $_POST['vmid'])) exit; 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); $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", "PUT", ['disk' => $_POST['disk'], 'size' => $_POST['size']]); $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;
echo json_encode(['success' => isset($res['data'])]); exit;
} }
if ($action === 'delete_vm') { if ($action === 'delete_vm') {
if (!$isAdmin) exit; if (!$isAdmin) exit;
$stmt = $pdo->prepare("SELECT * FROM nodes WHERE id = ?"); $stmt->execute([$_POST['node_id']]); $node = $stmt->fetch(PDO::FETCH_ASSOC); $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']}", "DELETE"); $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;
echo json_encode(['success' => isset($res['data'])]); exit; }
// NEU: ISO Images abrufen
if ($action === 'get_isos') {
if (!$isAdmin) exit;
$stmt = $pdo->prepare("SELECT * FROM nodes WHERE id = ?"); $stmt->execute([$_GET['node_id']]); $node = $stmt->fetch(PDO::FETCH_ASSOC);
$stRes = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$_GET['host']}/storage");
$isos = [];
if (isset($stRes['data'])) {
foreach ($stRes['data'] as $st) {
if (strpos($st['content'], 'iso') !== false) {
$cRes = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$_GET['host']}/storage/{$st['storage']}/content");
if (isset($cRes['data'])) {
foreach ($cRes['data'] as $item) {
if ($item['content'] === 'iso') $isos[] = $item['volid'];
}
}
}
}
}
echo json_encode(['success' => true, 'data' => $isos]); exit;
}
// NEU: Node-Spezifische Backup Jobs
if ($action === 'get_node_backup_jobs') {
if (!$isAdmin) exit;
$stmt = $pdo->prepare("SELECT * FROM nodes WHERE id = ?"); $stmt->execute([$_GET['node_id']]); $node = $stmt->fetch(PDO::FETCH_ASSOC);
$res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/cluster/backup");
$hostJobs = [];
if (isset($res['data'])) {
foreach ($res['data'] as $job) {
// Zeige nur Jobs, die diesen Host betreffen (oder alle, falls node nicht spezifiziert in PVE)
if (empty($job['node']) || $job['node'] === $_GET['host']) {
$hostJobs[] = $job;
}
}
}
echo json_encode(['success' => true, 'data' => $hostJobs]); exit;
} }
if ($action === 'get_vm_snapshots') { if ($action === 'get_vm_snapshots') {
@@ -148,11 +269,10 @@ if ($action === 'vm_snapshot_action') {
if (!checkVmPermission($pdo, $_POST['vmid'])) exit; 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); $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']; $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", "POST", ['snapname' => $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}", "DELETE"); } 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", "POST"); } 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;
echo json_encode(['success' => isset($res['data'])]); exit;
} }
if ($action === 'get_backup_storages') { if ($action === 'get_backup_storages') {
@@ -183,25 +303,78 @@ if ($action === 'get_vm_backups') {
if ($action === 'create_backup') { if ($action === 'create_backup') {
if (!checkVmPermission($pdo, $_POST['vmid'])) exit; 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); $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", "POST", ['vmid' => $_POST['vmid'], 'storage' => $_POST['storage'], 'mode' => 'snapshot']); $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;
echo json_encode(['success' => isset($res['data'])]); exit;
} }
if ($action === 'restore_backup') { if ($action === 'restore_backup') {
if (!checkVmPermission($pdo, $_POST['vmid'])) exit; 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); $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"); $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 muss gestoppt sein!']); exit; } 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']}", "POST", ['vmid' => $_POST['vmid'], 'archive' => $_POST['archive'], 'force' => 1]); $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;
echo json_encode(['success' => isset($resRestore['data'])]); exit;
} }
if ($action === 'get_node_status' || $action === 'get_vm_status') { 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); $stmt = $pdo->prepare("SELECT * FROM nodes WHERE id = ?"); $stmt->execute([$_GET['node_id']]); $node = $stmt->fetch(PDO::FETCH_ASSOC);
$endpoint = $action === 'get_node_status' ? "/api2/json/nodes/{$_GET['host']}/status" : "/api2/json/nodes/{$_GET['host']}/{$_GET['type']}/{$_GET['vmid']}/status/current"; $type = $node['type'] ?? 'pve';
$res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], $endpoint);
if ($action === 'get_node_status') {
$nData = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes", $type);
$internalName = $nData['data'][0]['node'] ?? ($type === 'pbs' ? 'localhost' : 'pve');
$res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$internalName}/status", $type);
$rrd = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$internalName}/rrddata?timeframe=hour&cf=AVERAGE", $type);
$netin = 0; $netout = 0;
if (isset($rrd['data']) && is_array($rrd['data'])) {
for ($i = count($rrd['data']) - 1; $i >= 0; $i--) {
if (isset($rrd['data'][$i]['netin']) && $rrd['data'][$i]['netin'] !== null) { $netin = $rrd['data'][$i]['netin']; $netout = $rrd['data'][$i]['netout']; break; }
}
}
$data = $res['data'] ?? []; $data['netin'] = $netin; $data['netout'] = $netout; $data['is_rrd_net'] = true;
echo json_encode(['success' => true, 'data' => $data]); exit;
} else {
$endpoint = "/api2/json/nodes/{$_GET['host']}/{$_GET['type']}/{$_GET['vmid']}/status/current";
$res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], $endpoint, $type);
echo json_encode(['success' => true, 'data' => $res['data'] ?? []]); exit;
}
}
if ($action === 'get_historical_rrd') {
if (!checkVmPermission($pdo, $_GET['vmid'] ?? 0)) exit;
$stmt = $pdo->prepare("SELECT * FROM nodes WHERE id = ?"); $stmt->execute([$_GET['node_id']]); $node = $stmt->fetch(PDO::FETCH_ASSOC);
$timeframe = $_GET['timeframe'] === 'week' ? 'week' : 'day'; $type = $node['type'] ?? 'pve';
if ($_GET['target_mode'] === 'node') {
$nData = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes", $type);
$internalName = $nData['data'][0]['node'] ?? 'localhost';
$endpoint = "/api2/json/nodes/{$internalName}/rrddata?timeframe={$timeframe}&cf=AVERAGE";
} else { $endpoint = "/api2/json/nodes/{$_GET['host']}/{$_GET['target_mode']}/{$_GET['vmid']}/rrddata?timeframe={$timeframe}&cf=AVERAGE"; }
$res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], $endpoint, $type);
echo json_encode(['success' => true, 'data' => $res['data'] ?? []]); exit; echo json_encode(['success' => true, 'data' => $res['data'] ?? []]); exit;
} }
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'] ]; }
}
}
}
echo json_encode(['success' => true, 'data' => $updateList]); exit;
}
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');
echo json_encode(['success' => isset($res['data']), 'upid' => $res['data'] ?? '', 'error' => $res['errors'] ?? 'API Fehler']); exit;
}
?> ?>
+416 -536
View File
File diff suppressed because it is too large. Load diff
+12 -10
View File
@@ -2,10 +2,14 @@
// /home/docker/pve_dashboard/src/cron.php // /home/docker/pve_dashboard/src/cron.php
require_once __DIR__ . '/db.php'; require_once __DIR__ . '/db.php';
// Hilfsfunktion: Prüft, ob ein Cron-Ausdruck (z.B. "0 3 * * *") zur aktuellen Zeit passt // FIX 1: Harte Zeitzone für die PHP-CLI (überschreibt Docker UTC)
date_default_timezone_set('Europe/Berlin');
function isCronMatch($cron, $time = null) { function isCronMatch($cron, $time = null) {
if ($time === null) $time = time(); 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; if (count($cronParts) !== 5) return false;
list($min, $hour, $day, $month, $weekday) = $cronParts; list($min, $hour, $day, $month, $weekday) = $cronParts;
@@ -19,8 +23,11 @@ function isCronMatch($cron, $time = null) {
function matchCronPart($part, $current) { function matchCronPart($part, $current) {
if ($part === '*') return true; if ($part === '*') return true;
if ($part === (string)(int)$current) return true;
if (strpos($part, '*/') === 0) { // Unterstützt z.B. */5 für "alle 5 Minuten" // 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); $step = (int)substr($part, 2);
return $step > 0 && ((int)$current % $step) === 0; return $step > 0 && ((int)$current % $step) === 0;
} }
@@ -39,12 +46,11 @@ function cronProxmoxData($ip, $tokenId, $tokenSecret, $endpoint, $method = "POST
if ($postData) $options[CURLOPT_POSTFIELDS] = http_build_query($postData); if ($postData) $options[CURLOPT_POSTFIELDS] = http_build_query($postData);
curl_setopt_array($ch, $options); curl_setopt_array($ch, $options);
$res = curl_exec($ch); curl_close($ch); $res = curl_exec($ch); curl_close($ch);
return json_decode($res, true); return json_decode($res, true) ?: [];
} }
echo "[" . date('Y-m-d H:i:s') . "] Starte Scheduler-Check...\n"; echo "[" . date('Y-m-d H:i:s') . "] Starte Scheduler-Check...\n";
// Hole alle aktiven Tasks
$stmt = $pdo->query("SELECT * FROM scheduled_tasks WHERE is_active = 1"); $stmt = $pdo->query("SELECT * FROM scheduled_tasks WHERE is_active = 1");
$tasks = $stmt->fetchAll(PDO::FETCH_ASSOC); $tasks = $stmt->fetchAll(PDO::FETCH_ASSOC);
@@ -59,7 +65,6 @@ foreach ($tasks as $task) {
if ($node) { if ($node) {
$success = false; $success = false;
// AKTION: PVE NODE NEUSTART
if ($task['action_type'] === 'reboot_node') { if ($task['action_type'] === 'reboot_node') {
$nData = cronProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes", 'GET'); $nData = cronProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes", 'GET');
if (isset($nData['data'][0]['node'])) { if (isset($nData['data'][0]['node'])) {
@@ -68,9 +73,7 @@ foreach ($tasks as $task) {
$success = true; $success = true;
} }
} }
// AKTION: VM START / STOP / REBOOT
elseif (in_array($task['action_type'], ['start_vm', 'stop_vm', 'reboot_vm'])) { elseif (in_array($task['action_type'], ['start_vm', 'stop_vm', 'reboot_vm'])) {
// Suche Host und Typ der VM
$vmsRes = cronProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/cluster/resources?type=vm", 'GET'); $vmsRes = cronProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/cluster/resources?type=vm", 'GET');
if (isset($vmsRes['data'])) { if (isset($vmsRes['data'])) {
foreach ($vmsRes['data'] as $vm) { foreach ($vmsRes['data'] as $vm) {
@@ -85,7 +88,6 @@ foreach ($tasks as $task) {
} }
} }
// Setze den Zeitstempel für den letzten Durchlauf
if ($success) { if ($success) {
$uStmt = $pdo->prepare("UPDATE scheduled_tasks SET last_run = ? WHERE id = ?"); $uStmt = $pdo->prepare("UPDATE scheduled_tasks SET last_run = ? WHERE id = ?");
$uStmt->execute([time(), $task['id']]); $uStmt->execute([time(), $task['id']]);
+101 -58
View File
@@ -38,100 +38,143 @@ $nodeCount = $stmt->fetchColumn();
<?php if ($isLoggedIn): ?> <?php if ($isLoggedIn): ?>
<div class="flex items-center gap-4 text-sm"> <div class="flex items-center gap-4 text-sm">
<span class="text-gray-400">Hallo, <span class="text-white font-bold"><?= htmlspecialchars($_SESSION['username']) ?></span></span> <span class="text-gray-400">Hallo, <span class="text-white font-bold"><?= htmlspecialchars($_SESSION['username']) ?></span></span>
<button onclick="openPasswordModal()" class="text-gray-400 hover:text-white transition-colors" title="Passwort ändern">🔑</button>
<button onclick="openPasswordModal()" class="text-gray-400 hover:text-white transition-colors" title="Passwort ändern">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4v-3.286l5.742-5.742C9.4 11.135 9 10.126 9 9a6 6 0 0112 0z"></path></svg>
</button>
<button onclick="logout()" class="text-red-400 hover:text-red-300 font-medium transition-colors">Abmelden</button> <button onclick="logout()" class="text-red-400 hover:text-red-300 font-medium transition-colors">Abmelden</button>
</div> </div>
<?php else: ?>
<div class="text-sm text-gray-400">System Status: <span class="text-green-500 font-bold">Sicher</span></div>
<?php endif; ?> <?php endif; ?>
</div> </div>
</header> </header>
<main class="flex-grow p-6 flex items-center justify-center max-w-[1600px] w-full mx-auto"> <main class="flex-grow p-6 flex items-center justify-center max-w-[1600px] w-full mx-auto">
<?php if (!$isLoggedIn): ?> <?php if (!$isLoggedIn): ?>
<div class="bg-darkcard border border-darkborder rounded-xl shadow-2xl p-8 max-w-sm w-full"><div class="flex justify-center mb-6"><div class="w-12 h-12 rounded bg-proxmox/10 flex items-center justify-center text-proxmox shadow-lg"><svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"></path></svg></div></div><h2 class="text-2xl font-bold mb-2 text-center text-white">Login</h2><form id="loginForm" class="space-y-4"><div><label class="block text-sm font-medium text-gray-400 mb-1">Benutzername</label><input type="text" id="loginUser" class="w-full bg-darkbg border border-darkborder rounded p-2.5 text-white focus:outline-none focus:border-proxmox" required></div><div><label class="block text-sm font-medium text-gray-400 mb-1">Passwort</label><input type="password" id="loginPass" class="w-full bg-darkbg border border-darkborder rounded p-2.5 text-white focus:outline-none focus:border-proxmox" required></div><button type="submit" class="w-full bg-proxmox hover:bg-orange-600 text-white font-bold py-3 px-4 rounded-lg transition-colors mt-4 shadow-lg shadow-orange-900/50">Einloggen</button></form></div> <!-- Login -->
<div class="bg-darkcard border border-darkborder rounded-xl shadow-2xl p-8 max-w-sm w-full"><h2 class="text-2xl font-bold mb-2 text-center text-white">Login</h2><form id="loginForm" class="space-y-4"><div><label class="block text-sm font-medium text-gray-400 mb-1">Benutzer</label><input type="text" id="loginUser" class="w-full bg-darkbg border border-darkborder rounded p-2.5 text-white" required></div><div><label class="block text-sm font-medium text-gray-400 mb-1">Passwort</label><input type="password" id="loginPass" class="w-full bg-darkbg border border-darkborder rounded p-2.5 text-white" required></div><button type="submit" class="w-full bg-proxmox hover:bg-orange-600 text-white font-bold py-3 px-4 rounded-lg mt-4">Einloggen</button></form></div>
<?php elseif ($nodeCount == 0): ?> <?php elseif ($nodeCount == 0): ?>
<div class="bg-darkcard border border-darkborder rounded-xl shadow-2xl p-8 max-w-md w-full"><h2 class="text-2xl font-bold mb-2 text-white">Willkommen! 👋</h2><p class="text-gray-400 text-sm mb-6">Verbinde deinen ersten Host, um zu starten.</p><form id="setupForm" class="space-y-4"><div><label class="block text-sm font-medium text-gray-400 mb-1">Anzeigename</label><input type="text" id="nodeName" class="w-full bg-darkbg border border-darkborder rounded p-2.5 text-white focus:outline-none focus:border-proxmox" required></div><div><label class="block text-sm font-medium text-gray-400 mb-1">IP-Adresse</label><input type="text" id="nodeIp" class="w-full bg-darkbg border border-darkborder rounded p-2.5 text-white focus:outline-none focus:border-proxmox" required></div><div><label class="block text-sm font-medium text-gray-400 mb-1">Benutzername</label><input type="text" id="nodeUser" value="root@pam" class="w-full bg-darkbg border border-darkborder rounded p-2.5 text-white focus:outline-none focus:border-proxmox" required></div><div><label class="block text-sm font-medium text-gray-400 mb-1">Passwort</label><input type="password" id="nodePass" class="w-full bg-darkbg border border-darkborder rounded p-2.5 text-white focus:outline-none focus:border-proxmox" required></div><button type="submit" class="w-full bg-proxmox hover:bg-orange-600 text-white font-bold py-3 px-4 rounded-lg transition-colors mt-6 shadow-lg shadow-orange-900/50">Node verbinden</button></form></div> <!-- Setup -->
<div class="bg-darkcard border border-darkborder rounded-xl shadow-2xl p-8 max-w-md w-full"><h2 class="text-2xl font-bold mb-2 text-white">Willkommen! 👋</h2><form id="setupForm" class="space-y-4"><div><label class="block text-sm font-medium text-gray-400 mb-1">Name</label><input type="text" id="nodeName" class="w-full bg-darkbg border border-darkborder rounded p-2.5 text-white" required></div><div><label class="block text-sm font-medium text-gray-400 mb-1">IP</label><input type="text" id="nodeIp" class="w-full bg-darkbg border border-darkborder rounded p-2.5 text-white" required></div><div><label class="block text-sm font-medium text-gray-400 mb-1">Benutzer</label><input type="text" id="nodeUser" value="root@pam" class="w-full bg-darkbg border border-darkborder rounded p-2.5 text-white" required></div><div><label class="block text-sm font-medium text-gray-400 mb-1">Passwort</label><input type="password" id="nodePass" class="w-full bg-darkbg border border-darkborder rounded p-2.5 text-white" required></div><button type="submit" class="w-full bg-proxmox text-white font-bold py-3 rounded-lg mt-6">Node verbinden</button></form></div>
<?php else: ?> <?php else: ?>
<div class="w-full h-[85vh] flex bg-darkbg overflow-hidden rounded-xl border border-darkborder shadow-2xl"> <div class="w-full h-[85vh] flex bg-darkbg overflow-hidden rounded-xl border border-darkborder shadow-2xl">
<!-- SIDEBAR --> <!-- SIDEBAR -->
<aside class="w-64 bg-darkcard border-r border-darkborder flex flex-col"> <aside class="w-64 bg-darkcard border-r border-darkborder flex flex-col shrink-0">
<div class="p-4 border-b border-darkborder"><span class="text-xs font-bold text-gray-500 uppercase tracking-wider">Dashboards</span></div> <nav class="flex-1 overflow-y-auto space-y-2 py-4">
<nav class="flex-1 overflow-y-auto p-4 space-y-2"> <div class="px-4 mb-2"><span class="text-xs font-bold text-gray-500 uppercase tracking-wider">Cluster Übersicht</span></div>
<a href="#" id="nav-tab-pve" onclick="switchTab('pve')" class="tab-active flex items-center gap-3 text-gray-400 hover:text-white px-3 py-2 rounded-r-lg transition-colors"><svg class="w-5 h-5 text-proxmox" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z"></path></svg> PVE Cluster</a> <a href="#" id="nav-tab-pve" onclick="switchTab('pve')" class="tab-active flex items-center gap-3 text-gray-400 hover:text-white px-3 py-2 rounded-r-lg transition-colors">📊 PVE Cluster</a>
<a href="#" id="nav-tab-pbs" onclick="switchTab('pbs')" class="flex items-center gap-3 text-gray-400 hover:text-white hover:bg-white/5 px-3 py-2 rounded-r-lg transition-colors"><svg class="w-5 h-5 text-pbs" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7v8a2 2 0 002 2h6M8 7V5a2 2 0 012-2h4.586a1 1 0 01.707.293l4.414 4.414a1 1 0 01.293.707V15a2 2 0 01-2 2h-2M8 7H6a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2v-2"></path></svg> Backup Server (PBS)</a>
<a href="#" id="nav-tab-pmg" onclick="switchTab('pmg')" class="flex items-center gap-3 text-gray-400 hover:text-white hover:bg-white/5 px-3 py-2 rounded-r-lg transition-colors"><svg class="w-5 h-5 text-pmg" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path></svg> Mail Gateway (PMG)</a> <div class="mt-4 border-t border-darkborder pt-4">
<div class="px-4 mb-2"><span class="text-xs font-bold text-gray-500 uppercase tracking-wider">PVE Nodes</span></div>
<div id="sidebar-pve-nodes" class="space-y-1">
<!-- Nodes per JS -->
</div>
</div>
<div class="pt-4 mt-4 border-t border-darkborder space-y-2"> <div class="mt-4 border-t border-darkborder pt-4 space-y-2">
<a href="#" onclick="openNodeTopology()" class="flex items-center gap-3 text-gray-400 hover:text-white hover:bg-white/5 px-3 py-2 rounded-lg transition-colors"><svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"></path></svg> Cluster Topologie</a> <div class="px-4 mb-2"><span class="text-xs font-bold text-gray-500 uppercase tracking-wider">Erweiterungen</span></div>
<a href="#" onclick="openVmManager()" class="flex items-center gap-3 text-gray-400 hover:text-white hover:bg-white/5 px-3 py-2 rounded-lg transition-colors"><svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01"></path></svg> VMs verwalten</a> <a href="#" id="nav-tab-pbs" onclick="switchTab('pbs')" class="flex items-center gap-3 text-gray-400 hover:text-white px-3 py-2 rounded-r-lg">🛡️ Backup Server (PBS)</a>
<a href="#" onclick="openNodeManager()" class="flex items-center gap-3 text-gray-400 hover:text-white hover:bg-white/5 px-3 py-2 rounded-lg transition-colors"><svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path></svg> Server & API</a> <a href="#" id="nav-tab-pmg" onclick="switchTab('pmg')" class="flex items-center gap-3 text-gray-400 hover:text-white px-3 py-2 rounded-r-lg">✉️ Mail Gateway (PMG)</a>
<?php if(($_SESSION['role'] ?? '') === 'admin'): ?>
<a href="#" onclick="openCronManager()" class="flex items-center gap-3 text-gray-400 hover:text-white hover:bg-white/5 px-3 py-2 rounded-lg transition-colors"><svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3"></path></svg> Task Scheduler</a>
<a href="#" onclick="openUserManager()" class="flex items-center gap-3 text-gray-400 hover:text-white hover:bg-white/5 px-3 py-2 rounded-lg transition-colors"><svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"></path></svg> Benutzerverwaltung</a>
<?php endif; ?>
</div> </div>
</nav> </nav>
<div class="p-4 border-t border-darkborder space-y-2 text-sm">
<a href="#" onclick="openNodeTopology()" class="block text-gray-400 hover:text-white">Cluster Topologie</a>
<a href="#" onclick="openVmManager()" class="block text-gray-400 hover:text-white">VMs verwalten</a>
<a href="#" onclick="openNodeManager()" class="block text-gray-400 hover:text-white">Server & API</a>
<?php if(($_SESSION['role'] ?? '') === 'admin'): ?>
<a href="#" onclick="openCronManager()" class="block text-gray-400 hover:text-white">Task Scheduler</a>
<a href="#" onclick="openUserManager()" class="block text-gray-400 hover:text-white">Benutzerverwaltung</a>
<?php endif; ?>
</div>
</aside> </aside>
<div class="flex-1 flex flex-col overflow-hidden relative"> <div class="flex-1 flex flex-col overflow-hidden relative">
<main class="absolute inset-0 overflow-x-hidden overflow-y-auto bg-darkbg p-6 flex gap-6 w-full"> <main class="absolute inset-0 overflow-x-hidden overflow-y-auto bg-darkbg p-6 flex gap-6 w-full">
<!-- TAB 1: PVE --> <!-- TAB 1: PVE CLUSTER (Global) -->
<div id="tab-pve" class="flex-1 flex flex-col min-w-0 transition-opacity duration-300"> <div id="tab-pve" class="flex-1 flex flex-col min-w-0 transition-opacity duration-300">
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-6"> <!-- Top Kachel Cluster Status -->
<div class="bg-darkcard border border-darkborder rounded-xl p-5 shadow-lg"><div class="flex justify-between items-start mb-4"><div><p class="text-gray-400 text-sm font-medium">Cluster CPU Cores</p><h3 id="stat-cpu-text" class="text-2xl font-bold text-white mt-1">Lade...</h3></div><div class="p-2 bg-blue-500/10 rounded-lg"><svg class="w-6 h-6 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z"></path></svg></div></div><div class="w-full bg-darkbg rounded-full h-2"><div id="stat-cpu-bar" class="bg-blue-500 h-2 rounded-full" style="width: 0%"></div></div></div> <div class="mb-6 bg-darkcard border border-darkborder rounded-xl p-6 shadow-lg flex flex-col md:flex-row justify-between items-center gap-6">
<div class="bg-darkcard border border-darkborder rounded-xl p-5 shadow-lg"><div class="flex justify-between items-start mb-4"><div><p class="text-gray-400 text-sm font-medium">Globaler RAM</p><h3 id="stat-ram-text" class="text-2xl font-bold text-white mt-1">Lade...</h3></div><div class="p-2 bg-proxmox/10 rounded-lg"><svg class="w-6 h-6 text-proxmox" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17v-2m3 2v-4m3 4v-6m2 10H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path></svg></div></div><div class="w-full bg-darkbg rounded-full h-2"><div id="stat-ram-bar" class="bg-proxmox h-2 rounded-full" style="width: 0%"></div></div></div> <div class="flex items-center gap-4">
<div class="bg-darkcard border border-darkborder rounded-xl p-5 shadow-lg"><div class="flex justify-between items-start mb-4"><div><p class="text-gray-400 text-sm font-medium">Datacenter Storage</p><h3 id="stat-disk-text" class="text-2xl font-bold text-white mt-1">Lade...</h3></div><div class="p-2 bg-emerald-500/10 rounded-lg"><svg class="w-6 h-6 text-emerald-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4"></path></svg></div></div><div class="w-full bg-darkbg rounded-full h-2"><div id="stat-disk-bar" class="bg-emerald-500 h-2 rounded-full" style="width: 0%"></div></div></div> <div class="p-3 bg-blue-500/10 rounded-xl"><svg class="w-8 h-8 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"></path></svg></div>
<div class="bg-darkcard border border-darkborder rounded-xl p-5 shadow-lg flex flex-col justify-center"><div class="flex justify-between items-start mb-1"><div><p class="text-gray-400 text-sm font-medium">System Updates (APT)</p><h3 id="stat-updates-text" class="text-2xl font-bold text-white mt-1">Lade...</h3></div><div class="p-2 bg-purple-500/10 rounded-lg"><svg class="w-6 h-6 text-purple-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path></svg></div></div><div id="stat-updates-sub" class="text-xs text-gray-500 mt-1 truncate">Prüfe Updates...</div></div> <div><h2 class="text-xl font-bold text-white">Gesamtübersicht</h2><p class="text-gray-400 text-sm">Cluster & Standalone Nodes</p></div>
</div>
<div class="flex flex-wrap justify-center gap-6 md:gap-12">
<div class="text-center"><p class="text-gray-400 text-xs font-bold uppercase mb-1">Server (Nodes)</p><p class="text-2xl font-bold text-white"><span id="stat-nodes-online" class="text-green-500">0</span><span class="text-gray-600 mx-1">/</span><span id="stat-nodes-total" class="text-gray-300">0</span></p></div>
<div class="hidden md:block w-px bg-darkborder"></div>
<div class="text-center"><p class="text-gray-400 text-xs font-bold uppercase mb-1">Total VMs/LXC</p><p class="text-2xl font-bold text-white" id="stat-vms-total">0</p></div>
<div class="text-center"><p class="text-gray-400 text-xs font-bold uppercase mb-1">Online</p><p class="text-2xl font-bold text-green-500" id="stat-vms-run">0</p></div>
<div class="text-center"><p class="text-gray-400 text-xs font-bold uppercase mb-1">Offline</p><p class="text-2xl font-bold text-red-500" id="stat-vms-stop">0</p></div>
</div>
</div> </div>
<div class="bg-darkcard border border-darkborder rounded-xl p-5 shadow-lg min-h-[300px] flex flex-col justify-center mb-6"><div class="flex justify-between items-center mb-4"><h3 class="text-white font-bold">Live Cluster Auslastung</h3><span class="text-xs text-gray-500 flex items-center gap-2"><span class="w-2 h-2 rounded-full bg-green-500 animate-pulse"></span> Live Sync</span></div><div class="relative h-full w-full min-h-[220px]"><canvas id="liveChart"></canvas></div></div>
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-6">
<div class="bg-darkcard border border-darkborder rounded-xl p-5 shadow-lg"><p class="text-gray-400 text-sm font-medium">Cluster CPU Cores</p><h3 id="stat-cpu-text" class="text-2xl font-bold text-white mt-1">Lade...</h3><div class="w-full bg-darkbg rounded-full h-2 mt-4"><div id="stat-cpu-bar" class="bg-blue-500 h-2 rounded-full" style="width: 0%"></div></div></div>
<div class="bg-darkcard border border-darkborder rounded-xl p-5 shadow-lg"><p class="text-gray-400 text-sm font-medium">Globaler RAM</p><h3 id="stat-ram-text" class="text-2xl font-bold text-white mt-1">Lade...</h3><div class="w-full bg-darkbg rounded-full h-2 mt-4"><div id="stat-ram-bar" class="bg-proxmox h-2 rounded-full" style="width: 0%"></div></div></div>
<div class="bg-darkcard border border-darkborder rounded-xl p-5 shadow-lg"><p class="text-gray-400 text-sm font-medium">Datacenter Storage</p><h3 id="stat-disk-text" class="text-2xl font-bold text-white mt-1">Lade...</h3><div class="w-full bg-darkbg rounded-full h-2 mt-4"><div id="stat-disk-bar" class="bg-emerald-500 h-2 rounded-full" style="width: 0%"></div></div></div>
<div onclick="openCreateVm()" class="bg-proxmox/10 border border-proxmox/30 hover:border-proxmox rounded-xl p-5 shadow-lg cursor-pointer flex flex-col items-center justify-center text-proxmox transition-colors"><span class="text-3xl font-bold mb-1">+</span><span class="font-bold">Neue VM anlegen</span></div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<div class="bg-darkcard border border-darkborder rounded-xl p-5 shadow-lg min-h-[300px] flex flex-col justify-center"><div class="flex justify-between items-center mb-4"><h3 class="text-white font-bold">Live Cluster Auslastung</h3><span class="text-xs text-gray-500 flex items-center gap-2"><span class="w-2 h-2 rounded-full bg-blue-500 animate-pulse"></span> CPU / RAM</span></div><div class="relative h-full w-full min-h-[220px]"><canvas id="liveChart"></canvas></div></div>
<div class="bg-darkcard border border-darkborder rounded-xl p-5 shadow-lg min-h-[300px] flex flex-col justify-center"><div class="flex justify-between items-center mb-4"><h3 class="text-white font-bold">Live Netzwerk Traffic</h3><span class="text-xs text-gray-500 flex items-center gap-2"><span class="w-2 h-2 rounded-full bg-emerald-500 animate-pulse"></span> MB/s Total pro Node</span></div><div class="relative h-full w-full min-h-[220px]"><canvas id="liveNetChart"></canvas></div></div>
</div>
<div class="bg-darkcard border border-darkborder rounded-xl p-5 shadow-lg"><h3 class="text-white font-bold mb-4">🔥 Top 5 Ressourcen-Fresser</h3><div id="top-vms-container" class="space-y-3"><p class="text-gray-400 text-sm">Lädt Live-Daten von Proxmox API...</p></div></div> <div class="bg-darkcard border border-darkborder rounded-xl p-5 shadow-lg"><h3 class="text-white font-bold mb-4">🔥 Top 5 Ressourcen-Fresser</h3><div id="top-vms-container" class="space-y-3"><p class="text-gray-400 text-sm">Lädt Live-Daten von Proxmox API...</p></div></div>
</div> </div>
<!-- TAB 2: PBS --> <!-- TAB NODE VIEW -->
<div id="tab-pbs" class="flex-1 flex flex-col min-w-0 hidden opacity-0 transition-opacity duration-300"> <div id="tab-node-view" class="hidden opacity-0 flex-1 flex flex-col min-w-0 transition-opacity duration-300">
<div class="mb-6 border-b border-darkborder pb-4"><h2 class="text-2xl font-bold text-white">Backup Datastores</h2><p class="text-sm text-gray-400">Live-Kapazitätsüberwachung deiner PBS Server.</p></div> <div class="flex justify-between items-center border-b border-darkborder pb-4 mb-6">
<div id="pbs-datastores-container" class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6"><div class="text-gray-500 animate-pulse">Lade PBS Daten...</div></div> <h2 id="nodeViewTitle" class="text-2xl font-bold text-white">Host: ...</h2>
<div class="space-x-3">
<button id="nodeViewBackupBtn" class="bg-darkcard border border-darkborder hover:border-proxmox text-white px-4 py-2 rounded text-sm transition-colors">📦 Backup Jobs</button>
<button id="nodeViewHeaderBtn" class="bg-darkcard border border-darkborder hover:border-proxmox text-white px-4 py-2 rounded text-sm transition-colors">📈 Live Performance</button>
<button onclick="openCreateVm()" class="bg-proxmox hover:bg-orange-500 text-white px-4 py-2 rounded text-sm font-bold transition-colors">+ Neue VM hier anlegen</button>
</div>
</div>
<div class="grid grid-cols-1 xl:grid-cols-4 gap-6">
<div class="xl:col-span-3 bg-darkcard border border-darkborder rounded-xl shadow-lg flex flex-col h-[600px]">
<div class="p-4 border-b border-darkborder"><h3 class="text-white font-bold">Laufende & Gestoppte Maschinen</h3></div>
<div class="overflow-y-auto flex-1">
<table class="w-full text-left text-sm text-gray-400">
<thead class="bg-darkbg text-xs uppercase sticky top-0 border-b border-darkborder z-10">
<tr><th class="py-2 px-3">ID</th><th class="py-2 px-3">Name</th><th class="py-2 px-3">Typ</th><th class="py-2 px-3">Status</th><th class="py-2 px-3 text-right">Aktionen</th></tr>
</thead>
<tbody id="nodeVmsTableBody" class="divide-y divide-darkborder/50"></tbody>
</table>
</div>
</div>
<div class="bg-darkcard border border-darkborder rounded-xl shadow-lg p-4 flex flex-col h-[600px]">
<h3 class="text-white font-bold mb-4 border-b border-darkborder pb-2">💾 Storages</h3>
<div id="nodeStoragesContainer" class="space-y-3 overflow-y-auto pr-2"></div>
</div>
</div>
</div> </div>
<!-- TAB 3: PMG --> <!-- TAB PBS -->
<div id="tab-pmg" class="flex-1 flex flex-col min-w-0 hidden opacity-0 transition-opacity duration-300"> <div id="tab-pbs" class="hidden opacity-0 flex-1 flex flex-col min-w-0 transition-opacity duration-300">
<div class="mb-6 border-b border-darkborder pb-4"><h2 class="text-2xl font-bold text-white">Mail Gateway Status</h2><p class="text-sm text-gray-400">Live-Metriken deiner PMG Instanzen.</p></div> <div class="mb-6 border-b border-darkborder pb-4"><h2 class="text-2xl font-bold text-white">Backup Datastores</h2></div>
<div id="pmg-nodes-container" class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6"><div class="text-gray-500 animate-pulse">Lade PMG Daten...</div></div> <div id="pbs-datastores-container" class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6"></div>
</div> </div>
<!-- RECHTS: JOBS --> <!-- TAB PMG -->
<div class="w-80 shrink-0 flex flex-col"><div class="bg-darkcard border border-darkborder rounded-xl p-5 shadow-lg flex-1 overflow-y-auto"><h3 class="text-white font-bold mb-4 sticky top-0 bg-darkcard pb-2 border-b border-darkborder">Letzte Jobs</h3><div id="recent-jobs-container" class="space-y-3"><p class="text-gray-400 text-sm">Lädt Job-Historie...</p></div></div></div> <div id="tab-pmg" class="hidden opacity-0 flex-1 flex flex-col min-w-0 transition-opacity duration-300">
<div class="mb-6 border-b border-darkborder pb-4"><h2 class="text-2xl font-bold text-white">Mail Gateway Status</h2></div>
<div id="pmg-nodes-container" class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6"></div>
</div>
<!-- RECHTS: JOBS (Immer sichtbar, es sei denn das Fenster ist zu schmal) -->
<div class="w-80 shrink-0 flex flex-col hidden lg:flex">
<div class="bg-darkcard border border-darkborder rounded-xl p-5 shadow-lg flex-1 overflow-y-auto">
<h3 class="text-white font-bold mb-4 sticky top-0 bg-darkcard pb-2 border-b border-darkborder z-10">Letzte Jobs</h3>
<div id="recent-jobs-container" class="space-y-3"><p class="text-gray-400 text-sm">Lädt Job-Historie...</p></div>
</div>
</div>
</main> </main>
</div> </div>
</div> </div>
<?php include 'modals.php'; ?> <?php include 'modals.php'; ?>
<?php endif; ?> <?php endif; ?>
</main> </main>
<footer class="bg-darkcard border-t border-darkborder py-4 mt-auto z-10 w-full">
<div class="max-w-[1600px] w-full mx-auto px-6 flex flex-col md:flex-row justify-between items-center gap-2 text-xs text-gray-500">
<div>
Proxmox Unified Console (PUC) &copy; <?= date('Y') ?> — Ein Open-Source Projekt.
</div>
<div>
Weitere private Projekte und Infos zur PUC findest du auf
<a href="https://tessmann.dev" target="_blank" rel="noopener noreferrer" class="text-proxmox hover:text-orange-400 font-bold transition-colors">
tessmann.dev
</a>
</div>
</div>
</footer>
<script> window.APP = { isLoggedIn: <?= $isLoggedIn ? 'true' : 'false' ?>, nodeCount: <?= $nodeCount ?>, username: '<?= htmlspecialchars($_SESSION['username'] ?? '') ?>' }; </script> <script> window.APP = { isLoggedIn: <?= $isLoggedIn ? 'true' : 'false' ?>, nodeCount: <?= $nodeCount ?>, username: '<?= htmlspecialchars($_SESSION['username'] ?? '') ?>' }; </script>
<!-- HIER IST DER CACHE-BUSTER, DAMIT DER LOGOUT IMMER GEHT! -->
<script src="app.js?v=<?= time() ?>"></script> <script src="app.js?v=<?= time() ?>"></script>
</body> </body>
</html> </html>
+13 -106
View File
@@ -7,117 +7,24 @@
<div id="createVmModal" class="fixed inset-0 bg-black/80 hidden z-[80] flex items-center justify-center backdrop-blur-sm"><div class="bg-darkcard border border-darkborder rounded-xl shadow-2xl w-full max-w-md overflow-hidden flex flex-col"><div class="p-5 border-b border-darkborder flex justify-between items-center bg-darkbg"><h2 class="text-xl font-bold text-white">✨ Neue VM erstellen</h2><button onclick="closeCreateVm()" class="text-gray-400 hover:text-white transition-colors">✕</button></div><div class="p-6"><form id="createVmForm" class="space-y-4"><div><label class="block text-xs font-medium text-gray-500 mb-1">Ziel-Host (PVE Node)</label><select id="createVmHost" class="w-full bg-darkbg border border-darkborder rounded p-2 text-white text-sm focus:border-proxmox"><option value="">Lade Hosts...</option></select></div><div><label class="block text-xs font-medium text-gray-500 mb-1">Name der VM</label><input type="text" id="createVmName" placeholder="z.B. webserver-01" class="w-full bg-darkbg border border-darkborder rounded p-2 text-white text-sm" required></div><div class="grid grid-cols-2 gap-4"><div><label class="block text-xs font-medium text-gray-500 mb-1">RAM (MB)</label><input type="number" id="createVmRam" value="2048" step="512" min="512" class="w-full bg-darkbg border border-darkborder rounded p-2 text-white text-sm" required></div><div><label class="block text-xs font-medium text-gray-500 mb-1">CPU Cores</label><input type="number" id="createVmCores" value="2" min="1" class="w-full bg-darkbg border border-darkborder rounded p-2 text-white text-sm" required></div></div><button type="submit" class="w-full bg-proxmox hover:bg-orange-600 text-white font-bold py-3 rounded mt-4 transition-colors">Freie ID suchen & Erstellen</button></form></div></div></div> <div id="createVmModal" class="fixed inset-0 bg-black/80 hidden z-[80] flex items-center justify-center backdrop-blur-sm"><div class="bg-darkcard border border-darkborder rounded-xl shadow-2xl w-full max-w-md overflow-hidden flex flex-col"><div class="p-5 border-b border-darkborder flex justify-between items-center bg-darkbg"><h2 class="text-xl font-bold text-white">✨ Neue VM erstellen</h2><button onclick="closeCreateVm()" class="text-gray-400 hover:text-white transition-colors">✕</button></div><div class="p-6"><form id="createVmForm" class="space-y-4"><div><label class="block text-xs font-medium text-gray-500 mb-1">Ziel-Host (PVE Node)</label><select id="createVmHost" class="w-full bg-darkbg border border-darkborder rounded p-2 text-white text-sm focus:border-proxmox"><option value="">Lade Hosts...</option></select></div><div><label class="block text-xs font-medium text-gray-500 mb-1">Name der VM</label><input type="text" id="createVmName" placeholder="z.B. webserver-01" class="w-full bg-darkbg border border-darkborder rounded p-2 text-white text-sm" required></div><div class="grid grid-cols-2 gap-4"><div><label class="block text-xs font-medium text-gray-500 mb-1">RAM (MB)</label><input type="number" id="createVmRam" value="2048" step="512" min="512" class="w-full bg-darkbg border border-darkborder rounded p-2 text-white text-sm" required></div><div><label class="block text-xs font-medium text-gray-500 mb-1">CPU Cores</label><input type="number" id="createVmCores" value="2" min="1" class="w-full bg-darkbg border border-darkborder rounded p-2 text-white text-sm" required></div></div><button type="submit" class="w-full bg-proxmox hover:bg-orange-600 text-white font-bold py-3 rounded mt-4 transition-colors">Freie ID suchen & Erstellen</button></form></div></div></div>
<div id="vmSettingsModal" class="fixed inset-0 bg-black/80 hidden z-[70] flex items-center justify-center backdrop-blur-sm"><div class="bg-darkcard border border-darkborder rounded-xl shadow-2xl w-full max-w-4xl overflow-hidden flex flex-col max-h-[90vh]"><div class="p-5 border-b border-darkborder flex justify-between items-center bg-darkbg"><h2 class="text-xl font-bold text-white flex items-center gap-2"><span id="settingsModalTitle">Einstellungen</span></h2><button onclick="closeVmSettings()" class="text-gray-400 hover:text-white transition-colors text-2xl">✕</button></div><div class="p-6 overflow-y-auto space-y-6 flex-1"><input type="hidden" id="setVmid"><input type="hidden" id="setHost"><input type="hidden" id="setType"><input type="hidden" id="setNodeId"><input type="hidden" id="setRawNet0"><input type="hidden" id="setPrimaryDisk"><div class="grid grid-cols-1 lg:grid-cols-2 gap-6"><div class="space-y-6"><div class="bg-darkbg p-5 rounded-lg border border-darkborder"><h3 class="text-white font-bold mb-4">💻 Ressourcen</h3><div class="grid grid-cols-2 gap-4"><div><label class="block text-xs font-medium text-gray-500 mb-1">RAM (MB)</label><input type="number" id="setMemory" class="w-full bg-darkcard border border-darkborder rounded p-2 text-white text-sm"></div><div><label class="block text-xs font-medium text-gray-500 mb-1">CPU Cores</label><input type="number" id="setCores" class="w-full bg-darkcard border border-darkborder rounded p-2 text-white text-sm"></div></div><button onclick="saveHardwareSettings()" class="mt-4 bg-proxmox hover:bg-orange-600 text-white font-bold py-2 px-4 rounded text-sm transition-colors">Speichern</button></div><div class="bg-darkbg p-5 rounded-lg border border-darkborder"><h3 class="text-white font-bold mb-4">🌐 Netzwerkverwaltung</h3><div class="flex items-center gap-3 mb-6 border-b border-darkborder pb-4"><span id="netStatusBadge" class="px-2 py-1 rounded text-xs font-bold text-gray-400 bg-gray-500/20">Lade...</span><button id="btnToggleNet" onclick="toggleNetwork()" class="bg-darkcard border border-darkborder hover:border-proxmox text-white font-bold py-1.5 px-4 rounded text-sm transition-colors">Lade...</button></div><div><h4 class="text-gray-400 font-bold mb-2 text-xs uppercase">Zusätzliche NIC</h4><div class="flex items-end gap-3"><div class="flex-1"><label class="block text-xs font-medium text-gray-500 mb-1">Bridge Name (z.B. vmbr0)</label><input type="text" id="addNicBridge" value="vmbr0" class="w-full bg-darkcard border border-darkborder rounded p-2 text-white text-sm"></div><button onclick="addNic()" class="bg-proxmox hover:bg-orange-600 text-white font-bold py-2 px-4 rounded text-sm">+ Hinzufügen</button></div></div></div><div class="bg-darkbg p-5 rounded-lg border border-darkborder border-l-4 border-l-blue-500"><h3 class="text-white font-bold mb-4">💾 Festplatte erweitern</h3><div class="flex items-end gap-4"><div class="flex-1"><label class="block text-xs font-medium text-gray-500 mb-1">Um wie viel GB? (Disk: <span id="diskLabelName" class="text-proxmox">Lade...</span>)</label><input type="number" id="addDiskGb" min="1" placeholder="z.B. 10" class="w-full bg-darkcard border border-darkborder rounded p-2 text-white text-sm"></div><button onclick="expandDisk()" class="bg-blue-600 hover:bg-blue-500 text-white font-bold py-2 px-6 rounded text-sm transition-colors">Erweitern</button></div></div></div><div class="space-y-6"><div class="bg-darkbg p-5 rounded-lg border border-darkborder border-l-4 border-l-emerald-500"><h3 class="text-white font-bold mb-4">📸 Sicherung & Snapshots</h3><div class="flex items-end gap-3 mb-4"><div class="flex-1"><label class="block text-xs font-medium text-gray-500 mb-1">Neuer Snapshot Name</label><input type="text" id="newSnapName" placeholder="z.B. pre-update" class="w-full bg-darkcard border border-darkborder rounded p-2 text-white text-sm"></div><button onclick="createSnapshot()" class="bg-emerald-600 hover:bg-emerald-500 text-white font-bold py-2 px-4 rounded text-sm transition-colors">+ Snapshot</button></div><div class="space-y-2 mt-4 max-h-40 overflow-y-auto" id="snapshotListContainer"><div class="text-gray-500 text-sm italic">Lade Snapshots...</div></div></div><div class="bg-darkbg p-5 rounded-lg border border-darkborder border-l-4 border-l-pbs"><h3 class="text-white font-bold mb-4">🛡️ Backup Archive (vzdump)</h3><div class="flex items-end gap-3 mb-4 border-b border-darkborder pb-4"><div class="flex-1"><label class="block text-xs font-medium text-gray-500 mb-1">Ziel-Storage für manuelles Backup</label><select id="backupTargetStorage" class="w-full bg-darkcard border border-darkborder rounded p-2 text-white text-sm"><option value="">Lade Storages...</option></select></div><button onclick="createBackup()" class="bg-pbs hover:bg-purple-500 text-white font-bold py-2 px-4 rounded text-sm transition-colors">Jetzt sichern</button></div><h4 class="text-xs font-bold text-gray-400 mb-2 uppercase tracking-wider">Verfügbare Line truncated <div id="vmSettingsModal" class="fixed inset-0 bg-black/80 hidden z-[70] flex items-center justify-center backdrop-blur-sm"><div class="bg-darkcard border border-darkborder rounded-xl shadow-2xl w-full max-w-4xl overflow-hidden flex flex-col max-h-[90vh]"><div class="p-5 border-b border-darkborder flex justify-between items-center bg-darkbg"><h2 class="text-xl font-bold text-white flex items-center gap-2"><span id="settingsModalTitle">Einstellungen</span></h2><button onclick="closeVmSettings()" class="text-gray-400 hover:text-white transition-colors text-2xl">✕</button></div><div class="p-6 overflow-y-auto space-y-6 flex-1"><input type="hidden" id="setVmid"><input type="hidden" id="setHost"><input type="hidden" id="setType"><input type="hidden" id="setNodeId"><input type="hidden" id="setRawNet0"><input type="hidden" id="setPrimaryDisk"><div class="grid grid-cols-1 lg:grid-cols-2 gap-6"><div class="space-y-6"><div class="bg-darkbg p-5 rounded-lg border border-darkborder"><h3 class="text-white font-bold mb-4">💻 Ressourcen & Boot</h3><div class="grid grid-cols-2 gap-4 mb-4"><div><label class="block text-xs font-medium text-gray-500 mb-1">RAM (MB)</label><input type="number" id="setMemory" class="w-full bg-darkcard border border-darkborder rounded p-2 text-white text-sm"></div><div><label class="block text-xs font-medium text-gray-500 mb-1">CPU Cores</label><input type="number" id="setCores" class="w-full bg-darkcard border border-darkborder rounded p-2 text-white text-sm"></div></div><div class="mb-4"><label class="block text-xs font-medium text-gray-500 mb-1">CD/DVD Laufwerk (ISO Mounten)</label><select id="setIso" class="w-full bg-darkcard border border-darkborder rounded p-2 text-white text-sm"><option value="">Lade ISOs...</option></select></div><div class="mb-4"><label class="flex items-center gap-2 text-white text-sm cursor-pointer"><input type="checkbox" id="setOnboot" class="accent-proxmox w-4 h-4"> Start at boot (Auto-Start mit PVE)</label></div><button onclick="saveHardwareSettings()" class="w-full bg-proxmox hover:bg-orange-600 text-white font-bold py-2 px-4 rounded text-sm transition-colors">Alle VM-Settings Speichern</button></div><div class="bg-darkbg p-5 rounded-lg border border-darkborder"><h3 class="text-white font-bold mb-4">🌐 Netzwerkverwaltung</h3><div class="flex items-center gap-3 mb-6 border-b border-darkborder pb-4"><span id="netStatusBadge" class="px-2 py-1 rounded text-xs font-bold text-gray-400 bg-gray-500/20">Lade...</span><button id="btnToggleNet" onclick="toggleNetwork()" class="bg-darkcard border border-darkborder hover:border-proxmox text-white font-bold py-1.5 px-4 rounded text-sm transition-colors">Lade...</button></div><div><h4 class="text-gray-400 font-bold mb-2 text-xs uppercase">Zusätzliche NIC</h4><div class="flex items-end gap-3"><div class="flex-1"><label class="block text-xs font-medium text-gray-500 mb-1">Bridge Name (z.B. vmbr0)</label><input type="text" id="addNicBridge" value="vmbr0" class="w-full bg-darkcard border border-darkborder rounded p-2 text-white text-sm"></div><button onclick="addNic()" class="bg-proxmox hover:bg-orange-600 text-white font-bold py-2 px-4 rounded text-sm">+ Hinzufügen</button></div></div></div><div class="bg-darkbg p-5 rounded-lg border border-darkborder border-l-4 border-l-blue-500"><h3 class="text-white font-bold mb-4">💾 Festplatte erweitern</h3><div class="flex items-end gap-4"><div class="flex-1"><label class="block text-xs font-medium text-gray-500 mb-1">Um wie viel GB? (Disk: <span id="diskLabelName" class="text-proxmox">Lade...</span>)</label><input type="number" id="addDiskGb" min="1" placeholder="z.B. 10" class="w-full bg-darkcard border border-darkborder rounded p-2 text-white text-sm"></div><button onclick="expandDisk()" class="bg-blue-600 hover:bg-blue-500 text-white font-bold py-2 px-6 rounded text-sm transition-colors">Erweitern</button></div></div></div><div class="space-y-6"><div class="bg-darkbg p-5 rounded-lg border border-darkborder border-l-4 border-l-emerald-500"><h3 class="text-white font-bold mb-4">📸 Sicherung & Snapshots</h3><div class="flex items-end gap-3 mb-4"><div class="flex-1"><label class="block text-xs font-medium text-gray-500 mb-1">Neuer Snapshot Name</label><input type="text" id="newSnapName" placeholder="z.B. pre-update" class="w-full bg-darkcard border border-darkborder rounded p-2 text-white text-sm"></div><button onclick="createSnapshot()" class="bg-emerald-600 hover:bg-emerald-500 text-white font-bold py-2 px-4 rounded text-sm transition-colors">+ Snapshot</button></div><div class="space-y-2 mt-4 max-h-40 overflow-y-auto" id="snapshotListContainer"><div class="text-gray-500 text-sm italic">Lade Snapshots...</div></div></div><div class="bg-darkbg p-5 rounded-lg border border-darkborder border-l-4 border-l-pbs"><h3 class="text-white font-bold mb-4">🛡️ Backup Archive (vzdump)</h3><div class="flex items-end gap-3 mb-4 border-b border-darkborder pb-4"><div class="flex-1"><label clasLine truncated
<div id="nodeBackupsModal" class="fixed inset-0 bg-black/80 hidden z-[75] flex items-center justify-center backdrop-blur-sm"><div class="bg-darkcard border border-darkborder rounded-xl shadow-2xl w-full max-w-4xl overflow-hidden flex flex-col max-h-[80vh]"><div class="p-5 border-b border-darkborder flex justify-between items-center bg-darkbg"><h2 class="text-xl font-bold text-white" id="nodeBackupsTitle">Backup Jobs</h2><button onclick="closeNodeBackups()" class="text-gray-400 hover:text-white transition-colors text-2xl">✕</button></div><div class="p-6 overflow-y-auto flex-1 bg-darkbg"><div class="bg-darkcard border border-darkborder rounded-xl shadow-lg"><table class="w-full text-left text-sm text-gray-400"><thead class="bg-darkbg text-xs uppercase border-b border-darkborder"><tr><th class="py-3 px-4">ID</th><th class="py-3 px-4">Schedule</th><th class="py-3 px-4">Storage</th><th class="py-3 px-4">VMs</th><th class="py-3 px-4 text-right">Info</th></tr></thead><tbody id="nodeBackupsTableBody" class="divide-y divide-darkborder/50"></tbody></table></div></div></div></div>
<div id="nodeTopologyModal" class="fixed inset-0 bg-black/80 hidden z-[65] flex items-center justify-center backdrop-blur-sm"><div class="bg-darkcard border border-darkborder rounded-xl shadow-2xl w-full max-w-6xl overflow-hidden flex flex-col max-h-[90vh]"><div class="p-5 border-b border-darkborder flex justify-between items-center bg-darkbg"><h2 class="text-xl font-bold text-white flex items-center gap-2"><svg class="w-6 h-6 text-proxmox" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"></path></svg> Cluster Topologie</h2><button onclick="closeNodeTopology()" class="text-gray-400 hover:text-white">✕</button></div><div class="p-6 overflow-y-auto flex-1 bg-darkbg"><div id="nodeTopologyContainer" class="space-y-6"></div></div></div></div> <div id="nodeTopologyModal" class="fixed inset-0 bg-black/80 hidden z-[65] flex items-center justify-center backdrop-blur-sm"><div class="bg-darkcard border border-darkborder rounded-xl shadow-2xl w-full max-w-6xl overflow-hidden flex flex-col max-h-[90vh]"><div class="p-5 border-b border-darkborder flex justify-between items-center bg-darkbg"><h2 class="text-xl font-bold text-white flex items-center gap-2"><svg class="w-6 h-6 text-proxmox" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"></path></svg> Cluster Topologie</h2><button onclick="closeNodeTopology()" class="text-gray-400 hover:text-white">✕</button></div><div class="p-6 overflow-y-auto flex-1 bg-darkbg"><div id="nodeTopologyContainer" class="space-y-6"></div></div></div></div>
<div id="liveGraphModal" class="fixed inset-0 bg-black/90 hidden z-[90] flex items-center justify-center backdrop-blur-md"><div class="bg-darkcard border border-darkborder rounded-xl shadow-2xl w-full max-w-6xl overflow-hidden flex flex-col max-h-[90vh]"><div class="p-5 border-b border-darkborder flex justify-between items-center bg-darkbg"><h2 class="text-xl font-bold text-white flex items-center gap-2"><span class="w-2 h-2 rounded-full bg-green-500 animate-pulse"></span><span id="graphModalTitle">Live Performance</span></h2><button onclick="closeLiveGraph()" class="text-gray-400 hover:text-white transition-colors">✕</button></div><div class="p-6 bg-darkbg overflow-y-auto"><div class="grid grid-cols-1 lg:grid-cols-2 gap-6"><div class="bg-darkcard border border-darkborder rounded-xl p-4 shadow-lg"><h3 class="text-white font-bold mb-3 text-sm">System: CPU & RAM (%)</h3><div class="relative w-full h-[250px]"><canvas id="perfChart"></canvas></div></div><div class="bg-darkcard border border-darkborder rounded-xl p-4 shadow-lg"><h3 class="text-white font-bold mb-3 text-sm">Netzwerk: RX / TX (MB/s)</h3><div class="relative w-full h-[250px]"><canvas id="netChart"></canvas></div></div></div><p class="text-center text-xs text-gray-500 mt-4">Metriken werden live im 2-Sekunden-Takt abgefragt.</p></div></div></div> <div id="liveGraphModal" class="fixed inset-0 bg-black/90 hidden z-[90] flex items-center justify-center backdrop-blur-md"><div class="bg-darkcard border border-darkborder rounded-xl shadow-2xl w-full max-w-6xl overflow-hidden flex flex-col max-h-[90vh]"><div class="p-5 border-b border-darkborder flex justify-between items-center bg-darkbg"><h2 class="text-xl font-bold text-white flex items-center gap-2"><span id="graphStatusDot" class="w-2 h-2 rounded-full bg-green-500 animate-pulse"></span><span id="graphModalTitle">Performance</span></h2><button onclick="closeLiveGraph()" class="text-gray-400 hover:text-white transition-colors text-2xl">✕</button></div><div class="flex border-b border-darkborder bg-darkbg px-5 pt-4"><button onclick="switchGraphTab('live')" id="btn-graph-live" class="px-4 py-2 border-b-2 border-proxmox text-white font-bold">Live</button><button onclick="switchGraphTab('day')" id="btn-graph-day" class="px-4 py-2 border-b-2 border-transparent text-gray-400 hover:text-white font-bold transition-colors">24 Stunden</button><button onclick="switchGraphTab('week')" id="btn-graph-week" class="px-4 py-2 border-b-2 border-transparent text-gray-400 hover:text-white font-bold transition-colors">7 Tage</button></div><div class="p-6 bg-darkbg overflow-y-auto"><div class="grid grid-cols-1 lg:grid-cols-2 gap-6"><div class="bg-darkcard border border-darkborder rounded-xl p-4 shadow-lg"><h3 class="text-white font-bold mb-3 text-sm">CPU & RAM (%)</h3><div class="relative w-full h-[250px]"><canvas id="perfChart"></canvas></div></div><div class="bg-darkcard border border-darkborder rounded-xl p-4 shadow-lg"><h3 class="text-white font-bold mb-3 text-sm">Netzwerk Traffic</h3><div class="relative w-full h-[250px]"><canvas id="netChart"></canvas></div></div></div><p id="graphSubText" class="text-center text-xs text-gray-500 mt-4">Metriken werden live abgefragt.</p></div></div></div>
<div id="pbsDatastoreModal" class="fixed inset-0 bg-black/80 hidden z-[70] flex items-center justify-center backdrop-blur-sm"><div class="bg-darkcard border border-darkborder rounded-xl shadow-2xl w-full max-w-7xl overflow-hidden flex flex-col max-h-[95vh]"><div class="p-5 border-b border-darkborder flex justify-between items-center bg-darkbg"><h2 class="text-xl font-bold text-white flex items-center gap-2"><svg class="w-6 h-6 text-pbs" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7v8a2 2 0 002 2h6M8 7V5a2 2 0 012-2h4.586a1 1 0 01.707.293l4.414 4.414a1 1 0 01.293.707V15a2 2 0 01-2 2h-2M8 7H6a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2v-2"></path></svg> <span id="pbsModalTitle">Datastore</span></h2><button onclick="closePbsDatastore()" class="text-gray-400 hover:text-white transition-colors text-2xl">✕</button></div><div class="p-6 overflow-y-auto space-y-6 flex-1 bg-darkbg"><input type="hidden" id="pbsNodeId"><input type="hidden" id="pbsStoreName"><div class="grid grid-cols-1 xl:grid-cols-3 gap-6"><div class="bg-darkcard border border-darkborder rounded-xl p-5 shadow-lg flex flex-col h-[350px]"><h3 class="text-white font-bold mb-4 flex justify-between items-center">🗄️ Gespeicherte Backups</h3><div id="pbsBackupsContainer" class="space-y-2 overflow-y-auto flex-1 pr-2">Lade...</div></div><div class="bg-darkcard border border-darkborder rounded-xl p-5 shadow-lg flex flex-col h-[350px]"><h3 class="text-white font-bold mb-4">🧹 Verify & Garbage Collection</h3><div id="pbsVerifyGcContainer" class="space-y-2 overflow-y-auto flex-1 pr-2">Lade...</div></div><div class="bg-darkcard border border-darkborder rounded-xl p-5 shadow-lg flex flex-col h-[350px] border-l-4 border-l-blue-500"><h3 class="text-white font-bold mb-4">🔄 Eingerichtete Sync Jobs</h3><div id="pbsSyncContainer" class="space-y-2 overflow-y-auto flex-1 pr-2">Lade...</div></div></div><div class="bg-darkcard border border-darkborder rounded-xl p-5 shadow-lg mt-6"><h3 class="text-white font-bold mb-4">📋 Letzte 100 PBS Jobs (Historie)</h3><div class="max-h-60 overflow-y-auto pr-2"><table class="w-full text-left text-sm text-gray-400"><thead class="bg-darkbg border-b border-darkborder text-xs uppercase sticky top-0"><tr><th class="px-4 py-2">Start</th><th class="px-4 py-2">Job Typ</th><th class="px-4 py-2">Status</th><th class="px-4 py-2">Dauer</th><th class="px-4 py-2 text-right">Log</th></tr></thead><tbody id="pbsJobsTable" class="divide-y divide-darkborder"></tbody></table></div></div></div></div></div> <div id="pbsDatastoreModal" class="fixed inset-0 bg-black/80 hidden z-[70] flex items-center justify-center backdrop-blur-sm"><div class="bg-darkcard border border-darkborder rounded-xl shadow-2xl w-full max-w-7xl overflow-hidden flex flex-col max-h-[95vh]"><div class="p-5 border-b border-darkborder flex justify-between items-center bg-darkbg"><h2 class="text-xl font-bold text-white flex items-center gap-2"><svg class="w-6 h-6 text-pbs" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7v8a2 2 0 002 2h6M8 7V5a2 2 0 012-2h4.586a1 1 0 01.707.293l4.414 4.414a1 1 0 01.293.707V15a2 2 0 01-2 2h-2M8 7H6a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2v-2"></path></svg> <span id="pbsModalTitle">Datastore</span> <button onclick="openLiveGraph('node', 0, 'localhost', document.getElementById('pbsNodeId').value, 'PBS Server')" class="ml-4 text-xs bg-darkcard border border-darkborder hover:border-pbs text-white px-3 py-1.5 rounded transition-colors">📈 Performance Historie</button></h2><button onclick="closePbsDatastore()" class="text-gray-400 hover:text-white transition-colors text-2xl">✕</button></div><div class="p-6 overflow-y-auto space-y-6 flex-1 bg-darkbg"><input type="hidden" id="pbsNodeId"><input type="hidden" id="pbsStoreName"><div class="grid grid-cols-1 xl:grid-cols-3 gap-6"><div class="bg-darkcard border border-darkborder rounded-xl p-5 shadow-lg flex flex-col h-[350px]"><h3 class="text-white font-bold mb-4 flex justify-between items-center">🗄️ Gespeicherte Backups</h3><div id="pbsBackupsContainer" class="space-y-2 overflow-y-auto flex-1 pr-2">Lade...</div></div><div class="bg-darkcard border border-darkborder rounded-xl p-5 shadow-lg flex flex-col h-[350px]"><h3 class="text-white font-bold mb-4">🧹 Verify & Garbage Collection</h3><div id="pbsVerifyGcContainer" class="space-y-2 overflow-y-auto flex-1 pr-2">Lade...</div></div><div class="bg-darkcard border border-darkborder rounded-xl p-5 shadow-lg flex flex-col h-[350px] border-l-4 border-l-blue-500"><h3 class="text-white font-bold mb-4">🔄 Eingerichtete Sync Jobs</h3><div id="pbsSyncContainer" class="space-y-2 overflow-y-auto flex-1 pr-2">Lade...</div></div></div><div class="bg-darkcard border border-darkborder rounded-xl p-5 shadow-lg mt-6"><h3 class="text-white font-bold mb-4">📋 Letzte 100 PBS Jobs (Historie)</h3><div class="max-h-60 overflow-y-auto pr-2"><table class="w-full text-left text-sm text-gray-400"><thead class="bg-darkbg border-b border-darkborder text-xs uppercase sticky top-0"><tr><th class="px-4 py-2">Start</th><th class="px-4 py-2">Job Typ</th><th class="px-4 py-2">Status</th><th class="px-4 py-2">Dauer</th><th class="px-4 py-2 text-right">Log</th></tr></thead><tbody id="pbsJobsTable" class="divide-y divide-darkborder"></tbody></table></div></div></div></div></div>
<div id="taskLogModal" class="fixed inset-0 bg-black/90 hidden z-[100] flex items-center justify-center backdrop-blur-sm"><div class="bg-darkcard border border-darkborder rounded-xl shadow-2xl w-full max-w-5xl overflow-hidden flex flex-col h-[85vh]"><div class="p-4 border-b border-darkborder flex justify-between items-center bg-darkbg"><h2 class="text-lg font-bold text-white flex items-center gap-2">📄 Task Log Viewer</h2><button onclick="closeTaskLog()" class="text-gray-400 hover:text-white transition-colors text-2xl">✕</button></div><div class="p-4 bg-black flex-1 overflow-y-auto"><pre id="taskLogContent" class="text-xs text-green-400 font-mono whitespace-pre-wrap">Lade Log...</pre></div></div></div> <div id="taskLogModal" class="fixed inset-0 bg-black/90 hidden z-[100] flex items-center justify-center backdrop-blur-sm"><div class="bg-darkcard border border-darkborder rounded-xl shadow-2xl w-full max-w-5xl overflow-hidden flex flex-col h-[85vh]"><div class="p-4 border-b border-darkborder flex justify-between items-center bg-darkbg"><h2 class="text-lg font-bold text-white flex items-center gap-2">📄 Task Log Viewer</h2><button onclick="closeTaskLog()" class="text-gray-400 hover:text-white transition-colors text-2xl">✕</button></div><div class="p-4 bg-black flex-1 overflow-y-auto"><pre id="taskLogContent" class="text-xs text-green-400 font-mono whitespace-pre-wrap">Lade Log...</pre></div></div></div>
<div id="pmgManagerModal" class="fixed inset-0 bg-black/80 hidden z-[75] flex items-center justify-center backdrop-blur-sm"><div class="bg-darkcard border border-darkborder rounded-xl shadow-2xl w-full max-w-7xl overflow-hidden flex flex-col h-[95vh]"><div class="p-5 border-b border-darkborder flex justify-between items-center bg-darkbg"><h2 class="text-xl font-bold text-white flex items-center gap-2"><svg class="w-6 h-6 text-pmg" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path></svg> <span id="pmgModalTitle">Mail Gateway</span></h2><button onclick="closePmgManager()" class="text-gray-400 hover:text-white transition-colors text-2xl">✕</button></div><div class="flex border-b border-darkborder bg-darkbg px-5 pt-4"><button onclick="switchPmgTab('spam')" id="btn-pmg-spam" class="px-4 py-2 border-b-2 border-pmg text-white font-bold">🛡️ Quarantäne</button><button onclick="switchPmgTab('tracking')" id="btn-pmg-tracking" class="px-4 py-2 border-b-2 border-transparent text-gray-400 hover:text-white font-bold transition-colors">🔎 Tracking Center</button><button onclick="switchPmgTab('queues')" id="btn-pmg-queues" class="px-4 py-2 border-b-2 border-transparent text-gray-400 hover:text-white font-bold transition-colors">📨 Mail Queues</button><button onclick="switchPmgTab('settings')" id="btn-pmg-settings" class="px-4 py-2 border-b-2 border-transparent text-gray-400 hover:text-white font-bold transition-colors ml-auto">⚙️ Einstellungen</button></div><div class="p-6 overflow-y-auto flex-1 bg-darkbg"><input type="hidden" id="pmgNodeId"><input type="hidden" id="pmgHostName"><input type="hidden" id="pmgInternalName"><div id="pmg-tab-spam" class="h-full flex flex-col"><div class="flex justify-between items-center mb-4"><h3 class="text-white font-bold">Zurückgehaltene Mails</h3><button onclick="loadPmgSpamQueue(document.getElementById('pmgNodeId').value)" class="text-xs bg-darkcard border border-darkborder hover:border-pmg text-white px-3 py-1.5 rounded transition-colors">🔄 Aktualisieren</button></div><div class="overflow-y-auto flex-1 bg-darkcard border border-darkborder rounded-lg relative"><table class="w-full text-left text-sm text-gray-400"><thead class="bg-darkbg border-b border-darkborder text-xs uppercase sticky top-0 z-10"><tr><th class="px-4 py-3">Datum</th><th class="px-4 py-3">Absender</th><th class="px-4 py-3">Empfänger</th><th class="px-4 py-3">Betreff</th><th class="px-4 py-3 text-center">Score</th><th class="px-4 py-3 text-right">Aktion</th></tr></thead><tbody id="pmgSpamTable" class="divide-y divide-darkborder"></tbody></table></div></div><div id="pmg-tab-tracking" class="hidden h-full flex flex-col"><div class="flex gap-4 mb-4 items-end"><div class="flex-1"><label class="block text-xs font-medium text-gray-500 mb-1">Nach E-Mail Adresse filtern (Letzte 24h)</label><input type="text" id="pmgTrackingFilter" placeholder="z.B. spammer@evil.com oder user@firma.de" class="w-full bg-darkcard border border-darkborder rounded p-2 text-white text-sm" onkeydown="if(event.key === 'Enter') loadPmgTracking()"></div><button onclick="loadPmgTracking()" class="bg-pmg hover:bg-blue-600 text-white font-bold py-2 px-6 rounded text-sm transition-colors">Suchen</button></div><div class="overflow-y-auto flex-1 bg-darkcard border border-darkborder rounded-lg relative"><table class="w-full text-left text-sm text-gray-400"><thead class="bg-darkbg border-b border-darkborder text-xs uppercase sticky top-0 z-10"><tr><th class="px-4 py-3">Zeit</th><th class="px-4 py-3">Absender</th><th class="px-4 py-3">Empfänger</th><th class="px-4 py-3">Status</th></tr></thead><tbody id="pmgTrackingTable" class="divide-y divide-darkborder"></tbody></table></div></div><div id="pmg-tab-queues" class="hidden h-full flex flex-col"><div class="flex justify-between items-center mb-4"><h3 class="text-white font-bold">Aktuelle Postfix Warteschlangen</h3><button onclick="loadPmgQueues()" class="text-xs bg-darkcard border border-darkborder hover:border-pmg text-white px-3 py-1.5 rounded transition-colors">🔄 Aktualisieren</button></div><div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6" id="pmgQueuesSummary"><div class="bg-darkcard border border-darkborder rounded-xl p-5 text-center animate-pulse"><p class="text-gray-500">Lade...</p></div></div><div class="overflow-y-auto flex-1 bg-darkcard border border-darkborder rounded-lg relative"><table class="w-full text-left text-sm text-gray-400"><thead class="bg-darkbg border-b border-darkborder text-xs uppercase sticky top-0 z-10"><tr><th class="px-4 py-3">Queue</th><th class="px-4 py-3">Zeit</th><th class="px-4 py-3">Absender</th><th class="px-4 py-3">Empfänger</th><th class="px-4 py-3">Grund</th></tr></thead><tbody id="pmgQueueTable" class="divide-y divide-darkborder"></tbody></table></div></div><div Line truncated <div id="pmgManagerModal" class="fixed inset-0 bg-black/80 hidden z-[75] flex items-center justify-center backdrop-blur-sm"><div class="bg-darkcard border border-darkborder rounded-xl shadow-2xl w-full max-w-7xl overflow-hidden flex flex-col h-[95vh]"><div class="p-5 border-b border-darkborder flex justify-between items-center bg-darkbg"><h2 class="text-xl font-bold text-white flex items-center gap-2"><svg class="w-6 h-6 text-pmg" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path></svg> <span id="pmgModalTitle">Mail Gateway</span> <button onclick="openLiveGraph('node', 0, document.getElementById('pmgInternalName').value, document.getElementById('pmgNodeId').value, document.getElementById('pmgHostName').value)" class="ml-4 text-xs bg-darkcard border border-darkborder hover:border-pmg text-white px-3 py-1.5 rounded transition-colors">📈 Performance Historie</button></h2><button onclick="closePmgManager()" class="text-gray-400 hover:text-white transition-colors text-2xl">✕</button></div><div class="flex border-b border-darkborder bg-darkbg px-5 pt-4"><button onclick="switchPmgTab('spam')" id="btn-pmg-spam" class="px-4 py-2 border-b-2 border-pmg text-white font-bold">🛡️ Quarantäne</button><button onclick="switchPmgTab('tracking')" id="btn-pmg-tracking" class="px-4 py-2 border-b-2 border-transparent text-gray-400 hover:text-white font-bold transition-colors">🔎 Tracking Center</button><button onclick="switchPmgTab('queues')" id="btn-pmg-queues" class="px-4 py-2 border-b-2 border-transparent text-gray-400 hover:text-white font-bold transition-colors">📨 Mail Queues</button><button onclick="switchPmgTab('settings')" id="btn-pmg-settings" class="px-4 py-2 border-b-2 border-transparent text-gray-400 hover:text-white font-bold transition-colors ml-auto">⚙️ Einstellungen</button></div><div class="p-6 overflow-y-auto flex-1 bg-darkbg"><input type="hidden" id="pmgNodeId"><input type="hidden" id="pmgHostName"><input type="hidden" id="pmgInternalName"><div id="pmg-tab-spam" class="h-full flex flex-col"><div class="flex justify-between items-center mb-4"><h3 class="text-white font-bold">Zurückgehaltene Mails</h3><button onclick="loadPmgSpamQueue(document.getElementById('pmgNodeId').value)" class="text-xs bg-darkcard border border-darkborder hover:border-pmg text-white px-3 py-1.5 rounded transition-colors">🔄 Aktualisieren</button></div><div class="overflow-y-auto flex-1 bg-darkcard border border-darkborder rounded-lg relative"><table class="w-full text-left text-sm text-gray-400"><thead class="bg-darkbg border-b border-darkborder text-xs uppercase sticky top-0 z-10"><tr><th class="px-4 py-3">Datum</th><th class="px-4 py-3">Absender</th><th class="px-4 py-3">Empfänger</th><th class="px-4 py-3">Betreff</th><th class="px-4 py-3 text-center">Score</th><th class="px-4 py-3 text-right">Aktion</th></tr></thead><tbody id="pmgSpamTable" class="divide-y divide-darkborder"></tbody></table></div></div><div id="pmg-tab-tracking" class="hidden h-full flex flex-col"><div class="flex gap-4 mb-4 items-end"><div class="flex-1"><label class="block text-xs font-medium text-gray-500 mb-1">Nach E-Mail Adresse filtern (Letzte 24h)</label><input type="text" id="pmgTrackingFilter" placeholder="z.B. spammer@evil.com oder user@firma.de" class="w-full bg-darkcard border border-darkborder rounded p-2 text-white text-sm" onkeydown="if(event.key === 'Enter') loadPmgTracking()"></div><button onclick="loadPmgTracking()" class="bg-pmg hover:bg-blue-600 text-white font-bold py-2 px-6 rounded text-sm transition-colors">Suchen</button></div><div class="overflow-y-auto flex-1 bg-darkcard border border-darkborder rounded-lg relative"><table class="w-full text-left text-sm text-gray-400"><thead class="bg-darkbg border-b border-darkborder text-xs uppercase sticky top-0 z-10"><tr><th class="px-4 py-3">Zeit</th><th class="px-4 py-3">Absender</th><th class="px-4 py-3">Empfänger</th><th class="px-4 py-3">Status</th></tr></thead><tbody id="pmgTrackingTable" class="divide-y divide-darkborder"></tbody></table></div></div><div id="pmg-tab-queues" class="hidden h-full flex flex-col"><div class="flex justify-between items-center mb-4"><h3 class="text-white font-bold">Aktuelle Postfix Warteschlangen</h3><button onclick="loadPmgQueues()" class="text-xs bg-darkcard border border-darkborder hover:border-pmg text-white px-3 py-1.5 rounded transition-colors">🔄 Aktualisieren</button></div><div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6" id="pmgQueuesSummary"><div class="bg-darkcard border border-darkborder rounded-xl p-5 text-center animate-pulse"><p class="text-gray-500">Lade...</p></div></div><div class="overflow-y-auto flex-1 bg-darkcard border border-darkborder rounded-lg relative"><table class="w-full text-left text-sm text-gray-400"><thead class="bg-darkbg Line truncated
<!-- NEU: CRON / SCHEDULER MODAL --> <div id="updateManagerModal" class="fixed inset-0 bg-black/80 hidden z-[85] flex items-center justify-center backdrop-blur-sm"><div class="bg-darkcard border border-darkborder rounded-xl shadow-2xl w-full max-w-5xl overflow-hidden flex flex-col max-h-[90vh]"><div class="p-5 border-b border-darkborder flex justify-between items-center bg-darkbg"><h2 class="text-xl font-bold text-white flex items-center gap-2">📦 System Updates (APT)</h2><button onclick="closeUpdateManager()" class="text-gray-400 hover:text-white transition-colors text-2xl">✕</button></div><div class="p-6 overflow-y-auto flex-1 bg-darkbg" id="updateManagerContent"><div class="text-center py-10 text-gray-500 animate-pulse">Prüfe alle Nodes auf Paket-Updates...</div></div></div></div>
<div id="cronManagerModal" class="fixed inset-0 bg-black/80 hidden z-50 flex items-center justify-center backdrop-blur-sm">
<div class="bg-darkcard border border-darkborder rounded-xl shadow-2xl w-full max-w-5xl overflow-hidden flex flex-col max-h-[90vh]"> <div id="cronManagerModal" class="fixed inset-0 bg-black/80 hidden z-50 flex items-center justify-center backdrop-blur-sm"><div class="bg-darkcard border border-darkborder rounded-xl shadow-2xl w-full max-w-5xl overflow-hidden flex flex-col max-h-[90vh]"><div class="p-5 border-b border-darkborder flex justify-between items-center bg-darkbg"><h2 class="text-xl font-bold text-white flex items-center gap-2">⏱️ Task Scheduler & Automatisierung</h2><button onclick="closeCronManager()" class="text-gray-400 hover:text-white transition-colors">✕</button></div><div class="p-6 overflow-y-auto flex-1 flex flex-col gap-6 bg-darkbg"><div class="bg-darkcard p-5 rounded-xl border border-darkborder shadow-lg"><h3 class="text-white font-bold mb-4 border-b border-darkborder pb-2">Neuen Cron-Job anlegen</h3><form id="addCronJobForm" class="space-y-4"><div class="grid grid-cols-1 md:grid-cols-2 gap-4"><div><label class="block text-xs font-medium text-gray-500 mb-1">Name / Beschreibung</label><input type="text" id="cronName" class="w-full bg-darkbg border border-darkborder rounded p-2 text-white text-sm" placeholder="z.B. Nächtlicher Reboot" required></div><div><label class="block text-xs font-medium text-gray-500 mb-1">Ziel-Host (PVE)</label><select id="cronNodeId" class="w-full bg-darkbg border border-darkborder rounded p-2 text-white text-sm" required><option value="">Lade Hosts...</option></select></div></div><div class="grid grid-cols-1 md:grid-cols-4 gap-4"><div><label class="block text-xs font-medium text-gray-500 mb-1">Aktion</label><select id="cronAction" onchange="checkCronAction()" class="w-full bg-darkbg border border-darkborder rounded p-2 text-white text-sm" required><option value="reboot_node">Ganzen Server neustarten</option><option value="start_vm">VM Starten</option><option value="stop_vm">VM Stoppen</option><option value="reboot_vm">VM Neustarten</option></select></div><div id="cronVmBlock" class="hidden"><label class="block text-xs font-medium text-gray-500 mb-1">Ziel VM-ID</label><input type="number" id="cronTargetVmid" class="w-full bg-darkbg border border-darkborder rounded p-2 text-white text-sm" placeholder="z.B. 100"></div><div><label class="block text-xs font-medium text-gray-500 mb-1">Uhrzeit</label><input type="time" id="cronTime" value="03:00" class="w-full bg-darkbg border border-darkborder rounded p-2 text-white text-sm" required></div><div><label class="block text-xs font-medium text-gray-500 mb-1">Tage / Datum</label><select id="cronDays" onchange="toggleCronDate()" class="w-full bg-darkbg border border-darkborder rounded p-2 text-white text-sm" required><option value="*">Jeden Tag</option><option value="1-5">Montag - Freitag</option><option value="6,0">Wochenende</option><option value="1">Jeden Montag</option><option value="specific">📅 Einmalig (Bestimmtes Datum)</option></select><input type="date" id="cronSpecificDate" class="w-full bg-darkbg border border-darkborder rounded p-2 text-white text-sm mt-2 hidden"></div></div><button type="submit" class="bg-proxmox hover:bg-orange-600 text-white font-bold py-2 px-6 rounded transition-colors text-sm w-full md:w-auto">Job Speichern</button></form></div><div class="bg-darkcard border border-darkborder rounded-xl shadow-lg flex-1 overflow-hidden flex flex-col"><table class="w-full text-left text-sm text-gray-400"><thead class="bg-darkbg border-b border-darkborder text-xs uppercase sticky top-0"><tr><th class="px-4 py-3">Status</th><th class="px-4 py-3">Beschreibung</th><th class="px-4 py-3">Host / Ziel</th><th class="px-4 py-3">Cron</th><th class="px-4 py-3">Letzter Lauf</th><th class="px-4 py-3 text-right">Löschen</th></tr></thead><tbody id="cronTableBody" class="divide-y divide-darkborder"></tbody></table></div></div></div></div>
<div class="p-5 border-b border-darkborder flex justify-between items-center bg-darkbg">
<h2 class="text-xl font-bold text-white flex items-center gap-2">⏱️ Task Scheduler & Automatisierung</h2> <div id="auditLogModal" class="fixed inset-0 bg-black/80 hidden z-[100] flex items-center justify-center backdrop-blur-sm"><div class="bg-darkcard border border-darkborder rounded-xl shadow-2xl w-full max-w-5xl overflow-hidden flex flex-col max-h-[90vh]"><div class="p-5 border-b border-darkborder flex justify-between items-center bg-darkbg"><h2 class="text-xl font-bold text-white flex items-center gap-2"><svg class="w-6 h-6 text-proxmox" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path></svg> Aktivitäten-Protokoll (Audit Log)</h2><button onclick="closeAuditLog()" class="text-gray-400 hover:text-white transition-colors">✕</button></div><div class="p-6 overflow-y-auto flex-1 bg-darkbg"><div class="bg-darkcard border border-darkborder rounded-xl shadow-lg flex-1 overflow-hidden flex flex-col"><table class="w-full text-left text-sm text-gray-400"><thead class="bg-darkbg border-b border-darkborder text-xs uppercase sticky top-0"><tr><th class="px-4 py-3">Datum / Uhrzeit</th><th class="px-4 py-3">Benutzer</th><th class="px-4 py-3">Aktion</th><th class="px-4 py-3">Ziel / Details</th></tr></thead><tbody id="auditLogTableBody" class="divide-y divide-darkborder"></tbody></table></div></div></div></div>
<button onclick="closeCronManager()" class="text-gray-400 hover:text-white transition-colors">✕</button>
</div> <div id="passwordModal" class="fixed inset-0 bg-black/80 hidden z-[100] flex items-center justify-center backdrop-blur-sm"><div class="bg-darkcard border border-darkborder rounded-xl shadow-2xl w-full max-w-sm overflow-hidden flex flex-col"><div class="p-5 border-b border-darkborder flex justify-between items-center bg-darkbg"><h2 class="text-lg font-bold text-white flex items-center gap-2">🔑 Passwort ändern</h2><button onclick="closePasswordModal()" class="text-gray-400 hover:text-white transition-colors text-xl">✕</button></div><div class="p-6"><form id="changePasswordForm" class="space-y-4"><div><label class="block text-xs font-medium text-gray-500 mb-1">Aktuelles Passwort</label><input type="password" id="oldPassword" class="w-full bg-darkbg border border-darkborder rounded p-2 text-white text-sm focus:border-proxmox" required></div><div><label class="block text-xs font-medium text-gray-500 mb-1">Neues Passwort</label><input type="password" id="newPassword" class="w-full bg-darkbg border border-darkborder rounded p-2 text-white text-sm focus:border-proxmox" required></div><div><label class="block text-xs font-medium text-gray-500 mb-1">Neues Passwort bestätigen</label><input type="password" id="newPasswordConfirm" class="w-full bg-darkbg border border-darkborder rounded p-2 text-white text-sm focus:border-proxmox" required></div><button type="submit" class="w-full bg-proxmox hover:bg-orange-600 text-white font-bold py-2.5 rounded mt-2 transition-colors">Passwort aktualisieren</button></form></div></div></div>
<div class="p-6 overflow-y-auto flex-1 flex flex-col gap-6 bg-darkbg">
<div class="bg-darkcard p-5 rounded-xl border border-darkborder shadow-lg">
<h3 class="text-white font-bold mb-4 border-b border-darkborder pb-2">Neuen Cron-Job anlegen</h3>
<form id="addCronJobForm" class="space-y-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div><label class="block text-xs font-medium text-gray-500 mb-1">Name / Beschreibung</label><input type="text" id="cronName" class="w-full bg-darkbg border border-darkborder rounded p-2 text-white text-sm" placeholder="z.B. Nächtlicher Reboot" required></div>
<div><label class="block text-xs font-medium text-gray-500 mb-1">Ziel-Host (PVE)</label><select id="cronNodeId" class="w-full bg-darkbg border border-darkborder rounded p-2 text-white text-sm" required><option value="">Lade Hosts...</option></select></div>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label class="block text-xs font-medium text-gray-500 mb-1">Aktion</label>
<select id="cronAction" onchange="checkCronAction()" class="w-full bg-darkbg border border-darkborder rounded p-2 text-white text-sm" required>
<option value="reboot_node">Ganzen Server neustarten</option>
<option value="start_vm">VM Starten</option>
<option value="stop_vm">VM Stoppen</option>
<option value="reboot_vm">VM Neustarten</option>
</select>
</div>
<div id="cronVmBlock" class="hidden">
<label class="block text-xs font-medium text-gray-500 mb-1">Ziel VM-ID</label>
<input type="number" id="cronTargetVmid" class="w-full bg-darkbg border border-darkborder rounded p-2 text-white text-sm" placeholder="z.B. 100">
</div>
<div>
<label class="block text-xs font-medium text-gray-500 mb-1">Zeitplan (Cron-Format)</label>
<input type="text" id="cronSchedule" value="0 3 * * *" class="w-full bg-darkbg border border-darkborder rounded p-2 text-proxmox font-mono text-sm font-bold" required title="Minute Stunde Tag Monat Wochentag">
<p class="text-[10px] text-gray-500 mt-1">Beispiel: "0 3 * * *" = Jeden Tag um 03:00 Uhr.</p>
</div>
</div>
<button type="submit" class="bg-proxmox hover:bg-orange-600 text-white font-bold py-2 px-6 rounded transition-colors text-sm w-full md:w-auto">Job Speichern</button>
</form>
</div>
<div class="bg-darkcard border border-darkborder rounded-xl shadow-lg flex-1 overflow-hidden flex flex-col">
<table class="w-full text-left text-sm text-gray-400">
<thead class="bg-darkbg border-b border-darkborder text-xs uppercase sticky top-0">
<tr><th class="px-4 py-3">Status</th><th class="px-4 py-3">Beschreibung</th><th class="px-4 py-3">Host / Ziel</th><th class="px-4 py-3">Cron</th><th class="px-4 py-3">Letzter Lauf</th><th class="px-4 py-3 text-right">Löschen</th></tr>
</thead>
<tbody id="cronTableBody" class="divide-y divide-darkborder"></tbody>
</table>
</div>
</div>
</div>
</div>
<!-- NEU: AUDIT LOG MODAL -->
<div id="auditLogModal" class="fixed inset-0 bg-black/80 hidden z-[100] flex items-center justify-center backdrop-blur-sm">
<div class="bg-darkcard border border-darkborder rounded-xl shadow-2xl w-full max-w-5xl overflow-hidden flex flex-col max-h-[90vh]">
<div class="p-5 border-b border-darkborder flex justify-between items-center bg-darkbg">
<h2 class="text-xl font-bold text-white flex items-center gap-2">
<svg class="w-6 h-6 text-proxmox" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path></svg>
Aktivitäten-Protokoll (Audit Log)
</h2>
<button onclick="closeAuditLog()" class="text-gray-400 hover:text-white transition-colors">✕</button>
</div>
<div class="p-6 overflow-y-auto flex-1 bg-darkbg">
<div class="bg-darkcard border border-darkborder rounded-xl shadow-lg flex-1 overflow-hidden flex flex-col">
<table class="w-full text-left text-sm text-gray-400">
<thead class="bg-darkbg border-b border-darkborder text-xs uppercase sticky top-0">
<tr><th class="px-4 py-3">Datum / Uhrzeit</th><th class="px-4 py-3">Benutzer</th><th class="px-4 py-3">Aktion</th><th class="px-4 py-3">Ziel / Details</th></tr>
</thead>
<tbody id="auditLogTableBody" class="divide-y divide-darkborder"></tbody>
</table>
</div>
</div>
</div>
</div>
<!-- NEU: PASSWORT ÄNDERN MODAL -->
<div id="passwordModal" class="fixed inset-0 bg-black/80 hidden z-[100] flex items-center justify-center backdrop-blur-sm">
<div class="bg-darkcard border border-darkborder rounded-xl shadow-2xl w-full max-w-sm overflow-hidden flex flex-col">
<div class="p-5 border-b border-darkborder flex justify-between items-center bg-darkbg">
<h2 class="text-lg font-bold text-white flex items-center gap-2">🔑 Passwort ändern</h2>
<button onclick="closePasswordModal()" class="text-gray-400 hover:text-white transition-colors text-xl">✕</button>
</div>
<div class="p-6">
<form id="changePasswordForm" class="space-y-4">
<div>
<label class="block text-xs font-medium text-gray-500 mb-1">Aktuelles Passwort</label>
<input type="password" id="oldPassword" class="w-full bg-darkbg border border-darkborder rounded p-2 text-white text-sm focus:border-proxmox" required>
</div>
<div>
<label class="block text-xs font-medium text-gray-500 mb-1">Neues Passwort</label>
<input type="password" id="newPassword" class="w-full bg-darkbg border border-darkborder rounded p-2 text-white text-sm focus:border-proxmox" required>
</div>
<div>
<label class="block text-xs font-medium text-gray-500 mb-1">Neues Passwort bestätigen</label>
<input type="password" id="newPasswordConfirm" class="w-full bg-darkbg border border-darkborder rounded p-2 text-white text-sm focus:border-proxmox" required>
</div>
<button type="submit" class="w-full bg-proxmox hover:bg-orange-600 text-white font-bold py-2.5 rounded mt-2 transition-colors">Passwort aktualisieren</button>
</form>
</div>
</div>
</div>