Compare commits
14
Commits
V1.0.1
...
3c49b306ab
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c49b306ab | ||
|
|
a39a04360d | ||
|
|
317cba0cbb | ||
|
|
487c49f6e7 | ||
|
|
5fcd306f5d | ||
|
|
b84effa66b | ||
|
|
4f0ce0f1a2 | ||
|
|
50140377e8 | ||
|
|
d726d048ac | ||
|
|
fffd05d490 | ||
|
|
5fb5cfd159 | ||
|
|
e72a9a53e7 | ||
|
|
a50a944f64 | ||
|
|
e13319cb88 |
No files matched your search
@@ -1,5 +1,7 @@
|
||||
# Proxmox Unified Console (PUC) 🚀
|
||||
|
||||

|
||||
|
||||
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.
|
||||
@@ -58,19 +60,20 @@ services:
|
||||
- ./data:/var/www/data
|
||||
environment:
|
||||
- TZ=Europe/Berlin
|
||||
|
||||
```
|
||||
2. Starten:
|
||||
```
|
||||
Bash
|
||||
|
||||
docker compose up -d
|
||||
|
||||
```
|
||||
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:
|
||||
Bash
|
||||
|
||||
```
|
||||
sudo chown -R 33:33 ./data
|
||||
sudo chmod -R 775 ./data
|
||||
|
||||
```
|
||||
4. Login:
|
||||
Rufe https://<DEINE-IP>:8443 in deinem Browser auf (Zertifikatswarnung ignorieren).
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 280 KiB |
+16
-25
@@ -1,6 +1,6 @@
|
||||
<?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);
|
||||
|
||||
require_once 'db.php';
|
||||
@@ -60,7 +60,6 @@ if (!isset($_SESSION['user_id'])) {
|
||||
exit;
|
||||
}
|
||||
|
||||
// === PASSWORT ÄNDERN ===
|
||||
if ($action === 'change_password') {
|
||||
$oldPass = $_POST['old_password'] ?? '';
|
||||
$newPass = $_POST['new_password'] ?? '';
|
||||
@@ -86,44 +85,37 @@ if ($action === 'change_password') {
|
||||
$isAdmin = ($_SESSION['role'] ?? '') === 'admin';
|
||||
@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) {
|
||||
$port = ($type === 'pbs') ? 8007 : 8006;
|
||||
|
||||
// 1. Ticket holen (Login)
|
||||
if ($type === 'pmg' || $type === 'pbs') {
|
||||
// TICKET AUTH für PBS und PMG
|
||||
$chAuth = curl_init("https://{$ip}:{$port}/api2/json/access/ticket");
|
||||
curl_setopt_array($chAuth, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_SSL_VERIFYPEER => false,
|
||||
CURLOPT_SSL_VERIFYHOST => false,
|
||||
CURLOPT_TIMEOUT => 4,
|
||||
CURLOPT_RETURNTRANSFER => true, CURLOPT_SSL_VERIFYPEER => false,
|
||||
CURLOPT_SSL_VERIFYHOST => false, CURLOPT_TIMEOUT => 4,
|
||||
CURLOPT_POST => true,
|
||||
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' => 'Authentifizierung fehlgeschlagen'];
|
||||
}
|
||||
|
||||
// 2. Passendes Cookie für das System wählen
|
||||
$cookieName = 'PVEAuthCookie';
|
||||
if ($type === 'pbs') $cookieName = 'PBSAuthCookie';
|
||||
if ($type === 'pmg') $cookieName = 'PMGAuthCookie';
|
||||
if(!isset($authRes['data']['ticket'])) return ['data' => null, 'error' => 'Auth failed'];
|
||||
|
||||
$cookieName = ($type === 'pbs') ? 'PBSAuthCookie' : 'PMGAuthCookie';
|
||||
$headers = [
|
||||
"Cookie: {$cookieName}=" . $authRes['data']['ticket'],
|
||||
"CSRFPreventionToken: " . $authRes['data']['CSRFPreventionToken']
|
||||
];
|
||||
} else {
|
||||
// NATIVE API TOKENS FÜR PVE
|
||||
$headers = ["Authorization: PVEAPIToken={$tokenId}={$tokenSecret}"];
|
||||
}
|
||||
|
||||
// 3. Eigentlichen API-Call ausführen
|
||||
$ch = curl_init("https://{$ip}:{$port}{$endpoint}");
|
||||
$options = [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_SSL_VERIFYPEER => false,
|
||||
CURLOPT_SSL_VERIFYHOST => false,
|
||||
CURLOPT_TIMEOUT => $timeout,
|
||||
CURLOPT_RETURNTRANSFER => true, CURLOPT_SSL_VERIFYPEER => false,
|
||||
CURLOPT_SSL_VERIFYHOST => false, CURLOPT_TIMEOUT => $timeout,
|
||||
CURLOPT_HTTPHEADER => $headers
|
||||
];
|
||||
|
||||
@@ -135,8 +127,7 @@ function getProxmoxData($ip, $tokenId, $tokenSecret, $endpoint, $type = 'pve', $
|
||||
}
|
||||
|
||||
curl_setopt_array($ch, $options);
|
||||
$res = curl_exec($ch);
|
||||
curl_close($ch);
|
||||
$res = curl_exec($ch); curl_close($ch);
|
||||
|
||||
return json_decode($res, true) ?: [];
|
||||
}
|
||||
|
||||
+219
-46
@@ -1,18 +1,48 @@
|
||||
<?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') {
|
||||
$stmt = $pdo->query("SELECT * FROM nodes WHERE type = 'pve'"); $nodes = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
$data = ['cpu_cores' => 0, 'cpu_used' => 0, 'ram_total' => 0, 'ram_used' => 0, 'disk_total' => 0, 'disk_used' => 0, 'cpu_percent' => 0];
|
||||
$stmt = $pdo->query("SELECT * FROM nodes WHERE type = 'pve'");
|
||||
$nodes = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
$data = [
|
||||
'cpu_cores' => 0, 'cpu_used' => 0, 'ram_total' => 0, 'ram_used' => 0,
|
||||
'disk_total' => 0, 'disk_used' => 0, 'cpu_percent' => 0, 'nodes_net' => [],
|
||||
'cluster_stats' => ['nodes_total' => count($nodes), 'nodes_online' => 0, 'vms_total' => 0, 'vms_running' => 0, 'vms_stopped' => 0]
|
||||
];
|
||||
$seenNodes = []; $seenVms = [];
|
||||
foreach ($nodes as $node) {
|
||||
$nData = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes");
|
||||
if (isset($nData['data'])) {
|
||||
foreach ($nData['data'] as $nInfo) {
|
||||
if (!isset($seenNodes[$nInfo['node']])) {
|
||||
$seenNodes[$nInfo['node']] = true;
|
||||
if ($nInfo['status'] === 'online') {
|
||||
$data['cpu_cores'] += $nInfo['maxcpu'] ?? 0; $data['cpu_used'] += ($nInfo['cpu'] ?? 0) * ($nInfo['maxcpu'] ?? 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') {
|
||||
$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) {
|
||||
$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); });
|
||||
echo json_encode(['success' => true, 'data' => array_slice($allVms, 0, 5)]); exit;
|
||||
}
|
||||
|
||||
if ($action === 'get_all_vms') {
|
||||
$stmt = $pdo->query("SELECT * FROM nodes WHERE type = 'pve'"); $nodes = $stmt->fetchAll(PDO::FETCH_ASSOC); $allVms = [];
|
||||
$stmt = $pdo->query("SELECT * FROM nodes WHERE type = 'pve'"); $nodes = $stmt->fetchAll(PDO::FETCH_ASSOC); $allVms = []; $seenVms = [];
|
||||
foreach ($nodes as $node) {
|
||||
$vms = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/cluster/resources?type=vm");
|
||||
if (isset($vms['data'])) { foreach ($vms['data'] as $vm) { 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']; });
|
||||
echo json_encode(['success' => true, 'data' => $allVms]); exit;
|
||||
}
|
||||
|
||||
// NEU: Node Dashboard Daten (VMs & Storage)
|
||||
if ($action === 'get_node_dashboard') {
|
||||
if (!$isAdmin) exit;
|
||||
$stmt = $pdo->prepare("SELECT * FROM nodes WHERE id = ?"); $stmt->execute([$_GET['node_id']]); $node = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
$host = $_GET['host'];
|
||||
|
||||
$vmsQemu = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$host}/qemu");
|
||||
$vmsLxc = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$host}/lxc");
|
||||
$storages = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$host}/storage");
|
||||
|
||||
$nodeVms = [];
|
||||
if (isset($vmsQemu['data'])) { foreach ($vmsQemu['data'] as $v) { $v['type'] = 'qemu'; $v['host'] = $host; $v['node_id'] = $node['id']; $v['node_ip'] = $node['ip_address']; $nodeVms[] = $v; } }
|
||||
if (isset($vmsLxc['data'])) { foreach ($vmsLxc['data'] as $v) { $v['type'] = 'lxc'; $v['host'] = $host; $v['node_id'] = $node['id']; $v['node_ip'] = $node['ip_address']; $nodeVms[] = $v; } }
|
||||
usort($nodeVms, function($a, $b) { if ($a['status'] === 'running' && $b['status'] !== 'running') return -1; if ($a['status'] !== 'running' && $b['status'] === 'running') return 1; return $a['vmid'] <=> $b['vmid']; });
|
||||
|
||||
echo json_encode(['success' => true, 'vms' => $nodeVms, 'storages' => $storages['data'] ?? []]); exit;
|
||||
}
|
||||
|
||||
if ($action === 'vm_action') {
|
||||
if (!checkVmPermission($pdo, $_POST['vmid'])) exit;
|
||||
$stmt = $pdo->prepare("SELECT * FROM nodes WHERE id = ?"); $stmt->execute([$_POST['node_id']]); $node = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
$res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$_POST['host']}/{$_POST['type']}/{$_POST['vmid']}/status/{$_POST['cmd']}", "POST");
|
||||
|
||||
// AUDIT LOG
|
||||
$res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$_POST['host']}/{$_POST['type']}/{$_POST['vmid']}/status/{$_POST['cmd']}", 'pve', 'POST');
|
||||
logAudit("VM {$_POST['cmd']}", "VMID: {$_POST['vmid']} auf Host: {$_POST['host']}");
|
||||
echo json_encode(['success' => isset($res['data']), 'error' => $res['errors'] ?? '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 === 'delete_node') { if (!$isAdmin) exit; $stmt = $pdo->prepare("DELETE FROM nodes WHERE id = ?"); $stmt->execute([$_POST['id']]); logAudit('Server gelöscht', "Node ID: {$_POST['id']}"); echo json_encode(['success' => true]); exit; }
|
||||
|
||||
if ($action === 'add_node') {
|
||||
if (!$isAdmin) exit;
|
||||
$stmt = $pdo->prepare("INSERT INTO nodes (name, ip_address, token_id, token_secret, type) VALUES (?, ?, ?, ?, ?)");
|
||||
$stmt->execute([$_POST['name'], $_POST['ip'], $_POST['user'], $_POST['pass'], $_POST['type'] ?? 'pve']);
|
||||
logAudit('Server hinzugefügt', "Node: {$_POST['name']} ({$_POST['ip']})");
|
||||
$name = trim($_POST['name']); $ip = trim($_POST['ip']); $user = trim($_POST['user']); $pass = trim($_POST['pass']); $type = $_POST['type'] ?? 'pve';
|
||||
$tokenIdToSave = $user; $tokenSecretToSave = $pass;
|
||||
if ($type === 'pve') {
|
||||
$chAuth = curl_init("https://{$ip}:8006/api2/json/access/ticket");
|
||||
curl_setopt_array($chAuth, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_SSL_VERIFYPEER => false, CURLOPT_SSL_VERIFYHOST => false, CURLOPT_TIMEOUT => 5, CURLOPT_POST => true, CURLOPT_POSTFIELDS => http_build_query(['username' => $user, 'password' => $pass]) ]);
|
||||
$authRes = json_decode(curl_exec($chAuth), true); curl_close($chAuth);
|
||||
if(!isset($authRes['data']['ticket'])) { echo json_encode(['success' => false, 'error' => 'Proxmox Login fehlgeschlagen. Passwort falsch?']); exit; }
|
||||
$ticket = $authRes['data']['ticket']; $csrf = $authRes['data']['CSRFPreventionToken'];
|
||||
$tokenName = 'pvedash' . rand(1000, 9999);
|
||||
$chToken = curl_init("https://{$ip}:8006/api2/json/access/users/{$user}/token/{$tokenName}");
|
||||
curl_setopt_array($chToken, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_SSL_VERIFYPEER => false, CURLOPT_SSL_VERIFYHOST => false, CURLOPT_TIMEOUT => 5, CURLOPT_POST => true, CURLOPT_POSTFIELDS => http_build_query(['privsep' => 0]), CURLOPT_HTTPHEADER => ["Cookie: PVEAuthCookie={$ticket}", "CSRFPreventionToken: {$csrf}"] ]);
|
||||
$tokenRes = json_decode(curl_exec($chToken), true); curl_close($chToken);
|
||||
if(!isset($tokenRes['data']['value'])) { echo json_encode(['success' => false, 'error' => 'Konnte API Token nicht erstellen. Admin-Rechte?']); exit; }
|
||||
$tokenIdToSave = $user . '!' . $tokenName; $tokenSecretToSave = $tokenRes['data']['value'];
|
||||
}
|
||||
$stmt = $pdo->prepare("INSERT INTO nodes (name, ip_address, token_id, token_secret, type) VALUES (?, ?, ?, ?, ?)"); $stmt->execute([$name, $ip, $tokenIdToSave, $tokenSecretToSave, $type]);
|
||||
logAudit('Server hinzugefügt', "Node: {$name} ({$ip})");
|
||||
echo json_encode(['success' => true]); exit;
|
||||
}
|
||||
|
||||
if ($action === 'get_users') { if (!$isAdmin) exit; $stmt = $pdo->query("SELECT id, username, role FROM users"); echo json_encode(['success' => true, 'data' => $stmt->fetchAll(PDO::FETCH_ASSOC)]); exit; }
|
||||
if ($action === 'delete_user') { if (!$isAdmin) exit; $stmt = $pdo->prepare("DELETE FROM users WHERE id = ?"); $stmt->execute([$_POST['id']]); logAudit('User gelöscht', "User ID: {$_POST['id']}"); echo json_encode(['success' => true]); exit; }
|
||||
if ($action === 'create_user') {
|
||||
if (!$isAdmin) exit;
|
||||
$stmt = $pdo->prepare("INSERT INTO users (username, password_hash, role, permissions) VALUES (?, ?, ?, ?)");
|
||||
$stmt->execute([$_POST['username'], password_hash($_POST['password'], PASSWORD_DEFAULT), $_POST['role'], $_POST['permissions']]);
|
||||
$stmt = $pdo->prepare("INSERT INTO users (username, password_hash, role, permissions) VALUES (?, ?, ?, ?)"); $stmt->execute([$_POST['username'], password_hash($_POST['password'], PASSWORD_DEFAULT), $_POST['role'], $_POST['permissions']]);
|
||||
logAudit('User angelegt', "Username: {$_POST['username']} Rolle: {$_POST['role']}");
|
||||
echo json_encode(['success' => true]); exit;
|
||||
}
|
||||
|
||||
if ($action === 'get_pve_nodes') {
|
||||
if (!$isAdmin) exit;
|
||||
$stmt = $pdo->query("SELECT * FROM nodes WHERE type = 'pve'"); $resList = [];
|
||||
$stmt = $pdo->query("SELECT * FROM nodes WHERE type = 'pve'"); $resList = []; $seenNodes = [];
|
||||
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $node) {
|
||||
$nData = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes");
|
||||
if(isset($nData['data'])) { foreach($nData['data'] as $n) { if($n['status'] === 'online') { $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;
|
||||
}
|
||||
@@ -87,9 +169,9 @@ if ($action === 'create_vm') {
|
||||
if(!isset($nextIdRes['data'])) { echo json_encode(['success' => false, 'error' => 'Konnte keine freie VMID finden.']); exit; }
|
||||
$vmid = $nextIdRes['data'];
|
||||
$params = ['vmid' => $vmid, 'name' => $_POST['name'], 'memory' => $_POST['memory'], 'cores' => $_POST['cores'], 'net0' => 'virtio,bridge=vmbr0'];
|
||||
$res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$_POST['host']}/qemu", "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']}");
|
||||
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') {
|
||||
@@ -99,6 +181,7 @@ if ($action === 'get_vm_config') {
|
||||
echo json_encode(['success' => true, 'data' => $res['data'] ?? []]); exit;
|
||||
}
|
||||
|
||||
// ANGEPASST: Update inkl. ISO und onboot
|
||||
if ($action === 'update_vm_config') {
|
||||
if (!checkVmPermission($pdo, $_POST['vmid'])) exit;
|
||||
$stmt = $pdo->prepare("SELECT * FROM nodes WHERE id = ?"); $stmt->execute([$_POST['node_id']]); $node = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
@@ -106,9 +189,12 @@ if ($action === 'update_vm_config') {
|
||||
if(isset($_POST['memory'])) $params['memory'] = $_POST['memory'];
|
||||
if(isset($_POST['cores'])) $params['cores'] = $_POST['cores'];
|
||||
if(isset($_POST['net0'])) $params['net0'] = $_POST['net0'];
|
||||
$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']}");
|
||||
echo json_encode(['success' => isset($res['data'])]); exit;
|
||||
echo json_encode(['success' => isset($res['data']), 'error' => 'API Fehler']); exit;
|
||||
}
|
||||
|
||||
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);
|
||||
$cfg = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$_POST['host']}/{$_POST['type']}/{$_POST['vmid']}/config");
|
||||
$nextNic = 0; for ($i=0; $i<10; $i++) { if (!isset($cfg['data']["net{$i}"])) { $nextNic = $i; break; } }
|
||||
$res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$_POST['host']}/{$_POST['type']}/{$_POST['vmid']}/config", "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}"]); exit;
|
||||
$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']}"]);
|
||||
echo json_encode(['success' => isset($res['data']), 'slot' => "net{$nextNic}", 'error' => 'API Fehler']); exit;
|
||||
}
|
||||
|
||||
if ($action === 'resize_vm_disk') {
|
||||
if (!checkVmPermission($pdo, $_POST['vmid'])) exit;
|
||||
$stmt = $pdo->prepare("SELECT * FROM nodes WHERE id = ?"); $stmt->execute([$_POST['node_id']]); $node = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
$res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$_POST['host']}/{$_POST['type']}/{$_POST['vmid']}/resize", "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'])]); exit;
|
||||
$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']]);
|
||||
echo json_encode(['success' => isset($res['data']), 'error' => 'API Fehler']); exit;
|
||||
}
|
||||
|
||||
if ($action === 'delete_vm') {
|
||||
if (!$isAdmin) exit;
|
||||
$stmt = $pdo->prepare("SELECT * FROM nodes WHERE id = ?"); $stmt->execute([$_POST['node_id']]); $node = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
$res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$_POST['host']}/{$_POST['type']}/{$_POST['vmid']}", "DELETE");
|
||||
logAudit('VM Gelöscht', "VMID: {$_POST['vmid']}");
|
||||
echo json_encode(['success' => isset($res['data'])]); exit;
|
||||
$res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$_POST['host']}/{$_POST['type']}/{$_POST['vmid']}", 'pve', 'DELETE');
|
||||
echo json_encode(['success' => isset($res['data']), 'error' => 'API Fehler']); exit;
|
||||
}
|
||||
|
||||
// NEU: ISO Images abrufen
|
||||
if ($action === 'get_isos') {
|
||||
if (!$isAdmin) exit;
|
||||
$stmt = $pdo->prepare("SELECT * FROM nodes WHERE id = ?"); $stmt->execute([$_GET['node_id']]); $node = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
$stRes = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$_GET['host']}/storage");
|
||||
$isos = [];
|
||||
if (isset($stRes['data'])) {
|
||||
foreach ($stRes['data'] as $st) {
|
||||
if (strpos($st['content'], 'iso') !== false) {
|
||||
$cRes = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$_GET['host']}/storage/{$st['storage']}/content");
|
||||
if (isset($cRes['data'])) {
|
||||
foreach ($cRes['data'] as $item) {
|
||||
if ($item['content'] === 'iso') $isos[] = $item['volid'];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
echo json_encode(['success' => true, 'data' => $isos]); exit;
|
||||
}
|
||||
|
||||
// NEU: Node-Spezifische Backup Jobs
|
||||
if ($action === 'get_node_backup_jobs') {
|
||||
if (!$isAdmin) exit;
|
||||
$stmt = $pdo->prepare("SELECT * FROM nodes WHERE id = ?"); $stmt->execute([$_GET['node_id']]); $node = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
$res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/cluster/backup");
|
||||
$hostJobs = [];
|
||||
if (isset($res['data'])) {
|
||||
foreach ($res['data'] as $job) {
|
||||
// Zeige nur Jobs, die diesen Host betreffen (oder alle, falls node nicht spezifiziert in PVE)
|
||||
if (empty($job['node']) || $job['node'] === $_GET['host']) {
|
||||
$hostJobs[] = $job;
|
||||
}
|
||||
}
|
||||
}
|
||||
echo json_encode(['success' => true, 'data' => $hostJobs]); exit;
|
||||
}
|
||||
|
||||
if ($action === 'get_vm_snapshots') {
|
||||
@@ -148,11 +269,10 @@ if ($action === 'vm_snapshot_action') {
|
||||
if (!checkVmPermission($pdo, $_POST['vmid'])) exit;
|
||||
$stmt = $pdo->prepare("SELECT * FROM nodes WHERE id = ?"); $stmt->execute([$_POST['node_id']]); $node = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
$cmd = $_POST['cmd']; $snapname = $_POST['snapname'];
|
||||
if ($cmd === 'create') { $res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$_POST['host']}/{$_POST['type']}/{$_POST['vmid']}/snapshot", "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 === '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"); }
|
||||
logAudit('Snapshot ' . $cmd, "VMID: {$_POST['vmid']} Snap: {$snapname}");
|
||||
echo json_encode(['success' => isset($res['data'])]); exit;
|
||||
if ($cmd === 'create') { $res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$_POST['host']}/{$_POST['type']}/{$_POST['vmid']}/snapshot", 'pve', 'POST', ['snapname' => $snapname]); }
|
||||
elseif ($cmd === 'delete') { $res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$_POST['host']}/{$_POST['type']}/{$_POST['vmid']}/snapshot/{$snapname}", 'pve', 'DELETE'); }
|
||||
elseif ($cmd === 'rollback') { $res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$_POST['host']}/{$_POST['type']}/{$_POST['vmid']}/snapshot/{$snapname}/rollback", 'pve', 'POST'); }
|
||||
echo json_encode(['success' => isset($res['data']), 'error' => 'API Fehler']); exit;
|
||||
}
|
||||
|
||||
if ($action === 'get_backup_storages') {
|
||||
@@ -183,25 +303,78 @@ if ($action === 'get_vm_backups') {
|
||||
if ($action === 'create_backup') {
|
||||
if (!checkVmPermission($pdo, $_POST['vmid'])) exit;
|
||||
$stmt = $pdo->prepare("SELECT * FROM nodes WHERE id = ?"); $stmt->execute([$_POST['node_id']]); $node = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
$res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$_POST['host']}/vzdump", "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'])]); exit;
|
||||
$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']);
|
||||
echo json_encode(['success' => isset($res['data']), 'error' => 'API Fehler']); exit;
|
||||
}
|
||||
|
||||
if ($action === 'restore_backup') {
|
||||
if (!checkVmPermission($pdo, $_POST['vmid'])) exit;
|
||||
$stmt = $pdo->prepare("SELECT * FROM nodes WHERE id = ?"); $stmt->execute([$_POST['node_id']]); $node = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
$res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$_POST['host']}/{$_POST['type']}/{$_POST['vmid']}/status/current");
|
||||
if (isset($res['data']) && $res['data']['status'] === 'running') { echo json_encode(['success' => false, 'error' => 'VM muss gestoppt sein!']); 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]);
|
||||
logAudit('Backup Restore', "VMID: {$_POST['vmid']} Archive: {$_POST['archive']}");
|
||||
echo json_encode(['success' => isset($resRestore['data'])]); 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']}", 'pve', 'POST', ['vmid' => $_POST['vmid'], 'archive' => $_POST['archive'], 'force' => 1]);
|
||||
echo json_encode(['success' => isset($resRestore['data']), 'error' => 'API Fehler']); exit;
|
||||
}
|
||||
|
||||
if ($action === 'get_node_status' || $action === 'get_vm_status') {
|
||||
$stmt = $pdo->prepare("SELECT * FROM nodes WHERE id = ?"); $stmt->execute([$_GET['node_id']]); $node = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
$endpoint = $action === 'get_node_status' ? "/api2/json/nodes/{$_GET['host']}/status" : "/api2/json/nodes/{$_GET['host']}/{$_GET['type']}/{$_GET['vmid']}/status/current";
|
||||
$res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], $endpoint);
|
||||
$type = $node['type'] ?? 'pve';
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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
File diff suppressed because it is too large.
Load diff
+12
-10
@@ -2,10 +2,14 @@
|
||||
// /home/docker/pve_dashboard/src/cron.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) {
|
||||
if ($time === null) $time = time();
|
||||
$cronParts = explode(' ', trim($cron));
|
||||
|
||||
// FIX 2: Ignoriert beliebig viele Leerzeichen zwischen den Werten
|
||||
$cronParts = preg_split('/\s+/', trim($cron));
|
||||
if (count($cronParts) !== 5) return false;
|
||||
|
||||
list($min, $hour, $day, $month, $weekday) = $cronParts;
|
||||
@@ -19,8 +23,11 @@ function isCronMatch($cron, $time = null) {
|
||||
|
||||
function matchCronPart($part, $current) {
|
||||
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);
|
||||
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);
|
||||
curl_setopt_array($ch, $options);
|
||||
$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";
|
||||
|
||||
// Hole alle aktiven Tasks
|
||||
$stmt = $pdo->query("SELECT * FROM scheduled_tasks WHERE is_active = 1");
|
||||
$tasks = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
@@ -59,7 +65,6 @@ foreach ($tasks as $task) {
|
||||
if ($node) {
|
||||
$success = false;
|
||||
|
||||
// AKTION: PVE NODE NEUSTART
|
||||
if ($task['action_type'] === 'reboot_node') {
|
||||
$nData = cronProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes", 'GET');
|
||||
if (isset($nData['data'][0]['node'])) {
|
||||
@@ -68,9 +73,7 @@ foreach ($tasks as $task) {
|
||||
$success = true;
|
||||
}
|
||||
}
|
||||
// AKTION: VM START / STOP / REBOOT
|
||||
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');
|
||||
if (isset($vmsRes['data'])) {
|
||||
foreach ($vmsRes['data'] as $vm) {
|
||||
@@ -85,7 +88,6 @@ foreach ($tasks as $task) {
|
||||
}
|
||||
}
|
||||
|
||||
// Setze den Zeitstempel für den letzten Durchlauf
|
||||
if ($success) {
|
||||
$uStmt = $pdo->prepare("UPDATE scheduled_tasks SET last_run = ? WHERE id = ?");
|
||||
$uStmt->execute([time(), $task['id']]);
|
||||
|
||||
+100
-57
@@ -38,100 +38,143 @@ $nodeCount = $stmt->fetchColumn();
|
||||
<?php if ($isLoggedIn): ?>
|
||||
<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>
|
||||
|
||||
<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="openPasswordModal()" class="text-gray-400 hover:text-white transition-colors" title="Passwort ändern">🔑</button>
|
||||
<button onclick="logout()" class="text-red-400 hover:text-red-300 font-medium transition-colors">Abmelden</button>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="text-sm text-gray-400">System Status: <span class="text-green-500 font-bold">Sicher</span></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="flex-grow p-6 flex items-center justify-center max-w-[1600px] w-full mx-auto">
|
||||
<?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): ?>
|
||||
<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: ?>
|
||||
<div class="w-full h-[85vh] flex bg-darkbg overflow-hidden rounded-xl border border-darkborder shadow-2xl">
|
||||
<!-- SIDEBAR -->
|
||||
<aside class="w-64 bg-darkcard border-r border-darkborder flex flex-col">
|
||||
<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 p-4 space-y-2">
|
||||
<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-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>
|
||||
<aside class="w-64 bg-darkcard border-r border-darkborder flex flex-col shrink-0">
|
||||
<nav class="flex-1 overflow-y-auto space-y-2 py-4">
|
||||
<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">📊 PVE Cluster</a>
|
||||
|
||||
<div class="pt-4 mt-4 border-t border-darkborder 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>
|
||||
<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="#" 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>
|
||||
<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>
|
||||
|
||||
<?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 class="mt-4 border-t border-darkborder pt-4 space-y-2">
|
||||
<div class="px-4 mb-2"><span class="text-xs font-bold text-gray-500 uppercase tracking-wider">Erweiterungen</span></div>
|
||||
<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="#" 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>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
<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">
|
||||
|
||||
<!-- 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 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"><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="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="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="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>
|
||||
<!-- Top Kachel Cluster Status -->
|
||||
<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="flex items-center gap-4">
|
||||
<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><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="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="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 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>
|
||||
|
||||
<!-- TAB 2: PBS -->
|
||||
<div id="tab-pbs" class="flex-1 flex flex-col min-w-0 hidden opacity-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 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>
|
||||
<!-- TAB NODE VIEW -->
|
||||
<div id="tab-node-view" class="hidden opacity-0 flex-1 flex flex-col min-w-0 transition-opacity duration-300">
|
||||
<div class="flex justify-between items-center border-b border-darkborder pb-4 mb-6">
|
||||
<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>
|
||||
|
||||
<!-- TAB 3: PMG -->
|
||||
<div id="tab-pmg" class="flex-1 flex flex-col min-w-0 hidden opacity-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 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 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>
|
||||
|
||||
<!-- RECHTS: JOBS -->
|
||||
<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>
|
||||
<!-- TAB PBS -->
|
||||
<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">Backup Datastores</h2></div>
|
||||
<div id="pbs-datastores-container" class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6"></div>
|
||||
</div>
|
||||
|
||||
<!-- TAB PMG -->
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<?php include 'modals.php'; ?>
|
||||
<?php endif; ?>
|
||||
</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) © <?= 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>
|
||||
|
||||
<!-- HIER IST DER CACHE-BUSTER, DAMIT DER LOGOUT IMMER GEHT! -->
|
||||
<script src="app.js?v=<?= time() ?>"></script>
|
||||
</body>
|
||||
</html>
|
||||
+10
-103
@@ -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="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="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="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="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 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 class="p-6 overflow-y-auto flex-1 flex flex-col gap-6 bg-darkbg">
|
||||
<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="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 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>
|
||||
|
||||
<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>
|
||||
<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>
|
||||
Reference in new issue
Block a user