Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e72a9a53e7 | ||
|
|
a50a944f64 | ||
|
|
e13319cb88 |
@@ -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.
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 280 KiB |
+26
-35
@@ -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)
|
||||
$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_POST => true,
|
||||
CURLOPT_POSTFIELDS => http_build_query(['username' => $tokenId, 'password' => $tokenSecret])
|
||||
]);
|
||||
$authRes = json_decode(curl_exec($chAuth), true);
|
||||
curl_close($chAuth);
|
||||
|
||||
if(!isset($authRes['data']['ticket'])) {
|
||||
return ['data' => null, 'error' => 'Authentifizierung fehlgeschlagen'];
|
||||
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_POST => true,
|
||||
CURLOPT_POSTFIELDS => http_build_query(['username' => $tokenId, 'password' => $tokenSecret])
|
||||
]);
|
||||
$authRes = json_decode(curl_exec($chAuth), true); curl_close($chAuth);
|
||||
|
||||
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}"];
|
||||
}
|
||||
|
||||
// 2. Passendes Cookie für das System wählen
|
||||
$cookieName = 'PVEAuthCookie';
|
||||
if ($type === 'pbs') $cookieName = 'PBSAuthCookie';
|
||||
if ($type === 'pmg') $cookieName = 'PMGAuthCookie';
|
||||
|
||||
$headers = [
|
||||
"Cookie: {$cookieName}=" . $authRes['data']['ticket'],
|
||||
"CSRFPreventionToken: " . $authRes['data']['CSRFPreventionToken']
|
||||
];
|
||||
|
||||
// 3. Eigentlichen API-Call ausführen
|
||||
$ch = curl_init("https://{$ip}:{$port}{$endpoint}");
|
||||
$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) ?: [];
|
||||
}
|
||||
|
||||
+192
-41
@@ -1,18 +1,66 @@
|
||||
<?php
|
||||
// /home/docker/pve_dashboard/src/api_pve.php
|
||||
if (!defined('PDO::ATTR_DRIVER_NAME')) exit; // Schutz
|
||||
if (!defined('PDO::ATTR_DRIVER_NAME')) exit; // Schutz vor direktem Aufruf
|
||||
|
||||
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 ($nInfo['status'] === '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;
|
||||
// Duplikate (z.B. in Clustern) vermeiden
|
||||
if (!isset($seenNodes[$nInfo['node']])) {
|
||||
$seenNodes[$nInfo['node']] = true;
|
||||
|
||||
if ($nInfo['status'] === 'online') {
|
||||
$data['cluster_stats']['nodes_online']++;
|
||||
$data['cpu_cores'] += $nInfo['maxcpu'] ?? 0;
|
||||
$data['cpu_used'] += ($nInfo['cpu'] ?? 0) * ($nInfo['maxcpu'] ?? 0);
|
||||
$data['ram_total'] += $nInfo['maxmem'] ?? 0;
|
||||
$data['ram_used'] += $nInfo['mem'] ?? 0;
|
||||
$data['disk_total'] += $nInfo['maxdisk'] ?? 0;
|
||||
$data['disk_used'] += $nInfo['disk'] ?? 0;
|
||||
|
||||
$nRrd = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$nInfo['node']}/rrddata?timeframe=hour&cf=AVERAGE");
|
||||
$netin = 0; $netout = 0;
|
||||
if (isset($nRrd['data']) && is_array($nRrd['data'])) {
|
||||
for ($i = count($nRrd['data']) - 1; $i >= 0; $i--) {
|
||||
if (isset($nRrd['data'][$i]['netin']) && $nRrd['data'][$i]['netin'] !== null) {
|
||||
$netin = $nRrd['data'][$i]['netin'];
|
||||
$netout = $nRrd['data'][$i]['netout'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
$data['nodes_net'][] = ['name' => $nInfo['node'], 'netin' => $netin, 'netout' => $netout];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// VMs von allen angebundenen Hosts ziehen und Duplikate filtern
|
||||
$vms = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/cluster/resources?type=vm");
|
||||
if (isset($vms['data'])) {
|
||||
foreach($vms['data'] as $vm) {
|
||||
$uniqueId = $vm['vmid'] . '@' . ($vm['node'] ?? 'unknown');
|
||||
if (!isset($seenVms[$uniqueId])) {
|
||||
$seenVms[$uniqueId] = true;
|
||||
$data['cluster_stats']['vms_total']++;
|
||||
if (isset($vm['status']) && $vm['status'] === 'running') {
|
||||
$data['cluster_stats']['vms_running']++;
|
||||
} else {
|
||||
$data['cluster_stats']['vms_stopped']++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,20 +70,42 @@ 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;
|
||||
@@ -44,22 +114,69 @@ if ($action === 'get_all_vms') {
|
||||
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;
|
||||
|
||||
$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([$_POST['name'], $_POST['ip'], $_POST['user'], $_POST['pass'], $_POST['type'] ?? 'pve']);
|
||||
logAudit('Server hinzugefügt', "Node: {$_POST['name']} ({$_POST['ip']})");
|
||||
$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') {
|
||||
@@ -72,10 +189,17 @@ if ($action === 'create_user') {
|
||||
|
||||
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 +211,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') {
|
||||
@@ -106,9 +230,9 @@ if ($action === 'update_vm_config') {
|
||||
if(isset($_POST['memory'])) $params['memory'] = $_POST['memory'];
|
||||
if(isset($_POST['cores'])) $params['cores'] = $_POST['cores'];
|
||||
if(isset($_POST['net0'])) $params['net0'] = $_POST['net0'];
|
||||
$res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$_POST['host']}/{$_POST['type']}/{$_POST['vmid']}/config", "POST", $params);
|
||||
$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 +240,25 @@ 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']}"]);
|
||||
$res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$_POST['host']}/{$_POST['type']}/{$_POST['vmid']}/config", 'pve', 'POST', ["net{$nextNic}" => "virtio,bridge={$_POST['bridge']}"]);
|
||||
logAudit('VM NIC hinzugefügt', "VMID: {$_POST['vmid']} Bridge: {$_POST['bridge']}");
|
||||
echo json_encode(['success' => isset($res['data']), 'slot' => "net{$nextNic}"]); exit;
|
||||
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']]);
|
||||
$res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$_POST['host']}/{$_POST['type']}/{$_POST['vmid']}/resize", 'pve', 'PUT', ['disk' => $_POST['disk'], 'size' => $_POST['size']]);
|
||||
logAudit('VM Disk erweitert', "VMID: {$_POST['vmid']} Disk: {$_POST['disk']} Size: {$_POST['size']}");
|
||||
echo json_encode(['success' => isset($res['data'])]); exit;
|
||||
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");
|
||||
$res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$_POST['host']}/{$_POST['type']}/{$_POST['vmid']}", 'pve', 'DELETE');
|
||||
logAudit('VM Gelöscht', "VMID: {$_POST['vmid']}");
|
||||
echo json_encode(['success' => isset($res['data'])]); exit;
|
||||
echo json_encode(['success' => isset($res['data']), 'error' => 'API Fehler']); exit;
|
||||
}
|
||||
|
||||
if ($action === 'get_vm_snapshots') {
|
||||
@@ -148,11 +272,11 @@ 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"); }
|
||||
if ($cmd === 'create') { $res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$_POST['host']}/{$_POST['type']}/{$_POST['vmid']}/snapshot", 'pve', 'POST', ['snapname' => $snapname]); }
|
||||
elseif ($cmd === 'delete') { $res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$_POST['host']}/{$_POST['type']}/{$_POST['vmid']}/snapshot/{$snapname}", 'pve', 'DELETE'); }
|
||||
elseif ($cmd === 'rollback') { $res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$_POST['host']}/{$_POST['type']}/{$_POST['vmid']}/snapshot/{$snapname}/rollback", 'pve', 'POST'); }
|
||||
logAudit('Snapshot ' . $cmd, "VMID: {$_POST['vmid']} Snap: {$snapname}");
|
||||
echo json_encode(['success' => isset($res['data'])]); exit;
|
||||
echo json_encode(['success' => isset($res['data']), 'error' => 'API Fehler']); exit;
|
||||
}
|
||||
|
||||
if ($action === 'get_backup_storages') {
|
||||
@@ -183,25 +307,52 @@ 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']);
|
||||
$res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$_POST['host']}/vzdump", 'pve', 'POST', ['vmid' => $_POST['vmid'], 'storage' => $_POST['storage'], 'mode' => 'snapshot']);
|
||||
logAudit('Manuelles Backup', "VMID: {$_POST['vmid']} Storage: {$_POST['storage']}");
|
||||
echo json_encode(['success' => isset($res['data'])]); exit;
|
||||
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]);
|
||||
if (isset($res['data']) && $res['data']['status'] === 'running') { echo json_encode(['success' => false, 'error' => 'VM gestoppt?']); exit; }
|
||||
$resRestore = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$_POST['host']}/{$_POST['type']}", 'pve', 'POST', ['vmid' => $_POST['vmid'], 'archive' => $_POST['archive'], 'force' => 1]);
|
||||
logAudit('Backup Restore', "VMID: {$_POST['vmid']} Archive: {$_POST['archive']}");
|
||||
echo json_encode(['success' => isset($resRestore['data'])]); exit;
|
||||
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);
|
||||
echo json_encode(['success' => true, 'data' => $res['data'] ?? []]); exit;
|
||||
|
||||
if ($action === 'get_node_status') {
|
||||
$nData = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes");
|
||||
$internalName = $nData['data'][0]['node'] ?? 'pve';
|
||||
|
||||
$res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$internalName}/status");
|
||||
$rrd = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$internalName}/rrddata?timeframe=hour&cf=AVERAGE");
|
||||
|
||||
$netin = 0; $netout = 0;
|
||||
if (isset($rrd['data']) && is_array($rrd['data'])) {
|
||||
for ($i = count($rrd['data']) - 1; $i >= 0; $i--) {
|
||||
if (isset($rrd['data'][$i]['netin']) && $rrd['data'][$i]['netin'] !== null) {
|
||||
$netin = $rrd['data'][$i]['netin'];
|
||||
$netout = $rrd['data'][$i]['netout'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$data = $res['data'] ?? [];
|
||||
$data['netin'] = $netin;
|
||||
$data['netout'] = $netout;
|
||||
$data['is_rrd_net'] = true;
|
||||
|
||||
echo json_encode(['success' => true, 'data' => $data]); exit;
|
||||
} else {
|
||||
$endpoint = "/api2/json/nodes/{$_GET['host']}/{$_GET['type']}/{$_GET['vmid']}/status/current";
|
||||
$res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], $endpoint);
|
||||
echo json_encode(['success' => true, 'data' => $res['data'] ?? []]); exit;
|
||||
}
|
||||
}
|
||||
?>
|
||||
+97
-8
@@ -18,7 +18,6 @@ async function logout() {
|
||||
|
||||
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()"]')) {
|
||||
const userBtn = document.querySelector('a[onclick="openUserManager()"]');
|
||||
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');
|
||||
window.openPasswordModal = function() { if(pwdModal) { pwdModal.classList.remove('hidden'); document.getElementById('changePasswordForm').reset(); } }
|
||||
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;
|
||||
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 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) {} }
|
||||
@@ -227,12 +302,28 @@ if (window.APP.isLoggedIn && window.APP.nodeCount > 0) {
|
||||
const d = res.data; const now = Date.now();
|
||||
let cpuRaw = 0; if (d.cpu !== undefined) cpuRaw = d.cpu; else if (d.cpuinfo && d.cpuinfo.cpus) cpuRaw = 0; const cpu = (cpuRaw * 100).toFixed(1);
|
||||
let ram = 0; if (d.maxmem && d.maxmem > 0) { ram = ((d.mem / d.maxmem) * 100).toFixed(1); } else if (d.memory && d.memory.total > 0) { ram = ((d.memory.used / d.memory.total) * 100).toFixed(1); }
|
||||
let currentNetIn = d.netin || 0; let currentNetOut = d.netout || 0; let rxSpeed = 0; let txSpeed = 0;
|
||||
if(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;
|
||||
const timeStr = new Date().toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
||||
|
||||
perfChartObj.data.labels.push(timeStr); perfChartObj.data.datasets[0].data.push(cpu); perfChartObj.data.datasets[1].data.push(ram); if(perfChartObj.data.labels.length > 30) { perfChartObj.data.labels.shift(); perfChartObj.data.datasets[0].data.shift(); perfChartObj.data.datasets[1].data.shift(); } perfChartObj.update();
|
||||
if(prevTime !== null) { 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) {}
|
||||
};
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
// === SCHEDULER ===
|
||||
const cronModal = document.getElementById('cronManagerModal');
|
||||
window.openCronManager = async function() {
|
||||
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.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');
|
||||
window.openAuditLog = async function() {
|
||||
auditModal.classList.remove('hidden');
|
||||
|
||||
+3
-9
@@ -2,7 +2,6 @@
|
||||
// /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
|
||||
function isCronMatch($cron, $time = null) {
|
||||
if ($time === null) $time = time();
|
||||
$cronParts = explode(' ', trim($cron));
|
||||
@@ -20,14 +19,14 @@ 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"
|
||||
if (strpos($part, '*/') === 0) {
|
||||
$step = (int)substr($part, 2);
|
||||
return $step > 0 && ((int)$current % $step) === 0;
|
||||
}
|
||||
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) {
|
||||
$headers = ["Authorization: PVEAPIToken={$tokenId}={$tokenSecret}"];
|
||||
$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);
|
||||
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 +57,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 +65,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 +80,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']]);
|
||||
|
||||
+47
-6
@@ -38,11 +38,9 @@ $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="logout()" class="text-red-400 hover:text-red-300 font-medium transition-colors">Abmelden</button>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
@@ -84,13 +82,58 @@ $nodeCount = $stmt->fetchColumn();
|
||||
|
||||
<!-- TAB 1: PVE -->
|
||||
<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">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>
|
||||
</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>
|
||||
|
||||
@@ -130,8 +173,6 @@ $nodeCount = $stmt->fetchColumn();
|
||||
</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>
|
||||
Reference in New Issue
Block a user