Release v1.0.2: Hotfix, Live Diagramme VMs und PVE Nodex
This commit is contained in:
@@ -1,5 +1,7 @@
|
|||||||
# Proxmox Unified Console (PUC) 🚀
|
# 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.
|
A lightning-fast, unified web dashboard to manage your **Proxmox Virtual Environment (PVE)**, **Proxmox Backup Server (PBS)**, and **Proxmox Mail Gateway (PMG)** from a single, clean interface.
|
||||||
|
|
||||||
Built entirely with native APIs—no slow iframes, no CORS issues.
|
Built entirely with native APIs—no slow iframes, no CORS issues.
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 280 KiB |
+25
-29
@@ -85,40 +85,37 @@ if ($action === 'change_password') {
|
|||||||
$isAdmin = ($_SESSION['role'] ?? '') === 'admin';
|
$isAdmin = ($_SESSION['role'] ?? '') === 'admin';
|
||||||
@session_write_close();
|
@session_write_close();
|
||||||
|
|
||||||
|
// === PROXMOX API FETCHER ===
|
||||||
function getProxmoxData($ip, $tokenId, $tokenSecret, $endpoint, $type = 'pve', $method = "GET", $postData = null, $timeout = 6) {
|
function getProxmoxData($ip, $tokenId, $tokenSecret, $endpoint, $type = 'pve', $method = "GET", $postData = null, $timeout = 6) {
|
||||||
$port = ($type === 'pbs') ? 8007 : 8006;
|
$port = ($type === 'pbs') ? 8007 : 8006;
|
||||||
|
|
||||||
$chAuth = curl_init("https://{$ip}:{$port}/api2/json/access/ticket");
|
if ($type === 'pmg' || $type === 'pbs') {
|
||||||
curl_setopt_array($chAuth, [
|
// TICKET AUTH für PBS und PMG
|
||||||
CURLOPT_RETURNTRANSFER => true,
|
$chAuth = curl_init("https://{$ip}:{$port}/api2/json/access/ticket");
|
||||||
CURLOPT_SSL_VERIFYPEER => false,
|
curl_setopt_array($chAuth, [
|
||||||
CURLOPT_SSL_VERIFYHOST => false,
|
CURLOPT_RETURNTRANSFER => true, CURLOPT_SSL_VERIFYPEER => false,
|
||||||
CURLOPT_TIMEOUT => 4,
|
CURLOPT_SSL_VERIFYHOST => false, CURLOPT_TIMEOUT => 4,
|
||||||
CURLOPT_POST => true,
|
CURLOPT_POST => true,
|
||||||
CURLOPT_POSTFIELDS => http_build_query(['username' => $tokenId, 'password' => $tokenSecret])
|
CURLOPT_POSTFIELDS => http_build_query(['username' => $tokenId, 'password' => $tokenSecret])
|
||||||
]);
|
]);
|
||||||
$authRes = json_decode(curl_exec($chAuth), true);
|
$authRes = json_decode(curl_exec($chAuth), true); curl_close($chAuth);
|
||||||
curl_close($chAuth);
|
|
||||||
|
if(!isset($authRes['data']['ticket'])) return ['data' => null, 'error' => 'Auth failed'];
|
||||||
if(!isset($authRes['data']['ticket'])) {
|
|
||||||
return ['data' => null, 'error' => 'Authentifizierung fehlgeschlagen'];
|
|
||||||
}
|
|
||||||
|
|
||||||
$cookieName = 'PVEAuthCookie';
|
$cookieName = ($type === 'pbs') ? 'PBSAuthCookie' : 'PMGAuthCookie';
|
||||||
if ($type === 'pbs') $cookieName = 'PBSAuthCookie';
|
$headers = [
|
||||||
if ($type === 'pmg') $cookieName = 'PMGAuthCookie';
|
"Cookie: {$cookieName}=" . $authRes['data']['ticket'],
|
||||||
|
"CSRFPreventionToken: " . $authRes['data']['CSRFPreventionToken']
|
||||||
$headers = [
|
];
|
||||||
"Cookie: {$cookieName}=" . $authRes['data']['ticket'],
|
} else {
|
||||||
"CSRFPreventionToken: " . $authRes['data']['CSRFPreventionToken']
|
// NATIVE API TOKENS FÜR PVE
|
||||||
];
|
$headers = ["Authorization: PVEAPIToken={$tokenId}={$tokenSecret}"];
|
||||||
|
}
|
||||||
|
|
||||||
$ch = curl_init("https://{$ip}:{$port}{$endpoint}");
|
$ch = curl_init("https://{$ip}:{$port}{$endpoint}");
|
||||||
$options = [
|
$options = [
|
||||||
CURLOPT_RETURNTRANSFER => true,
|
CURLOPT_RETURNTRANSFER => true, CURLOPT_SSL_VERIFYPEER => false,
|
||||||
CURLOPT_SSL_VERIFYPEER => false,
|
CURLOPT_SSL_VERIFYHOST => false, CURLOPT_TIMEOUT => $timeout,
|
||||||
CURLOPT_SSL_VERIFYHOST => false,
|
|
||||||
CURLOPT_TIMEOUT => $timeout,
|
|
||||||
CURLOPT_HTTPHEADER => $headers
|
CURLOPT_HTTPHEADER => $headers
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -130,8 +127,7 @@ function getProxmoxData($ip, $tokenId, $tokenSecret, $endpoint, $type = 'pve', $
|
|||||||
}
|
}
|
||||||
|
|
||||||
curl_setopt_array($ch, $options);
|
curl_setopt_array($ch, $options);
|
||||||
$res = curl_exec($ch);
|
$res = curl_exec($ch); curl_close($ch);
|
||||||
curl_close($ch);
|
|
||||||
|
|
||||||
return json_decode($res, true) ?: [];
|
return json_decode($res, true) ?: [];
|
||||||
}
|
}
|
||||||
|
|||||||
+147
-20
@@ -3,16 +3,64 @@
|
|||||||
if (!defined('PDO::ATTR_DRIVER_NAME')) exit; // Schutz vor direktem Aufruf
|
if (!defined('PDO::ATTR_DRIVER_NAME')) exit; // Schutz vor direktem Aufruf
|
||||||
|
|
||||||
if ($action === 'get_stats') {
|
if ($action === 'get_stats') {
|
||||||
$stmt = $pdo->query("SELECT * FROM nodes WHERE type = 'pve'"); $nodes = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
$stmt = $pdo->query("SELECT * FROM nodes WHERE type = 'pve'");
|
||||||
$data = ['cpu_cores' => 0, 'cpu_used' => 0, 'ram_total' => 0, 'ram_used' => 0, 'disk_total' => 0, 'disk_used' => 0, 'cpu_percent' => 0];
|
$nodes = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
$data = [
|
||||||
|
'cpu_cores' => 0, 'cpu_used' => 0, 'ram_total' => 0, 'ram_used' => 0,
|
||||||
|
'disk_total' => 0, 'disk_used' => 0, 'cpu_percent' => 0, 'nodes_net' => [],
|
||||||
|
'cluster_stats' => ['nodes_total' => count($nodes), 'nodes_online' => 0, 'vms_total' => 0, 'vms_running' => 0, 'vms_stopped' => 0]
|
||||||
|
];
|
||||||
|
|
||||||
|
$seenNodes = [];
|
||||||
|
$seenVms = [];
|
||||||
|
|
||||||
foreach ($nodes as $node) {
|
foreach ($nodes as $node) {
|
||||||
$nData = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes");
|
$nData = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes");
|
||||||
if (isset($nData['data'])) {
|
if (isset($nData['data'])) {
|
||||||
foreach ($nData['data'] as $nInfo) {
|
foreach ($nData['data'] as $nInfo) {
|
||||||
if ($nInfo['status'] === 'online') {
|
// Duplikate (z.B. in Clustern) vermeiden
|
||||||
$data['cpu_cores'] += $nInfo['maxcpu'] ?? 0; $data['cpu_used'] += ($nInfo['cpu'] ?? 0) * ($nInfo['maxcpu'] ?? 0);
|
if (!isset($seenNodes[$nInfo['node']])) {
|
||||||
$data['ram_total'] += $nInfo['maxmem'] ?? 0; $data['ram_used'] += $nInfo['mem'] ?? 0;
|
$seenNodes[$nInfo['node']] = true;
|
||||||
$data['disk_total'] += $nInfo['maxdisk'] ?? 0; $data['disk_used'] += $nInfo['disk'] ?? 0;
|
|
||||||
|
if ($nInfo['status'] === 'online') {
|
||||||
|
$data['cluster_stats']['nodes_online']++;
|
||||||
|
$data['cpu_cores'] += $nInfo['maxcpu'] ?? 0;
|
||||||
|
$data['cpu_used'] += ($nInfo['cpu'] ?? 0) * ($nInfo['maxcpu'] ?? 0);
|
||||||
|
$data['ram_total'] += $nInfo['maxmem'] ?? 0;
|
||||||
|
$data['ram_used'] += $nInfo['mem'] ?? 0;
|
||||||
|
$data['disk_total'] += $nInfo['maxdisk'] ?? 0;
|
||||||
|
$data['disk_used'] += $nInfo['disk'] ?? 0;
|
||||||
|
|
||||||
|
$nRrd = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$nInfo['node']}/rrddata?timeframe=hour&cf=AVERAGE");
|
||||||
|
$netin = 0; $netout = 0;
|
||||||
|
if (isset($nRrd['data']) && is_array($nRrd['data'])) {
|
||||||
|
for ($i = count($nRrd['data']) - 1; $i >= 0; $i--) {
|
||||||
|
if (isset($nRrd['data'][$i]['netin']) && $nRrd['data'][$i]['netin'] !== null) {
|
||||||
|
$netin = $nRrd['data'][$i]['netin'];
|
||||||
|
$netout = $nRrd['data'][$i]['netout'];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$data['nodes_net'][] = ['name' => $nInfo['node'], 'netin' => $netin, 'netout' => $netout];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// VMs von allen angebundenen Hosts ziehen und Duplikate filtern
|
||||||
|
$vms = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/cluster/resources?type=vm");
|
||||||
|
if (isset($vms['data'])) {
|
||||||
|
foreach($vms['data'] as $vm) {
|
||||||
|
$uniqueId = $vm['vmid'] . '@' . ($vm['node'] ?? 'unknown');
|
||||||
|
if (!isset($seenVms[$uniqueId])) {
|
||||||
|
$seenVms[$uniqueId] = true;
|
||||||
|
$data['cluster_stats']['vms_total']++;
|
||||||
|
if (isset($vm['status']) && $vm['status'] === 'running') {
|
||||||
|
$data['cluster_stats']['vms_running']++;
|
||||||
|
} else {
|
||||||
|
$data['cluster_stats']['vms_stopped']++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -22,12 +70,14 @@ if ($action === 'get_stats') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($action === 'get_top_vms') {
|
if ($action === 'get_top_vms') {
|
||||||
$stmt = $pdo->query("SELECT * FROM nodes WHERE type = 'pve'"); $nodes = $stmt->fetchAll(PDO::FETCH_ASSOC); $allVms = [];
|
$stmt = $pdo->query("SELECT * FROM nodes WHERE type = 'pve'"); $nodes = $stmt->fetchAll(PDO::FETCH_ASSOC); $allVms = []; $seenVms = [];
|
||||||
foreach ($nodes as $node) {
|
foreach ($nodes as $node) {
|
||||||
$vms = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/cluster/resources?type=vm");
|
$vms = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/cluster/resources?type=vm");
|
||||||
if (isset($vms['data'])) {
|
if (isset($vms['data'])) {
|
||||||
foreach ($vms['data'] as $vm) {
|
foreach ($vms['data'] as $vm) {
|
||||||
if (checkVmPermission($pdo, $vm['vmid'])) {
|
$uniqueId = $vm['vmid'] . '@' . ($vm['node'] ?? 'unknown');
|
||||||
|
if (!isset($seenVms[$uniqueId]) && checkVmPermission($pdo, $vm['vmid'])) {
|
||||||
|
$seenVms[$uniqueId] = true;
|
||||||
$vm['node_id'] = $node['id'];
|
$vm['node_id'] = $node['id'];
|
||||||
$vm['node_ip'] = $node['ip_address'];
|
$vm['node_ip'] = $node['ip_address'];
|
||||||
$vm['host'] = $vm['node'] ?? 'unknown';
|
$vm['host'] = $vm['node'] ?? 'unknown';
|
||||||
@@ -41,12 +91,14 @@ if ($action === 'get_top_vms') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($action === 'get_all_vms') {
|
if ($action === 'get_all_vms') {
|
||||||
$stmt = $pdo->query("SELECT * FROM nodes WHERE type = 'pve'"); $nodes = $stmt->fetchAll(PDO::FETCH_ASSOC); $allVms = [];
|
$stmt = $pdo->query("SELECT * FROM nodes WHERE type = 'pve'"); $nodes = $stmt->fetchAll(PDO::FETCH_ASSOC); $allVms = []; $seenVms = [];
|
||||||
foreach ($nodes as $node) {
|
foreach ($nodes as $node) {
|
||||||
$vms = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/cluster/resources?type=vm");
|
$vms = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/cluster/resources?type=vm");
|
||||||
if (isset($vms['data'])) {
|
if (isset($vms['data'])) {
|
||||||
foreach ($vms['data'] as $vm) {
|
foreach ($vms['data'] as $vm) {
|
||||||
if (checkVmPermission($pdo, $vm['vmid'])) {
|
$uniqueId = $vm['vmid'] . '@' . ($vm['node'] ?? 'unknown');
|
||||||
|
if (!isset($seenVms[$uniqueId]) && checkVmPermission($pdo, $vm['vmid'])) {
|
||||||
|
$seenVms[$uniqueId] = true;
|
||||||
$vm['node_id'] = $node['id'];
|
$vm['node_id'] = $node['id'];
|
||||||
$vm['node_ip'] = $node['ip_address'];
|
$vm['node_ip'] = $node['ip_address'];
|
||||||
$vm['host'] = $vm['node'] ?? 'unknown';
|
$vm['host'] = $vm['node'] ?? 'unknown';
|
||||||
@@ -68,14 +120,63 @@ if ($action === 'vm_action') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($action === 'get_nodes') { if (!$isAdmin) exit; $stmt = $pdo->query("SELECT id, name, ip_address, type FROM nodes"); echo json_encode(['success' => true, 'data' => $stmt->fetchAll(PDO::FETCH_ASSOC)]); exit; }
|
if ($action === 'get_nodes') { if (!$isAdmin) exit; $stmt = $pdo->query("SELECT id, name, ip_address, type FROM nodes"); echo json_encode(['success' => true, 'data' => $stmt->fetchAll(PDO::FETCH_ASSOC)]); exit; }
|
||||||
|
|
||||||
if ($action === 'delete_node') { if (!$isAdmin) exit; $stmt = $pdo->prepare("DELETE FROM nodes WHERE id = ?"); $stmt->execute([$_POST['id']]); logAudit('Server gelöscht', "Node ID: {$_POST['id']}"); echo json_encode(['success' => true]); exit; }
|
if ($action === 'delete_node') { if (!$isAdmin) exit; $stmt = $pdo->prepare("DELETE FROM nodes WHERE id = ?"); $stmt->execute([$_POST['id']]); logAudit('Server gelöscht', "Node ID: {$_POST['id']}"); echo json_encode(['success' => true]); exit; }
|
||||||
|
|
||||||
if ($action === 'add_node') {
|
if ($action === 'add_node') {
|
||||||
if (!$isAdmin) exit;
|
if (!$isAdmin) exit;
|
||||||
|
|
||||||
|
$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 = $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']);
|
$stmt->execute([$name, $ip, $tokenIdToSave, $tokenSecretToSave, $type]);
|
||||||
logAudit('Server hinzugefügt', "Node: {$_POST['name']} ({$_POST['ip']})");
|
logAudit('Server hinzugefügt', "Node: {$name} ({$ip})");
|
||||||
echo json_encode(['success' => true]); exit;
|
echo json_encode(['success' => true]); exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($action === 'get_users') { if (!$isAdmin) exit; $stmt = $pdo->query("SELECT id, username, role FROM users"); echo json_encode(['success' => true, 'data' => $stmt->fetchAll(PDO::FETCH_ASSOC)]); exit; }
|
if ($action === 'get_users') { if (!$isAdmin) exit; $stmt = $pdo->query("SELECT id, username, role FROM users"); echo json_encode(['success' => true, 'data' => $stmt->fetchAll(PDO::FETCH_ASSOC)]); exit; }
|
||||||
if ($action === 'delete_user') { if (!$isAdmin) exit; $stmt = $pdo->prepare("DELETE FROM users WHERE id = ?"); $stmt->execute([$_POST['id']]); logAudit('User gelöscht', "User ID: {$_POST['id']}"); echo json_encode(['success' => true]); exit; }
|
if ($action === 'delete_user') { if (!$isAdmin) exit; $stmt = $pdo->prepare("DELETE FROM users WHERE id = ?"); $stmt->execute([$_POST['id']]); logAudit('User gelöscht', "User ID: {$_POST['id']}"); echo json_encode(['success' => true]); exit; }
|
||||||
if ($action === 'create_user') {
|
if ($action === 'create_user') {
|
||||||
@@ -88,10 +189,17 @@ if ($action === 'create_user') {
|
|||||||
|
|
||||||
if ($action === 'get_pve_nodes') {
|
if ($action === 'get_pve_nodes') {
|
||||||
if (!$isAdmin) exit;
|
if (!$isAdmin) exit;
|
||||||
$stmt = $pdo->query("SELECT * FROM nodes WHERE type = 'pve'"); $resList = [];
|
$stmt = $pdo->query("SELECT * FROM nodes WHERE type = 'pve'"); $resList = []; $seenNodes = [];
|
||||||
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $node) {
|
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $node) {
|
||||||
$nData = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes");
|
$nData = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes");
|
||||||
if(isset($nData['data'])) { foreach($nData['data'] as $n) { if($n['status'] === 'online') { $resList[] = ['node_id' => $node['id'], 'host' => $n['node'], 'display' => $node['name'] . ' -> ' . $n['node']]; } } }
|
if(isset($nData['data'])) {
|
||||||
|
foreach($nData['data'] as $n) {
|
||||||
|
if($n['status'] === 'online' && !isset($seenNodes[$n['node']])) {
|
||||||
|
$seenNodes[$n['node']] = true;
|
||||||
|
$resList[] = ['node_id' => $node['id'], 'host' => $n['node'], 'display' => $node['name'] . ' -> ' . $n['node']];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
echo json_encode(['success' => true, 'data' => $resList]); exit;
|
echo json_encode(['success' => true, 'data' => $resList]); exit;
|
||||||
}
|
}
|
||||||
@@ -105,7 +213,7 @@ if ($action === 'create_vm') {
|
|||||||
$params = ['vmid' => $vmid, 'name' => $_POST['name'], 'memory' => $_POST['memory'], 'cores' => $_POST['cores'], 'net0' => 'virtio,bridge=vmbr0'];
|
$params = ['vmid' => $vmid, 'name' => $_POST['name'], 'memory' => $_POST['memory'], 'cores' => $_POST['cores'], 'net0' => 'virtio,bridge=vmbr0'];
|
||||||
$res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$_POST['host']}/qemu", 'pve', 'POST', $params);
|
$res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$_POST['host']}/qemu", 'pve', 'POST', $params);
|
||||||
logAudit('VM Erstellt', "VMID: {$vmid} Name: {$_POST['name']}");
|
logAudit('VM Erstellt', "VMID: {$vmid} Name: {$_POST['name']}");
|
||||||
echo json_encode(['success' => isset($res['data']), 'vmid' => $vmid, 'error' => isset($res['data']) ? '' : 'Fehler bei der VM Erstellung in Proxmox.']); exit;
|
echo json_encode(['success' => isset($res['data']), 'vmid' => $vmid, 'error' => isset($res['data']) ? '' : 'Fehler bei Erstellung.']); exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($action === 'get_vm_config') {
|
if ($action === 'get_vm_config') {
|
||||||
@@ -208,7 +316,7 @@ if ($action === 'restore_backup') {
|
|||||||
if (!checkVmPermission($pdo, $_POST['vmid'])) exit;
|
if (!checkVmPermission($pdo, $_POST['vmid'])) exit;
|
||||||
$stmt = $pdo->prepare("SELECT * FROM nodes WHERE id = ?"); $stmt->execute([$_POST['node_id']]); $node = $stmt->fetch(PDO::FETCH_ASSOC);
|
$stmt = $pdo->prepare("SELECT * FROM nodes WHERE id = ?"); $stmt->execute([$_POST['node_id']]); $node = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
$res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$_POST['host']}/{$_POST['type']}/{$_POST['vmid']}/status/current");
|
$res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$_POST['host']}/{$_POST['type']}/{$_POST['vmid']}/status/current");
|
||||||
if (isset($res['data']) && $res['data']['status'] === 'running') { echo json_encode(['success' => false, 'error' => 'VM muss gestoppt sein!']); exit; }
|
if (isset($res['data']) && $res['data']['status'] === 'running') { echo json_encode(['success' => false, 'error' => 'VM gestoppt?']); exit; }
|
||||||
$resRestore = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$_POST['host']}/{$_POST['type']}", 'pve', 'POST', ['vmid' => $_POST['vmid'], 'archive' => $_POST['archive'], 'force' => 1]);
|
$resRestore = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$_POST['host']}/{$_POST['type']}", 'pve', 'POST', ['vmid' => $_POST['vmid'], 'archive' => $_POST['archive'], 'force' => 1]);
|
||||||
logAudit('Backup Restore', "VMID: {$_POST['vmid']} Archive: {$_POST['archive']}");
|
logAudit('Backup Restore', "VMID: {$_POST['vmid']} Archive: {$_POST['archive']}");
|
||||||
echo json_encode(['success' => isset($resRestore['data']), 'error' => 'API Fehler']); exit;
|
echo json_encode(['success' => isset($resRestore['data']), 'error' => 'API Fehler']); exit;
|
||||||
@@ -220,12 +328,31 @@ if ($action === 'get_node_status' || $action === 'get_vm_status') {
|
|||||||
if ($action === 'get_node_status') {
|
if ($action === 'get_node_status') {
|
||||||
$nData = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes");
|
$nData = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes");
|
||||||
$internalName = $nData['data'][0]['node'] ?? 'pve';
|
$internalName = $nData['data'][0]['node'] ?? 'pve';
|
||||||
$endpoint = "/api2/json/nodes/{$internalName}/status";
|
|
||||||
|
$res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$internalName}/status");
|
||||||
|
$rrd = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$internalName}/rrddata?timeframe=hour&cf=AVERAGE");
|
||||||
|
|
||||||
|
$netin = 0; $netout = 0;
|
||||||
|
if (isset($rrd['data']) && is_array($rrd['data'])) {
|
||||||
|
for ($i = count($rrd['data']) - 1; $i >= 0; $i--) {
|
||||||
|
if (isset($rrd['data'][$i]['netin']) && $rrd['data'][$i]['netin'] !== null) {
|
||||||
|
$netin = $rrd['data'][$i]['netin'];
|
||||||
|
$netout = $rrd['data'][$i]['netout'];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = $res['data'] ?? [];
|
||||||
|
$data['netin'] = $netin;
|
||||||
|
$data['netout'] = $netout;
|
||||||
|
$data['is_rrd_net'] = true;
|
||||||
|
|
||||||
|
echo json_encode(['success' => true, 'data' => $data]); exit;
|
||||||
} else {
|
} else {
|
||||||
$endpoint = "/api2/json/nodes/{$_GET['host']}/{$_GET['type']}/{$_GET['vmid']}/status/current";
|
$endpoint = "/api2/json/nodes/{$_GET['host']}/{$_GET['type']}/{$_GET['vmid']}/status/current";
|
||||||
|
$res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], $endpoint);
|
||||||
|
echo json_encode(['success' => true, 'data' => $res['data'] ?? []]); exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
$res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], $endpoint);
|
|
||||||
echo json_encode(['success' => true, 'data' => $res['data'] ?? []]); exit;
|
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
+97
-8
@@ -18,7 +18,6 @@ async function logout() {
|
|||||||
|
|
||||||
if (window.APP.isLoggedIn && window.APP.nodeCount > 0) {
|
if (window.APP.isLoggedIn && window.APP.nodeCount > 0) {
|
||||||
|
|
||||||
// Inject Audit Log Button automatically into Sidebar for Admins
|
|
||||||
if (window.APP.username === 'admin' || document.querySelector('a[onclick="openUserManager()"]')) {
|
if (window.APP.username === 'admin' || document.querySelector('a[onclick="openUserManager()"]')) {
|
||||||
const userBtn = document.querySelector('a[onclick="openUserManager()"]');
|
const userBtn = document.querySelector('a[onclick="openUserManager()"]');
|
||||||
if (userBtn && !document.getElementById('btnAuditLog')) {
|
if (userBtn && !document.getElementById('btnAuditLog')) {
|
||||||
@@ -27,7 +26,6 @@ if (window.APP.isLoggedIn && window.APP.nodeCount > 0) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// === NEU: PASSWORT ÄNDERN LOGIK ===
|
|
||||||
const pwdModal = document.getElementById('passwordModal');
|
const pwdModal = document.getElementById('passwordModal');
|
||||||
window.openPasswordModal = function() { if(pwdModal) { pwdModal.classList.remove('hidden'); document.getElementById('changePasswordForm').reset(); } }
|
window.openPasswordModal = function() { if(pwdModal) { pwdModal.classList.remove('hidden'); document.getElementById('changePasswordForm').reset(); } }
|
||||||
window.closePasswordModal = function() { if(pwdModal) pwdModal.classList.add('hidden'); }
|
window.closePasswordModal = function() { if(pwdModal) pwdModal.classList.add('hidden'); }
|
||||||
@@ -63,7 +61,84 @@ if (window.APP.isLoggedIn && window.APP.nodeCount > 0) {
|
|||||||
const ctx = document.getElementById('liveChart')?.getContext('2d'); let liveChart;
|
const ctx = document.getElementById('liveChart')?.getContext('2d'); let liveChart;
|
||||||
if(ctx) { liveChart = new Chart(ctx, { type: 'line', data: { labels: [], datasets: [{ label: 'CPU (%)', borderColor: '#3b82f6', backgroundColor: 'rgba(59, 130, 246, 0.1)', borderWidth: 2, tension: 0.4, fill: true, data: [] }, { label: 'RAM (%)', borderColor: '#E57000', backgroundColor: 'rgba(229, 112, 0, 0.1)', borderWidth: 2, tension: 0.4, fill: true, data: [] }] }, options: { responsive: true, maintainAspectRatio: false, animation: { duration: 500 }, scales: { x: { ticks: { color: '#9ca3af' }, grid: { color: '#33334d' } }, y: { min: 0, max: 100, ticks: { color: '#9ca3af', callback: v => v + '%' }, grid: { color: '#33334d' } } }, plugins: { legend: { labels: { color: '#e2e8f0', usePointStyle: true } } } } }); }
|
if(ctx) { liveChart = new Chart(ctx, { type: 'line', data: { labels: [], datasets: [{ label: 'CPU (%)', borderColor: '#3b82f6', backgroundColor: 'rgba(59, 130, 246, 0.1)', borderWidth: 2, tension: 0.4, fill: true, data: [] }, { label: 'RAM (%)', borderColor: '#E57000', backgroundColor: 'rgba(229, 112, 0, 0.1)', borderWidth: 2, tension: 0.4, fill: true, data: [] }] }, options: { responsive: true, maintainAspectRatio: false, animation: { duration: 500 }, scales: { x: { ticks: { color: '#9ca3af' }, grid: { color: '#33334d' } }, y: { min: 0, max: 100, ticks: { color: '#9ca3af', callback: v => v + '%' }, grid: { color: '#33334d' } } }, plugins: { legend: { labels: { color: '#e2e8f0', usePointStyle: true } } } } }); }
|
||||||
|
|
||||||
async function fetchGlobalStats() { if(!document.getElementById('stat-cpu-text')) return; try { const res = await (await fetch('api.php?action=get_stats')).json(); if(res.success && res.data) { const d = res.data; document.getElementById('stat-cpu-text').innerText = `${d.cpu_percent}% (${d.cpu_cores} Cores)`; document.getElementById('stat-cpu-bar').style.width = `${d.cpu_percent}%`; let ramPercent = (d.ram_used / d.ram_total) * 100 || 0; document.getElementById('stat-ram-text').innerText = `${formatBytes(d.ram_used)} / ${formatBytes(d.ram_total)}`; document.getElementById('stat-ram-bar').style.width = `${ramPercent}%`; let diskPercent = (d.disk_used / d.disk_total) * 100 || 0; document.getElementById('stat-disk-text').innerText = `${formatBytes(d.disk_used)} / ${formatBytes(d.disk_total)}`; document.getElementById('stat-disk-bar').style.width = `${diskPercent}%`; if(liveChart) { const now = new Date(); const timeStr = now.getHours().toString().padStart(2, '0') + ':' + now.getMinutes().toString().padStart(2, '0') + ':' + now.getSeconds().toString().padStart(2, '0'); liveChart.data.labels.push(timeStr); liveChart.data.datasets[0].data.push(d.cpu_percent); liveChart.data.datasets[1].data.push(ramPercent.toFixed(1)); if (liveChart.data.labels.length > 15) { liveChart.data.labels.shift(); liveChart.data.datasets[0].data.shift(); liveChart.data.datasets[1].data.shift(); } liveChart.update(); } } } catch (err) {} }
|
const ctxNet = document.getElementById('liveNetChart')?.getContext('2d'); let liveNetChart;
|
||||||
|
if(ctxNet) { liveNetChart = new Chart(ctxNet, { type: 'line', data: { labels: [], datasets: [] }, options: { responsive: true, maintainAspectRatio: false, animation: { duration: 500 }, scales: { x: { ticks: { color: '#9ca3af' }, grid: { color: '#33334d' } }, y: { min: 0, ticks: { color: '#9ca3af' }, grid: { color: '#33334d' } } }, plugins: { legend: { labels: { color: '#e2e8f0', usePointStyle: true } } } } }); }
|
||||||
|
|
||||||
|
let prevGlobalTime = null;
|
||||||
|
|
||||||
|
async function fetchGlobalStats() {
|
||||||
|
if(!document.getElementById('stat-cpu-text')) return;
|
||||||
|
try {
|
||||||
|
const res = await (await fetch('api.php?action=get_stats')).json();
|
||||||
|
if(res.success && res.data) {
|
||||||
|
const d = res.data;
|
||||||
|
document.getElementById('stat-cpu-text').innerText = `${d.cpu_percent}% (${d.cpu_cores} Cores)`;
|
||||||
|
document.getElementById('stat-cpu-bar').style.width = `${d.cpu_percent}%`;
|
||||||
|
let ramPercent = (d.ram_used / d.ram_total) * 100 || 0;
|
||||||
|
document.getElementById('stat-ram-text').innerText = `${formatBytes(d.ram_used)} / ${formatBytes(d.ram_total)}`;
|
||||||
|
document.getElementById('stat-ram-bar').style.width = `${ramPercent}%`;
|
||||||
|
let diskPercent = (d.disk_used / d.disk_total) * 100 || 0;
|
||||||
|
document.getElementById('stat-disk-text').innerText = `${formatBytes(d.disk_used)} / ${formatBytes(d.disk_total)}`;
|
||||||
|
document.getElementById('stat-disk-bar').style.width = `${diskPercent}%`;
|
||||||
|
|
||||||
|
// Neue Übersichtskachel füttern
|
||||||
|
if (d.cluster_stats) {
|
||||||
|
const elNodesOn = document.getElementById('stat-nodes-online');
|
||||||
|
if (elNodesOn) {
|
||||||
|
elNodesOn.innerText = d.cluster_stats.nodes_online;
|
||||||
|
if (d.cluster_stats.nodes_online < d.cluster_stats.nodes_total) {
|
||||||
|
elNodesOn.className = 'text-red-500';
|
||||||
|
} else {
|
||||||
|
elNodesOn.className = 'text-green-500';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(document.getElementById('stat-nodes-total')) document.getElementById('stat-nodes-total').innerText = d.cluster_stats.nodes_total;
|
||||||
|
if(document.getElementById('stat-vms-total')) document.getElementById('stat-vms-total').innerText = d.cluster_stats.vms_total;
|
||||||
|
if(document.getElementById('stat-vms-run')) document.getElementById('stat-vms-run').innerText = d.cluster_stats.vms_running;
|
||||||
|
if(document.getElementById('stat-vms-stop')) document.getElementById('stat-vms-stop').innerText = d.cluster_stats.vms_stopped;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(liveChart) {
|
||||||
|
const now = new Date();
|
||||||
|
const timeStr = now.getHours().toString().padStart(2, '0') + ':' + now.getMinutes().toString().padStart(2, '0') + ':' + now.getSeconds().toString().padStart(2, '0');
|
||||||
|
liveChart.data.labels.push(timeStr);
|
||||||
|
liveChart.data.datasets[0].data.push(d.cpu_percent);
|
||||||
|
liveChart.data.datasets[1].data.push(ramPercent.toFixed(1));
|
||||||
|
if (liveChart.data.labels.length > 15) {
|
||||||
|
liveChart.data.labels.shift(); liveChart.data.datasets[0].data.shift(); liveChart.data.datasets[1].data.shift();
|
||||||
|
}
|
||||||
|
liveChart.update();
|
||||||
|
|
||||||
|
if(liveNetChart && d.nodes_net) {
|
||||||
|
const nowTs = Date.now();
|
||||||
|
if (prevGlobalTime !== null) {
|
||||||
|
liveNetChart.data.labels.push(timeStr);
|
||||||
|
if (liveNetChart.data.labels.length > 15) liveNetChart.data.labels.shift();
|
||||||
|
const colors = ['#10b981', '#8b5cf6', '#f59e0b', '#ef4444', '#06b6d4'];
|
||||||
|
|
||||||
|
d.nodes_net.forEach((n, idx) => {
|
||||||
|
let rxSpeed = n.netin / (1024 * 1024);
|
||||||
|
let txSpeed = n.netout / (1024 * 1024);
|
||||||
|
let totalSpeed = (rxSpeed + txSpeed).toFixed(2);
|
||||||
|
|
||||||
|
let ds = liveNetChart.data.datasets.find(ds => ds.label === n.name);
|
||||||
|
if (!ds) {
|
||||||
|
const c = colors[idx % colors.length];
|
||||||
|
ds = { label: n.name, borderColor: c, backgroundColor: c + '1a', borderWidth: 2, tension: 0.4, fill: true, data: new Array(Math.max(0, liveNetChart.data.labels.length - 1)).fill(0) };
|
||||||
|
liveNetChart.data.datasets.push(ds);
|
||||||
|
}
|
||||||
|
ds.data.push(totalSpeed);
|
||||||
|
if (ds.data.length > 15) ds.data.shift();
|
||||||
|
});
|
||||||
|
liveNetChart.update();
|
||||||
|
}
|
||||||
|
prevGlobalTime = nowTs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {}
|
||||||
|
}
|
||||||
|
|
||||||
async function fetchTopVms() { if(!document.getElementById('top-vms-container')) return; try { const res = await (await fetch('api.php?action=get_top_vms')).json(); if(res.success && res.data) { const container = document.getElementById('top-vms-container'); container.innerHTML = ''; if(res.data.length === 0) { container.innerHTML = '<p class="text-gray-400 text-sm">Keine aktiven VMs.</p>'; return; } res.data.forEach((vm, i) => { const cpuPercent = ((vm.cpu || 0) * 100).toFixed(1); const ramUsed = formatBytes(vm.mem || 0); const icon = vm.type === 'lxc' ? '📦' : '🖥️'; const numberColor = i === 0 ? 'text-red-500' : (i === 1 ? 'text-orange-400' : (i === 2 ? 'text-yellow-400' : 'text-gray-400')); container.innerHTML += `<div class="bg-darkbg border border-darkborder rounded-lg p-3 flex justify-between items-center transition-transform hover:scale-[1.02] cursor-default"><div class="flex items-center gap-3"><span class="font-bold text-xl ${numberColor}">#${i + 1}</span><div><h4 class="text-white font-semibold text-sm truncate w-32">${icon} ${vm.name}</h4><p class="text-xs text-gray-500">Host: ${vm.host}</p></div></div><div class="text-right"><p class="text-proxmox font-bold text-sm">${cpuPercent}% CPU</p><p class="text-xs text-gray-400">${ramUsed} RAM</p></div></div>`; }); } } catch (err) {} }
|
async function fetchTopVms() { if(!document.getElementById('top-vms-container')) return; try { const res = await (await fetch('api.php?action=get_top_vms')).json(); if(res.success && res.data) { const container = document.getElementById('top-vms-container'); container.innerHTML = ''; if(res.data.length === 0) { container.innerHTML = '<p class="text-gray-400 text-sm">Keine aktiven VMs.</p>'; return; } res.data.forEach((vm, i) => { const cpuPercent = ((vm.cpu || 0) * 100).toFixed(1); const ramUsed = formatBytes(vm.mem || 0); const icon = vm.type === 'lxc' ? '📦' : '🖥️'; const numberColor = i === 0 ? 'text-red-500' : (i === 1 ? 'text-orange-400' : (i === 2 ? 'text-yellow-400' : 'text-gray-400')); container.innerHTML += `<div class="bg-darkbg border border-darkborder rounded-lg p-3 flex justify-between items-center transition-transform hover:scale-[1.02] cursor-default"><div class="flex items-center gap-3"><span class="font-bold text-xl ${numberColor}">#${i + 1}</span><div><h4 class="text-white font-semibold text-sm truncate w-32">${icon} ${vm.name}</h4><p class="text-xs text-gray-500">Host: ${vm.host}</p></div></div><div class="text-right"><p class="text-proxmox font-bold text-sm">${cpuPercent}% CPU</p><p class="text-xs text-gray-400">${ramUsed} RAM</p></div></div>`; }); } } catch (err) {} }
|
||||||
async function fetchRecentJobs() { if(!document.getElementById('recent-jobs-container')) return; try { const res = await (await fetch('api.php?action=get_recent_jobs')).json(); if(res.success && res.data) { const container = document.getElementById('recent-jobs-container'); container.innerHTML = ''; if(res.data.length === 0) { container.innerHTML = '<p class="text-gray-400 text-sm">Keine aktuellen Jobs.</p>'; return; } res.data.forEach(job => { const jobTypeStr = job.type || job.worker_type || 'unknown'; let statusColor = 'text-gray-400', statusIcon = '⏳', statusText = job.status || 'running...'; if(statusText.toLowerCase() === 'ok') { statusColor = 'text-green-500'; statusIcon = '✅'; } else if(statusText !== 'running...') { statusColor = 'text-red-500'; statusIcon = '❌'; } else { statusColor = 'text-blue-400'; statusIcon = '🔄'; } const date = new Date(job.starttime * 1000); const timeStr = date.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' }), dateStr = date.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' }); const isBackup = jobTypeStr.includes('sync') || jobTypeStr.includes('prune') || jobTypeStr.includes('garbage_collection') || jobTypeStr.includes('vzdump') || jobTypeStr.includes('verify'); const jobTypeColor = isBackup ? 'text-purple-400' : 'text-white'; container.innerHTML += `<div class="bg-darkbg border border-darkborder rounded-lg p-3 flex justify-between items-center transition-colors hover:bg-darkborder/50"><div class="flex items-center gap-3"><div class="text-lg">${statusIcon}</div><div class="max-w-[120px]"><p class="${jobTypeColor} font-medium text-sm capitalize truncate" title="${jobTypeStr}">${jobTypeStr}</p><p class="text-xs text-gray-500 truncate" title="${job.node_name}">Host: <span class="text-proxmox">${job.node_name}</span></p></div></div><div class="text-right"><p class="${statusColor} font-bold text-sm uppercase">${statusText}</p><p class="text-xs text-gray-500">${dateStr} - ${timeStr}</p></div></div>`; }); } } catch (err) { console.error(err); } }
|
async function fetchRecentJobs() { if(!document.getElementById('recent-jobs-container')) return; try { const res = await (await fetch('api.php?action=get_recent_jobs')).json(); if(res.success && res.data) { const container = document.getElementById('recent-jobs-container'); container.innerHTML = ''; if(res.data.length === 0) { container.innerHTML = '<p class="text-gray-400 text-sm">Keine aktuellen Jobs.</p>'; return; } res.data.forEach(job => { const jobTypeStr = job.type || job.worker_type || 'unknown'; let statusColor = 'text-gray-400', statusIcon = '⏳', statusText = job.status || 'running...'; if(statusText.toLowerCase() === 'ok') { statusColor = 'text-green-500'; statusIcon = '✅'; } else if(statusText !== 'running...') { statusColor = 'text-red-500'; statusIcon = '❌'; } else { statusColor = 'text-blue-400'; statusIcon = '🔄'; } const date = new Date(job.starttime * 1000); const timeStr = date.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' }), dateStr = date.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' }); const isBackup = jobTypeStr.includes('sync') || jobTypeStr.includes('prune') || jobTypeStr.includes('garbage_collection') || jobTypeStr.includes('vzdump') || jobTypeStr.includes('verify'); const jobTypeColor = isBackup ? 'text-purple-400' : 'text-white'; container.innerHTML += `<div class="bg-darkbg border border-darkborder rounded-lg p-3 flex justify-between items-center transition-colors hover:bg-darkborder/50"><div class="flex items-center gap-3"><div class="text-lg">${statusIcon}</div><div class="max-w-[120px]"><p class="${jobTypeColor} font-medium text-sm capitalize truncate" title="${jobTypeStr}">${jobTypeStr}</p><p class="text-xs text-gray-500 truncate" title="${job.node_name}">Host: <span class="text-proxmox">${job.node_name}</span></p></div></div><div class="text-right"><p class="${statusColor} font-bold text-sm uppercase">${statusText}</p><p class="text-xs text-gray-500">${dateStr} - ${timeStr}</p></div></div>`; }); } } catch (err) { console.error(err); } }
|
||||||
async function fetchUpdates() { if(!document.getElementById('stat-updates-text')) return; try { const res = await (await fetch('api.php?action=get_updates')).json(); if(res.success) { const el = document.getElementById('stat-updates-text'), subEl = document.getElementById('stat-updates-sub'); if(res.total === 0) { el.innerText = '0 Updates'; el.className = 'text-2xl font-bold text-green-500 mt-1'; subEl.innerText = 'Alle Systeme sind aktuell.'; } else { el.innerText = res.total + ' Updates'; el.className = 'text-2xl font-bold text-red-500 mt-1'; subEl.innerText = 'Auf: ' + res.details; } } } catch (err) {} }
|
async function fetchUpdates() { if(!document.getElementById('stat-updates-text')) return; try { const res = await (await fetch('api.php?action=get_updates')).json(); if(res.success) { const el = document.getElementById('stat-updates-text'), subEl = document.getElementById('stat-updates-sub'); if(res.total === 0) { el.innerText = '0 Updates'; el.className = 'text-2xl font-bold text-green-500 mt-1'; subEl.innerText = 'Alle Systeme sind aktuell.'; } else { el.innerText = res.total + ' Updates'; el.className = 'text-2xl font-bold text-red-500 mt-1'; subEl.innerText = 'Auf: ' + res.details; } } } catch (err) {} }
|
||||||
@@ -227,12 +302,28 @@ if (window.APP.isLoggedIn && window.APP.nodeCount > 0) {
|
|||||||
const d = res.data; const now = Date.now();
|
const d = res.data; const now = Date.now();
|
||||||
let cpuRaw = 0; if (d.cpu !== undefined) cpuRaw = d.cpu; else if (d.cpuinfo && d.cpuinfo.cpus) cpuRaw = 0; const cpu = (cpuRaw * 100).toFixed(1);
|
let cpuRaw = 0; if (d.cpu !== undefined) cpuRaw = d.cpu; else if (d.cpuinfo && d.cpuinfo.cpus) cpuRaw = 0; const cpu = (cpuRaw * 100).toFixed(1);
|
||||||
let ram = 0; if (d.maxmem && d.maxmem > 0) { ram = ((d.mem / d.maxmem) * 100).toFixed(1); } else if (d.memory && d.memory.total > 0) { ram = ((d.memory.used / d.memory.total) * 100).toFixed(1); }
|
let ram = 0; if (d.maxmem && d.maxmem > 0) { ram = ((d.mem / d.maxmem) * 100).toFixed(1); } else if (d.memory && d.memory.total > 0) { ram = ((d.memory.used / d.memory.total) * 100).toFixed(1); }
|
||||||
let currentNetIn = d.netin || 0; let currentNetOut = d.netout || 0; let rxSpeed = 0; let txSpeed = 0;
|
|
||||||
if(prevTime !== null) { const timeSec = (now - prevTime) / 1000; if (timeSec > 0) { rxSpeed = Math.max(0, ((currentNetIn - prevNetIn) / timeSec / (1024 * 1024))).toFixed(2); txSpeed = Math.max(0, ((currentNetOut - prevNetOut) / timeSec / (1024 * 1024))).toFixed(2); } }
|
let currentNetIn = d.netin || 0; let currentNetOut = d.netout || 0;
|
||||||
|
let rxSpeed = 0; let txSpeed = 0;
|
||||||
|
|
||||||
|
if (d.is_rrd_net) {
|
||||||
|
rxSpeed = (currentNetIn / (1024 * 1024)).toFixed(2);
|
||||||
|
txSpeed = (currentNetOut / (1024 * 1024)).toFixed(2);
|
||||||
|
} else {
|
||||||
|
if(prevTime !== null) {
|
||||||
|
const timeSec = (now - prevTime) / 1000;
|
||||||
|
if (timeSec > 0) {
|
||||||
|
rxSpeed = Math.max(0, ((currentNetIn - prevNetIn) / timeSec / (1024 * 1024))).toFixed(2);
|
||||||
|
txSpeed = Math.max(0, ((currentNetOut - prevNetOut) / timeSec / (1024 * 1024))).toFixed(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
prevNetIn = currentNetIn; prevNetOut = currentNetOut; prevTime = now;
|
prevNetIn = currentNetIn; prevNetOut = currentNetOut; prevTime = now;
|
||||||
const timeStr = new Date().toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
const timeStr = new Date().toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
||||||
|
|
||||||
perfChartObj.data.labels.push(timeStr); perfChartObj.data.datasets[0].data.push(cpu); perfChartObj.data.datasets[1].data.push(ram); if(perfChartObj.data.labels.length > 30) { perfChartObj.data.labels.shift(); perfChartObj.data.datasets[0].data.shift(); perfChartObj.data.datasets[1].data.shift(); } perfChartObj.update();
|
perfChartObj.data.labels.push(timeStr); perfChartObj.data.datasets[0].data.push(cpu); perfChartObj.data.datasets[1].data.push(ram); if(perfChartObj.data.labels.length > 30) { perfChartObj.data.labels.shift(); perfChartObj.data.datasets[0].data.shift(); perfChartObj.data.datasets[1].data.shift(); } perfChartObj.update();
|
||||||
if(prevTime !== null) { netChartObj.data.labels.push(timeStr); netChartObj.data.datasets[0].data.push(rxSpeed); netChartObj.data.datasets[1].data.push(txSpeed); if(netChartObj.data.labels.length > 30) { netChartObj.data.labels.shift(); netChartObj.data.datasets[0].data.shift(); netChartObj.data.datasets[1].data.shift(); } netChartObj.update(); }
|
if(prevTime !== null || d.is_rrd_net) { netChartObj.data.labels.push(timeStr); netChartObj.data.datasets[0].data.push(rxSpeed); netChartObj.data.datasets[1].data.push(txSpeed); if(netChartObj.data.labels.length > 30) { netChartObj.data.labels.shift(); netChartObj.data.datasets[0].data.shift(); netChartObj.data.datasets[1].data.shift(); } netChartObj.update(); }
|
||||||
}
|
}
|
||||||
} catch(e) {}
|
} catch(e) {}
|
||||||
};
|
};
|
||||||
@@ -519,7 +610,6 @@ if (window.APP.isLoggedIn && window.APP.nodeCount > 0) {
|
|||||||
try { const res = await (await fetch('api.php?action=pmg_upload_ssl', {method: 'POST', body: fd})).json(); if(res.success) { alert('Zertifikat hochgeladen! Dienste werden neu gestartet.'); document.getElementById('pmgSslCert').value = ''; document.getElementById('pmgSslKey').value = ''; loadPmgSsl(document.getElementById('pmgNodeId').value); } else { alert('Fehler beim Upload. (Format prüfen)'); } } catch(e) { alert('Netzwerkfehler.'); } finally { btn.innerText = oTxt; }
|
try { const res = await (await fetch('api.php?action=pmg_upload_ssl', {method: 'POST', body: fd})).json(); if(res.success) { alert('Zertifikat hochgeladen! Dienste werden neu gestartet.'); document.getElementById('pmgSslCert').value = ''; document.getElementById('pmgSslKey').value = ''; loadPmgSsl(document.getElementById('pmgNodeId').value); } else { alert('Fehler beim Upload. (Format prüfen)'); } } catch(e) { alert('Netzwerkfehler.'); } finally { btn.innerText = oTxt; }
|
||||||
}
|
}
|
||||||
|
|
||||||
// === SCHEDULER ===
|
|
||||||
const cronModal = document.getElementById('cronManagerModal');
|
const cronModal = document.getElementById('cronManagerModal');
|
||||||
window.openCronManager = async function() {
|
window.openCronManager = async function() {
|
||||||
cronModal.classList.remove('hidden');
|
cronModal.classList.remove('hidden');
|
||||||
@@ -568,7 +658,6 @@ if (window.APP.isLoggedIn && window.APP.nodeCount > 0) {
|
|||||||
window.toggleCronJob = async function(id, newState) { const fd = new FormData(); fd.append('id', id); fd.append('is_active', newState); await fetch('api.php?action=toggle_cron_job', {method: 'POST', body: fd}); loadCronJobs(); }
|
window.toggleCronJob = async function(id, newState) { const fd = new FormData(); fd.append('id', id); fd.append('is_active', newState); await fetch('api.php?action=toggle_cron_job', {method: 'POST', body: fd}); loadCronJobs(); }
|
||||||
window.deleteCronJob = async function(id) { if(!confirm('Diesen geplanten Job wirklich löschen?')) return; const fd = new FormData(); fd.append('id', id); await fetch('api.php?action=delete_cron_job', {method: 'POST', body: fd}); loadCronJobs(); }
|
window.deleteCronJob = async function(id) { if(!confirm('Diesen geplanten Job wirklich löschen?')) return; const fd = new FormData(); fd.append('id', id); await fetch('api.php?action=delete_cron_job', {method: 'POST', body: fd}); loadCronJobs(); }
|
||||||
|
|
||||||
// === AUDIT LOG ===
|
|
||||||
const auditModal = document.getElementById('auditLogModal');
|
const auditModal = document.getElementById('auditLogModal');
|
||||||
window.openAuditLog = async function() {
|
window.openAuditLog = async function() {
|
||||||
auditModal.classList.remove('hidden');
|
auditModal.classList.remove('hidden');
|
||||||
|
|||||||
+3
-9
@@ -2,7 +2,6 @@
|
|||||||
// /home/docker/pve_dashboard/src/cron.php
|
// /home/docker/pve_dashboard/src/cron.php
|
||||||
require_once __DIR__ . '/db.php';
|
require_once __DIR__ . '/db.php';
|
||||||
|
|
||||||
// Hilfsfunktion: Prüft, ob ein Cron-Ausdruck (z.B. "0 3 * * *") zur aktuellen Zeit passt
|
|
||||||
function isCronMatch($cron, $time = null) {
|
function isCronMatch($cron, $time = null) {
|
||||||
if ($time === null) $time = time();
|
if ($time === null) $time = time();
|
||||||
$cronParts = explode(' ', trim($cron));
|
$cronParts = explode(' ', trim($cron));
|
||||||
@@ -20,14 +19,14 @@ function isCronMatch($cron, $time = null) {
|
|||||||
function matchCronPart($part, $current) {
|
function matchCronPart($part, $current) {
|
||||||
if ($part === '*') return true;
|
if ($part === '*') return true;
|
||||||
if ($part === (string)(int)$current) return true;
|
if ($part === (string)(int)$current) return true;
|
||||||
if (strpos($part, '*/') === 0) { // Unterstützt z.B. */5 für "alle 5 Minuten"
|
if (strpos($part, '*/') === 0) {
|
||||||
$step = (int)substr($part, 2);
|
$step = (int)substr($part, 2);
|
||||||
return $step > 0 && ((int)$current % $step) === 0;
|
return $step > 0 && ((int)$current % $step) === 0;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mini-Proxmox-Fetcher für das Cron-Skript
|
// Mini-Proxmox-Fetcher für das Cron-Skript (Mit Token-Auth für PVE!)
|
||||||
function cronProxmoxData($ip, $tokenId, $tokenSecret, $endpoint, $method = "POST", $postData = null) {
|
function cronProxmoxData($ip, $tokenId, $tokenSecret, $endpoint, $method = "POST", $postData = null) {
|
||||||
$headers = ["Authorization: PVEAPIToken={$tokenId}={$tokenSecret}"];
|
$headers = ["Authorization: PVEAPIToken={$tokenId}={$tokenSecret}"];
|
||||||
$ch = curl_init("https://{$ip}:8006{$endpoint}");
|
$ch = curl_init("https://{$ip}:8006{$endpoint}");
|
||||||
@@ -39,12 +38,11 @@ function cronProxmoxData($ip, $tokenId, $tokenSecret, $endpoint, $method = "POST
|
|||||||
if ($postData) $options[CURLOPT_POSTFIELDS] = http_build_query($postData);
|
if ($postData) $options[CURLOPT_POSTFIELDS] = http_build_query($postData);
|
||||||
curl_setopt_array($ch, $options);
|
curl_setopt_array($ch, $options);
|
||||||
$res = curl_exec($ch); curl_close($ch);
|
$res = curl_exec($ch); curl_close($ch);
|
||||||
return json_decode($res, true);
|
return json_decode($res, true) ?: [];
|
||||||
}
|
}
|
||||||
|
|
||||||
echo "[" . date('Y-m-d H:i:s') . "] Starte Scheduler-Check...\n";
|
echo "[" . date('Y-m-d H:i:s') . "] Starte Scheduler-Check...\n";
|
||||||
|
|
||||||
// Hole alle aktiven Tasks
|
|
||||||
$stmt = $pdo->query("SELECT * FROM scheduled_tasks WHERE is_active = 1");
|
$stmt = $pdo->query("SELECT * FROM scheduled_tasks WHERE is_active = 1");
|
||||||
$tasks = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
$tasks = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
@@ -59,7 +57,6 @@ foreach ($tasks as $task) {
|
|||||||
if ($node) {
|
if ($node) {
|
||||||
$success = false;
|
$success = false;
|
||||||
|
|
||||||
// AKTION: PVE NODE NEUSTART
|
|
||||||
if ($task['action_type'] === 'reboot_node') {
|
if ($task['action_type'] === 'reboot_node') {
|
||||||
$nData = cronProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes", 'GET');
|
$nData = cronProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes", 'GET');
|
||||||
if (isset($nData['data'][0]['node'])) {
|
if (isset($nData['data'][0]['node'])) {
|
||||||
@@ -68,9 +65,7 @@ foreach ($tasks as $task) {
|
|||||||
$success = true;
|
$success = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// AKTION: VM START / STOP / REBOOT
|
|
||||||
elseif (in_array($task['action_type'], ['start_vm', 'stop_vm', 'reboot_vm'])) {
|
elseif (in_array($task['action_type'], ['start_vm', 'stop_vm', 'reboot_vm'])) {
|
||||||
// Suche Host und Typ der VM
|
|
||||||
$vmsRes = cronProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/cluster/resources?type=vm", 'GET');
|
$vmsRes = cronProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/cluster/resources?type=vm", 'GET');
|
||||||
if (isset($vmsRes['data'])) {
|
if (isset($vmsRes['data'])) {
|
||||||
foreach ($vmsRes['data'] as $vm) {
|
foreach ($vmsRes['data'] as $vm) {
|
||||||
@@ -85,7 +80,6 @@ foreach ($tasks as $task) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setze den Zeitstempel für den letzten Durchlauf
|
|
||||||
if ($success) {
|
if ($success) {
|
||||||
$uStmt = $pdo->prepare("UPDATE scheduled_tasks SET last_run = ? WHERE id = ?");
|
$uStmt = $pdo->prepare("UPDATE scheduled_tasks SET last_run = ? WHERE id = ?");
|
||||||
$uStmt->execute([time(), $task['id']]);
|
$uStmt->execute([time(), $task['id']]);
|
||||||
|
|||||||
+47
-6
@@ -38,11 +38,9 @@ $nodeCount = $stmt->fetchColumn();
|
|||||||
<?php if ($isLoggedIn): ?>
|
<?php if ($isLoggedIn): ?>
|
||||||
<div class="flex items-center gap-4 text-sm">
|
<div class="flex items-center gap-4 text-sm">
|
||||||
<span class="text-gray-400">Hallo, <span class="text-white font-bold"><?= htmlspecialchars($_SESSION['username']) ?></span></span>
|
<span class="text-gray-400">Hallo, <span class="text-white font-bold"><?= htmlspecialchars($_SESSION['username']) ?></span></span>
|
||||||
|
|
||||||
<button onclick="openPasswordModal()" class="text-gray-400 hover:text-white transition-colors" title="Passwort ändern">
|
<button 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>
|
<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>
|
||||||
|
|
||||||
<button onclick="logout()" class="text-red-400 hover:text-red-300 font-medium transition-colors">Abmelden</button>
|
<button onclick="logout()" class="text-red-400 hover:text-red-300 font-medium transition-colors">Abmelden</button>
|
||||||
</div>
|
</div>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
@@ -84,13 +82,58 @@ $nodeCount = $stmt->fetchColumn();
|
|||||||
|
|
||||||
<!-- TAB 1: PVE -->
|
<!-- TAB 1: PVE -->
|
||||||
<div id="tab-pve" class="flex-1 flex flex-col min-w-0 transition-opacity duration-300">
|
<div id="tab-pve" class="flex-1 flex flex-col min-w-0 transition-opacity duration-300">
|
||||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-6">
|
|
||||||
|
<!-- NEUE TOP KACHEL: Gesamtübersicht -->
|
||||||
|
<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="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>
|
||||||
|
|
||||||
|
<!-- Hardware 4 Columns -->
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 xl: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">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">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"><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>
|
<div class="bg-darkcard border border-darkborder rounded-xl p-5 shadow-lg flex flex-col justify-center"><div class="flex justify-between items-start mb-1"><div><p class="text-gray-400 text-sm font-medium">System Updates (APT)</p><h3 id="stat-updates-text" class="text-2xl font-bold text-white mt-1">Lade...</h3></div><div class="p-2 bg-purple-500/10 rounded-lg"><svg class="w-6 h-6 text-purple-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path></svg></div></div><div id="stat-updates-sub" class="text-xs text-gray-500 mt-1 truncate">Prüfe Updates...</div></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-darkcard border border-darkborder rounded-xl p-5 shadow-lg min-h-[300px] flex flex-col justify-center mb-6"><div class="flex justify-between items-center mb-4"><h3 class="text-white font-bold">Live Cluster Auslastung</h3><span class="text-xs text-gray-500 flex items-center gap-2"><span class="w-2 h-2 rounded-full bg-green-500 animate-pulse"></span> Live Sync</span></div><div class="relative h-full w-full min-h-[220px]"><canvas id="liveChart"></canvas></div></div>
|
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
||||||
|
<div class="bg-darkcard border border-darkborder rounded-xl p-5 shadow-lg min-h-[300px] flex flex-col justify-center">
|
||||||
|
<div class="flex justify-between items-center mb-4"><h3 class="text-white font-bold">Live Cluster Auslastung</h3><span class="text-xs text-gray-500 flex items-center gap-2"><span class="w-2 h-2 rounded-full bg-blue-500 animate-pulse"></span> CPU / RAM</span></div>
|
||||||
|
<div class="relative h-full w-full min-h-[220px]"><canvas id="liveChart"></canvas></div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-darkcard border border-darkborder rounded-xl p-5 shadow-lg min-h-[300px] flex flex-col justify-center">
|
||||||
|
<div class="flex justify-between items-center mb-4"><h3 class="text-white font-bold">Live Netzwerk Traffic</h3><span class="text-xs text-gray-500 flex items-center gap-2"><span class="w-2 h-2 rounded-full bg-emerald-500 animate-pulse"></span> MB/s Total pro Node</span></div>
|
||||||
|
<div class="relative h-full w-full min-h-[220px]"><canvas id="liveNetChart"></canvas></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="bg-darkcard border border-darkborder rounded-xl p-5 shadow-lg"><h3 class="text-white font-bold mb-4">🔥 Top 5 Ressourcen-Fresser</h3><div id="top-vms-container" class="space-y-3"><p class="text-gray-400 text-sm">Lädt Live-Daten von Proxmox API...</p></div></div>
|
<div class="bg-darkcard border border-darkborder rounded-xl p-5 shadow-lg"><h3 class="text-white font-bold mb-4">🔥 Top 5 Ressourcen-Fresser</h3><div id="top-vms-container" class="space-y-3"><p class="text-gray-400 text-sm">Lädt Live-Daten von Proxmox API...</p></div></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -130,8 +173,6 @@ $nodeCount = $stmt->fetchColumn();
|
|||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<script> window.APP = { isLoggedIn: <?= $isLoggedIn ? 'true' : 'false' ?>, nodeCount: <?= $nodeCount ?>, username: '<?= htmlspecialchars($_SESSION['username'] ?? '') ?>' }; </script>
|
<script> window.APP = { isLoggedIn: <?= $isLoggedIn ? 'true' : 'false' ?>, nodeCount: <?= $nodeCount ?>, username: '<?= htmlspecialchars($_SESSION['username'] ?? '') ?>' }; </script>
|
||||||
|
|
||||||
<!-- HIER IST DER CACHE-BUSTER, DAMIT DER LOGOUT IMMER GEHT! -->
|
|
||||||
<script src="app.js?v=<?= time() ?>"></script>
|
<script src="app.js?v=<?= time() ?>"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
Reference in New Issue
Block a user