commit 12a93301f5ceabc9f280472f70e356f64f27469b Author: root Date: Sun Aug 23 18:50:09 2026 +0200 Initial Release v1.0 🚀 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ca5e61d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,35 @@ +# /home/docker/pve_dashboard/Dockerfile +FROM php:8.2-apache + +# Benötigte Pakete installieren (SQLite + OpenSSL für Zertifikate + Cron für den Scheduler) +RUN apt-get update && apt-get install -y \ + libsqlite3-dev \ + openssl \ + cron \ + && docker-php-ext-install pdo_sqlite + +# Apache Rewrite-Modul (für schöne URLs) und SSL-Modul aktivieren +RUN a2enmod rewrite ssl + +# Generiere ein Self-Signed Zertifikat (Gültig für 10 Jahre) +RUN openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \ + -keyout /etc/ssl/private/apache-selfsigned.key \ + -out /etc/ssl/certs/apache-selfsigned.crt \ + -subj "/C=DE/ST=Brandenburg/L=Gruenheide/O=PUC/OU=IT/CN=localhost" + +# Passe die Default SSL Konfiguration an +RUN sed -i 's/ssl-cert-snakeoil.pem/apache-selfsigned.crt/g' /etc/apache2/sites-available/default-ssl.conf \ + && sed -i 's/ssl-cert-snakeoil.key/apache-selfsigned.key/g' /etc/apache2/sites-available/default-ssl.conf + +# SSL Site in Apache aktivieren +RUN a2ensite default-ssl.conf + +# Cronjob einrichten (Ruft die cron.php minütlich auf und leitet Output ins Docker-Log um) +RUN echo "* * * * * root /usr/local/bin/php /var/www/html/cron.php > /proc/1/fd/1 2>/proc/1/fd/2\n" >> /etc/crontab + +# Beide Ports freigeben +EXPOSE 80 +EXPOSE 443 + +# Startbefehl: Startet erst den Cron-Daemon im Hintergrund und dann Apache im Vordergrund +CMD cron && apache2-foreground \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..6d30fa0 --- /dev/null +++ b/README.md @@ -0,0 +1,39 @@ +# 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. + +## ✨ Features + +* **Unified Multi-Node Management:** Connect multiple PVE, PBS, and PMG instances via API tokens. +* **Proxmox VE (PVE):** + * Live CPU/RAM/Net metrics using Chart.js. + * Start, Stop, Reboot, Delete, and Create VMs & LXC containers. + * Smart NoVNC-Console integration (Popout). + * Hardware & Network config (Resize Disks, add NICs). + * Backup & Snapshot management. +* **Proxmox Backup Server (PBS):** + * Live Datastore capacity monitoring. + * Snapshot explorer with namespace support. + * System jobs overview (Garbage Collection, Prune, Verify). + * Task Log Viewer. +* **Proxmox Mail Gateway (PMG):** + * Unified Spam Quarantine management (Deliver/Delete). + * Cross-node Mail Queue tracking (Active, Deferred, Hold). + * Advanced Syslog Tracking Center. + * Configuration Editor (Relay, Ports, TLS, Trusted Networks, Relay Domains). + * 1-Click SSL Certificate Upload & Management. +* **Task Scheduler:** Built-in cron system to automate VM and Node reboots overnight. +* **Security First:** + * Granular User Roles (Admin, VM-Manager, Viewer). + * Restrict specific VMs to specific users. + * Comprehensive Audit Log tracking all actions. + * Auto-generated Self-Signed SSL out of the box (Port 443). + +## 📦 Quickstart (Docker) + +1. **Clone the repository:** + ```bash + git clone [https://github.com/DEIN_GITHUB_NAME/proxmox-unified-console.git](https://github.com/DEIN_GITHUB_NAME/proxmox-unified-console.git) + cd proxmox-unified-console \ No newline at end of file diff --git a/data/dashboard.sqlite b/data/dashboard.sqlite new file mode 100644 index 0000000..9d51834 Binary files /dev/null and b/data/dashboard.sqlite differ diff --git a/data/dashboard.sqlite.back b/data/dashboard.sqlite.back new file mode 100644 index 0000000..b1e1c4d Binary files /dev/null and b/data/dashboard.sqlite.back differ diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..f4a17e2 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,20 @@ +# /home/docker/pve_dashboard/docker-compose.yml +version: '3.8' + +services: + pve-dashboard: + build: . + container_name: pve_unified_console + restart: unless-stopped + ports: + # Port 80 bleibt fürs Testing / HAProxy + - "8080:80" + # NEU: Port 443 für sicheres NoVNC! (Wir mappen es nach außen z.B. auf 8443) + - "8443:443" + volumes: + # Dein Source-Code + - ./src:/var/www/html + # Deine Datenbank, sicher ausgelagert + - ./data:/var/www/data + environment: + - TZ=Europe/Berlin diff --git a/gitignore b/gitignore new file mode 100644 index 0000000..ac3a91f --- /dev/null +++ b/gitignore @@ -0,0 +1,4 @@ +# /home/docker/pve_dashboard/.gitignore +data/* +!data/.gitkeep +.DS_Store \ No newline at end of file diff --git a/src/api.php b/src/api.php new file mode 100644 index 0000000..8dfe238 --- /dev/null +++ b/src/api.php @@ -0,0 +1,153 @@ +prepare("INSERT INTO audit_logs (user_id, username, action, target) VALUES (?, ?, ?, ?)"); + $stmt->execute([$userId, $username, $actionName, $target]); +} + +if ($action === 'login') { + $username = trim($_POST['username'] ?? ''); $password = $_POST['password'] ?? ''; + $stmt = $pdo->prepare("SELECT * FROM users WHERE username = ?"); $stmt->execute([$username]); $user = $stmt->fetch(PDO::FETCH_ASSOC); + if ($user && password_verify($password, $user['password_hash'])) { + $_SESSION['user_id'] = $user['id']; $_SESSION['username'] = $user['username']; $_SESSION['role'] = $user['role']; + logAudit('User Login', 'IP: ' . $_SERVER['REMOTE_ADDR']); + echo json_encode(['success' => true]); + } else echo json_encode(['success' => false, 'error' => 'Login fehlgeschlagen.']); + exit; +} +if ($action === 'logout') { + logAudit('User Logout', ''); + session_destroy(); echo json_encode(['success' => true]); exit; +} + +if (!isset($_SESSION['user_id'])) { echo json_encode(['success' => false, 'error' => 'Zugriff verweigert.']); exit; } + +$isAdmin = ($_SESSION['role'] ?? '') === 'admin'; +session_write_close(); + +function getProxmoxData($ip, $tokenId, $tokenSecret, $endpoint, $type = 'pve', $method = "GET", $postData = null, $timeout = 4) { + $port = ($type === 'pbs') ? 8007 : 8006; + if ($type === 'pmg' || $type === 'pbs') { + $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 => 2, 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' => []]; + $cookieName = ($type === 'pbs') ? 'PBSAuthCookie' : 'PMGAuthCookie'; + $headers = ["Cookie: {$cookieName}=" . $authRes['data']['ticket'], "CSRFPreventionToken: " . $authRes['data']['CSRFPreventionToken']]; + } else { + $headers = ["Authorization: PVEAPIToken={$tokenId}={$tokenSecret}"]; + } + + $ch = curl_init("https://{$ip}:{$port}{$endpoint}"); + $options = [CURLOPT_RETURNTRANSFER => true, CURLOPT_SSL_VERIFYPEER => false, CURLOPT_SSL_VERIFYHOST => false, CURLOPT_TIMEOUT => $timeout, CURLOPT_HTTPHEADER => $headers]; + if ($method !== "GET") { $options[CURLOPT_CUSTOMREQUEST] = $method; if ($postData) $options[CURLOPT_POSTFIELDS] = is_array($postData) ? http_build_query($postData) : $postData; } + curl_setopt_array($ch, $options); + $res = curl_exec($ch); curl_close($ch); return json_decode($res, true); +} + +function checkVmPermission($pdo, $vmid) { + global $isAdmin; if ($isAdmin) return true; + @session_start(); $userId = $_SESSION['user_id'] ?? 0; session_write_close(); + $stmt = $pdo->prepare("SELECT permissions FROM users WHERE id = ?"); $stmt->execute([$userId]); return in_array($vmid, json_decode($stmt->fetchColumn(), true)['allowed_vms'] ?? []); +} + +// === AUDIT LOG ENDPUNKT === +if ($action === 'get_audit_logs') { + if (!$isAdmin) exit; + $stmt = $pdo->query("SELECT * FROM audit_logs ORDER BY id DESC LIMIT 200"); + echo json_encode(['success' => true, 'data' => $stmt->fetchAll(PDO::FETCH_ASSOC)]); exit; +} + +if ($action === 'get_users' || $action === 'create_user' || $action === 'delete_user' || $action === 'get_nodes' || $action === 'delete_node' || $action === 'add_node') { + require_once 'api_pve.php'; exit; +} + +if ($action === 'get_recent_jobs') { + $stmt = $pdo->query("SELECT * FROM nodes"); $allTasks = []; + foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $node) { + if ($node['type'] === 'pbs') { + $tasks = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/localhost/tasks?limit=10", 'pbs'); + if (isset($tasks['data'])) { foreach ($tasks['data'] as $t) { $t['node_name'] = $node['name']; $allTasks[] = $t; } } + } elseif ($node['type'] === 'pmg') { + $nData = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes", 'pmg'); + if (isset($nData['data'][0]['node'])) { $tasks = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$nData['data'][0]['node']}/tasks?limit=10", 'pmg'); if (isset($tasks['data'])) { foreach ($tasks['data'] as $t) { $t['node_name'] = $node['name']; $allTasks[] = $t; } } } + } else { + $nData = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes", 'pve'); + if (isset($nData['data'])) { foreach ($nData['data'] as $nInfo) { $tasks = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$nInfo['node']}/tasks?limit=10", 'pve'); if (isset($tasks['data'])) { foreach ($tasks['data'] as $t) { $t['node_name'] = $node['name']; $allTasks[] = $t; } } } } + } + } + usort($allTasks, function($a, $b) { return ($b['starttime'] ?? 0) <=> ($a['starttime'] ?? 0); }); echo json_encode(['success' => true, 'data' => array_slice($allTasks, 0, 15)]); exit; +} + +if ($action === 'get_updates') { + $stmt = $pdo->query("SELECT * FROM nodes WHERE type != 'pmg'"); $total = 0; $nodesNeed = []; + foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $node) { + if ($node['type'] === 'pbs') { + $apt = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/localhost/apt/update", 'pbs'); + if (isset($apt['data'])) { $c = count($apt['data']); $total += $c; if ($c > 0) $nodesNeed[] = $node['name'] . " (" . $c . ")"; } + } else { + $nData = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes", 'pve'); + if (isset($nData['data'])) { foreach ($nData['data'] as $nInfo) { $apt = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$nInfo['node']}/apt/update", 'pve'); if (isset($apt['data'])) { $c = count($apt['data']); $total += $c; if ($c > 0) $nodesNeed[] = $nInfo['node'] . " (" . $c . ")"; } } } + } + } + echo json_encode(['success' => true, 'total' => $total, 'details' => implode(', ', $nodesNeed)]); exit; +} + +if ($action === 'get_task_log') { + if (!$isAdmin) exit; + $stmt = $pdo->prepare("SELECT * FROM nodes WHERE id = ?"); $stmt->execute([$_GET['node_id']]); $node = $stmt->fetch(PDO::FETCH_ASSOC); + $upid = $_GET['upid']; $host = $_GET['host'] ?? 'localhost'; + $log = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$host}/tasks/{$upid}/log", $node['type']); + echo json_encode(['success' => true, 'data' => $log['data'] ?? []]); exit; +} + +if ($action === 'get_cron_jobs') { + if (!$isAdmin) exit; + $stmt = $pdo->query("SELECT s.*, n.name as node_name FROM scheduled_tasks s LEFT JOIN nodes n ON s.node_id = n.id ORDER BY s.id DESC"); + echo json_encode(['success' => true, 'data' => $stmt->fetchAll(PDO::FETCH_ASSOC)]); exit; +} +if ($action === 'add_cron_job') { + if (!$isAdmin) exit; + $stmt = $pdo->prepare("INSERT INTO scheduled_tasks (name, node_id, action_type, target_vmid, cron_schedule) VALUES (?, ?, ?, ?, ?)"); + $stmt->execute([$_POST['name'], $_POST['node_id'], $_POST['action_type'], $_POST['target_vmid'] ?? '', $_POST['cron_schedule']]); + logAudit('Cronjob erstellt', $_POST['name']); + echo json_encode(['success' => true]); exit; +} +if ($action === 'delete_cron_job') { + if (!$isAdmin) exit; + $stmt = $pdo->prepare("DELETE FROM scheduled_tasks WHERE id = ?"); + $stmt->execute([$_POST['id']]); + logAudit('Cronjob gelöscht', 'ID: ' . $_POST['id']); + echo json_encode(['success' => true]); exit; +} +if ($action === 'toggle_cron_job') { + if (!$isAdmin) exit; + $stmt = $pdo->prepare("UPDATE scheduled_tasks SET is_active = ? WHERE id = ?"); + $stmt->execute([$_POST['is_active'], $_POST['id']]); + logAudit('Cronjob Status geändert', 'ID: ' . $_POST['id'] . ' -> ' . ($_POST['is_active'] ? 'Aktiv' : 'Pausiert')); + echo json_encode(['success' => true]); exit; +} + +$pbsActions = ['get_pbs_stats', 'pbs_get_datastore_content', 'pbs_delete_snapshot', 'pbs_get_jobs', 'pbs_get_sync_jobs']; +$pmgActions = [ + 'get_pmg_stats', 'pmg_get_spam_queue', 'pmg_quarantine_action', 'pmg_get_queues', 'pmg_get_tracking', + 'pmg_get_config', 'pmg_set_config', 'pmg_get_ssl', 'pmg_upload_ssl', + 'pmg_get_domains', 'pmg_add_domain', 'pmg_delete_domain', + 'pmg_get_networks', 'pmg_add_network', 'pmg_delete_network' +]; + +if (in_array($action, $pbsActions)) { require_once 'api_pbs.php'; } +elseif (in_array($action, $pmgActions)) { require_once 'api_pmg.php'; } +else { require_once 'api_pve.php'; } +?> \ No newline at end of file diff --git a/src/api_pbs.php b/src/api_pbs.php new file mode 100644 index 0000000..6fb4575 --- /dev/null +++ b/src/api_pbs.php @@ -0,0 +1,80 @@ +query("SELECT * FROM nodes WHERE type = 'pbs'"); $datastores = []; + foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $node) { + $dsData = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/status/datastore-usage", 'pbs'); + if (isset($dsData['data'])) { + foreach ($dsData['data'] as $ds) { + $ds['host'] = $node['name']; + $ds['node_id'] = $node['id']; + $datastores[] = $ds; + } + } + } + echo json_encode(['success' => true, 'data' => $datastores]); exit; +} + +if ($action === 'pbs_get_datastore_content') { + if (!$isAdmin) exit; + $stmt = $pdo->prepare("SELECT * FROM nodes WHERE id = ? AND type = 'pbs'"); $stmt->execute([$_GET['node_id']]); $node = $stmt->fetch(PDO::FETCH_ASSOC); + $store = $_GET['store']; + + // 1. Hole alle Namespaces (Hetzner, Zaccaria, Zuhause etc.) + $nsData = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/admin/datastore/{$store}/namespace", 'pbs'); + $namespaces = ['']; // Root Namespace immer mitnehmen + if (isset($nsData['data'])) { + foreach ($nsData['data'] as $nsItem) { + if (!empty($nsItem['ns'])) $namespaces[] = $nsItem['ns']; + } + } + + $result = []; + // 2. Iteriere durch JEDEN Namespace und hole die Backups + foreach ($namespaces as $ns) { + $nsParam = empty($ns) ? "" : "?ns=" . urlencode($ns); + $snaps = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/admin/datastore/{$store}/snapshots{$nsParam}", 'pbs', 'GET', null, 15); + + if (isset($snaps['data'])) { + foreach ($snaps['data'] as $snap) { + if (!isset($snap['size']) && isset($snap['files'])) { + $size = 0; foreach ($snap['files'] as $f) { $size += $f['size'] ?? 0; } + $snap['size'] = $size; + } + $snap['ns'] = $ns; // Merken, in welchem Namespace das Backup liegt (wichtig fürs Löschen!) + $result[] = $snap; + } + } + } + + echo json_encode(['success' => true, 'data' => $result]); exit; +} + +if ($action === 'pbs_delete_snapshot') { + if (!$isAdmin) exit; + $stmt = $pdo->prepare("SELECT * FROM nodes WHERE id = ? AND type = 'pbs'"); $stmt->execute([$_POST['node_id']]); $node = $stmt->fetch(PDO::FETCH_ASSOC); + $store = $_POST['store']; $btype = $_POST['btype']; $bid = $_POST['bid']; $btime = $_POST['btime']; $ns = $_POST['ns'] ?? ''; + + $nsParam = empty($ns) ? "" : "&ns=" . urlencode($ns); + $url = "/api2/json/admin/datastore/{$store}/snapshots?backup-type={$btype}&backup-id={$bid}&backup-time={$btime}{$nsParam}"; + + getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], $url, 'pbs', 'DELETE'); + echo json_encode(['success' => true]); exit; +} + +if ($action === 'pbs_get_jobs') { + if (!$isAdmin) exit; + $stmt = $pdo->prepare("SELECT * FROM nodes WHERE id = ? AND type = 'pbs'"); $stmt->execute([$_GET['node_id']]); $node = $stmt->fetch(PDO::FETCH_ASSOC); + $tasks = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/localhost/tasks?limit=100", 'pbs'); + echo json_encode(['success' => true, 'data' => $tasks['data'] ?? []]); exit; +} + +if ($action === 'pbs_get_sync_jobs') { + if (!$isAdmin) exit; + $stmt = $pdo->prepare("SELECT * FROM nodes WHERE id = ? AND type = 'pbs'"); $stmt->execute([$_GET['node_id']]); $node = $stmt->fetch(PDO::FETCH_ASSOC); + $syncs = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/config/sync", 'pbs'); + echo json_encode(['success' => true, 'data' => $syncs['data'] ?? []]); exit; +} +?> \ No newline at end of file diff --git a/src/api_pmg.php b/src/api_pmg.php new file mode 100644 index 0000000..080dbaa --- /dev/null +++ b/src/api_pmg.php @@ -0,0 +1,144 @@ +query("SELECT * FROM nodes WHERE type = 'pmg'"); $pmgNodes = []; + foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $node) { + $nData = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes", 'pmg'); + if (isset($nData['data'][0]['node'])) { + $nName = $nData['data'][0]['node']; + $status = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$nName}/status", 'pmg'); + if(isset($status['data'])) { + $status['data']['host'] = $node['name']; + $status['data']['node_id'] = $node['id']; + $status['data']['internal_name'] = $nName; + $pmgNodes[] = $status['data']; + } + } + } + echo json_encode(['success' => true, 'data' => $pmgNodes]); exit; +} + +if ($action === 'pmg_get_spam_queue') { + if (!$isAdmin) exit; + $stmt = $pdo->prepare("SELECT * FROM nodes WHERE id = ? AND type = 'pmg'"); $stmt->execute([$_GET['node_id']]); $node = $stmt->fetch(PDO::FETCH_ASSOC); + $res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/quarantine/spam", 'pmg', 'GET', null, 8); + echo json_encode(['success' => true, 'data' => $res['data'] ?? []]); exit; +} + +if ($action === 'pmg_quarantine_action') { + if (!$isAdmin) exit; + $stmt = $pdo->prepare("SELECT * FROM nodes WHERE id = ? AND type = 'pmg'"); $stmt->execute([$_POST['node_id']]); $node = $stmt->fetch(PDO::FETCH_ASSOC); + $res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/quarantine/content", 'pmg', "POST", ['action' => $_POST['pmg_action'], 'id' => $_POST['mailid']]); + echo json_encode(['success' => true, 'res' => $res]); exit; +} + +if ($action === 'pmg_get_queues') { + if (!$isAdmin) exit; + $stmt = $pdo->prepare("SELECT * FROM nodes WHERE id = ? AND type = 'pmg'"); $stmt->execute([$_GET['node_id']]); $node = $stmt->fetch(PDO::FETCH_ASSOC); + $internalName = $_GET['internal_name'] ?? ''; if (!$internalName) exit; + $allMails = []; $queues = ['deferred', 'active', 'incoming', 'hold']; + foreach ($queues as $qName) { + $res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$internalName}/postfix/queue/{$qName}", 'pmg'); + if (isset($res['data'])) { foreach ($res['data'] as $mail) { $mail['queue_name'] = $qName; $allMails[] = $mail; } } + } + echo json_encode(['success' => true, 'data' => $allMails]); exit; +} + +if ($action === 'pmg_get_tracking') { + if (!$isAdmin) exit; + $stmt = $pdo->prepare("SELECT * FROM nodes WHERE id = ? AND type = 'pmg'"); $stmt->execute([$_GET['node_id']]); $node = $stmt->fetch(PDO::FETCH_ASSOC); + $internalName = $_GET['internal_name'] ?? ''; $filter = $_GET['filter'] ?? ''; if (!$internalName) exit; + $startTime = time() - 86400; $endTime = time(); $filterStr = !empty($filter) ? "&pmail=" . urlencode($filter) : ""; + $res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$internalName}/tracker?starttime={$startTime}&endtime={$endTime}{$filterStr}", 'pmg', 'GET', null, 10); + echo json_encode(['success' => true, 'data' => $res['data'] ?? []]); exit; +} + +// === CONFIG ROUTEN === +if ($action === 'pmg_get_config') { + if (!$isAdmin) exit; + $stmt = $pdo->prepare("SELECT * FROM nodes WHERE id = ? AND type = 'pmg'"); $stmt->execute([$_GET['node_id']]); $node = $stmt->fetch(PDO::FETCH_ASSOC); + $res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/config/mail", 'pmg'); + echo json_encode(['success' => true, 'data' => $res['data'] ?? []]); exit; +} + +if ($action === 'pmg_set_config') { + if (!$isAdmin) exit; + $stmt = $pdo->prepare("SELECT * FROM nodes WHERE id = ? AND type = 'pmg'"); $stmt->execute([$_POST['node_id']]); $node = $stmt->fetch(PDO::FETCH_ASSOC); + $params = []; + if (isset($_POST['relay'])) $params['relay'] = $_POST['relay']; + if (isset($_POST['ext_port'])) $params['ext_port'] = $_POST['ext_port']; + if (isset($_POST['int_port'])) $params['int_port'] = $_POST['int_port']; + if (isset($_POST['tls'])) $params['tls'] = $_POST['tls']; + if (isset($_POST['tlslog'])) $params['tlslog'] = $_POST['tlslog']; + $res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/config/mail", 'pmg', 'PUT', $params); + echo json_encode(['success' => true, 'res' => $res]); exit; +} + +// === RELAY DOMAINS === +if ($action === 'pmg_get_domains') { + if (!$isAdmin) exit; + $stmt = $pdo->prepare("SELECT * FROM nodes WHERE id = ? AND type = 'pmg'"); $stmt->execute([$_GET['node_id']]); $node = $stmt->fetch(PDO::FETCH_ASSOC); + $res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/config/domains", 'pmg'); + echo json_encode(['success' => true, 'data' => $res['data'] ?? []]); exit; +} +if ($action === 'pmg_add_domain') { + if (!$isAdmin) exit; + $stmt = $pdo->prepare("SELECT * FROM nodes WHERE id = ? AND type = 'pmg'"); $stmt->execute([$_POST['node_id']]); $node = $stmt->fetch(PDO::FETCH_ASSOC); + $res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/config/domains", 'pmg', 'POST', ['domain' => $_POST['domain']]); + echo json_encode(['success' => true, 'res' => $res]); exit; +} +if ($action === 'pmg_delete_domain') { + if (!$isAdmin) exit; + $stmt = $pdo->prepare("SELECT * FROM nodes WHERE id = ? AND type = 'pmg'"); $stmt->execute([$_POST['node_id']]); $node = $stmt->fetch(PDO::FETCH_ASSOC); + $domain = urlencode($_POST['domain']); + $res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/config/domains/{$domain}", 'pmg', 'DELETE'); + echo json_encode(['success' => true, 'res' => $res]); exit; +} + +// === TRUSTED NETWORKS === +if ($action === 'pmg_get_networks') { + if (!$isAdmin) exit; + $stmt = $pdo->prepare("SELECT * FROM nodes WHERE id = ? AND type = 'pmg'"); $stmt->execute([$_GET['node_id']]); $node = $stmt->fetch(PDO::FETCH_ASSOC); + $res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/config/mynetworks", 'pmg'); + echo json_encode(['success' => true, 'data' => $res['data'] ?? []]); exit; +} +if ($action === 'pmg_add_network') { + if (!$isAdmin) exit; + $stmt = $pdo->prepare("SELECT * FROM nodes WHERE id = ? AND type = 'pmg'"); $stmt->execute([$_POST['node_id']]); $node = $stmt->fetch(PDO::FETCH_ASSOC); + $res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/config/mynetworks", 'pmg', 'POST', ['cidr' => $_POST['cidr']]); + echo json_encode(['success' => true, 'res' => $res]); exit; +} +if ($action === 'pmg_delete_network') { + if (!$isAdmin) exit; + $stmt = $pdo->prepare("SELECT * FROM nodes WHERE id = ? AND type = 'pmg'"); $stmt->execute([$_POST['node_id']]); $node = $stmt->fetch(PDO::FETCH_ASSOC); + $cidr = urlencode($_POST['cidr']); // Verhindert Router-Crashes wegen Slashes in der IP (z.B. /24) + $res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/config/mynetworks/{$cidr}", 'pmg', 'DELETE'); + echo json_encode(['success' => true, 'res' => $res]); exit; +} + +// === SSL ZERTIFIKATE === +if ($action === 'pmg_get_ssl') { + if (!$isAdmin) exit; + $stmt = $pdo->prepare("SELECT * FROM nodes WHERE id = ? AND type = 'pmg'"); $stmt->execute([$_GET['node_id']]); $node = $stmt->fetch(PDO::FETCH_ASSOC); + $internalName = $_GET['internal_name'] ?? ''; if (!$internalName) exit; + $res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$internalName}/certificates/info", 'pmg'); + echo json_encode(['success' => true, 'data' => $res['data'] ?? []]); exit; +} +if ($action === 'pmg_upload_ssl') { + if (!$isAdmin) exit; + $stmt = $pdo->prepare("SELECT * FROM nodes WHERE id = ? AND type = 'pmg'"); $stmt->execute([$_POST['node_id']]); $node = $stmt->fetch(PDO::FETCH_ASSOC); + $internalName = $_POST['internal_name'] ?? ''; if (!$internalName) exit; + + // Proxmox erlaubt Upload von Zertifikaten. PMG lädt diese unter /nodes/{node}/certificates/custom hoch. + $params = [ + 'certificates' => $_POST['certificate'], + 'key' => $_POST['private_key'], + 'force' => 1, + 'restart' => 1 + ]; + $res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$internalName}/certificates/custom", 'pmg', 'POST', $params); + echo json_encode(['success' => true, 'res' => $res]); exit; +} +?> \ No newline at end of file diff --git a/src/api_pve.php b/src/api_pve.php new file mode 100644 index 0000000..8fbbf9e --- /dev/null +++ b/src/api_pve.php @@ -0,0 +1,207 @@ +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]; + 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; + } + } + } + } + if ($data['cpu_cores'] > 0) $data['cpu_percent'] = round(($data['cpu_used'] / $data['cpu_cores']) * 100, 1); + echo json_encode(['success' => true, 'data' => $data]); exit; +} + +if ($action === 'get_top_vms') { + $stmt = $pdo->query("SELECT * FROM nodes WHERE type = 'pve'"); $nodes = $stmt->fetchAll(PDO::FETCH_ASSOC); $allVms = []; + 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; } } } + } + 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 = []; + 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; } } } + } + usort($allVms, function($a, $b) { if ($a['status'] === 'running' && $b['status'] !== 'running') return -1; if ($a['status'] !== 'running' && $b['status'] === 'running') return 1; return $a['vmid'] <=> $b['vmid']; }); + echo json_encode(['success' => true, 'data' => $allVms]); exit; +} + +if ($action === 'vm_action') { + if (!checkVmPermission($pdo, $_POST['vmid'])) exit; + $stmt = $pdo->prepare("SELECT * FROM nodes WHERE id = ?"); $stmt->execute([$_POST['node_id']]); $node = $stmt->fetch(PDO::FETCH_ASSOC); + $res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$_POST['host']}/{$_POST['type']}/{$_POST['vmid']}/status/{$_POST['cmd']}", "POST"); + + // AUDIT LOG + logAudit("VM {$_POST['cmd']}", "VMID: {$_POST['vmid']} auf Host: {$_POST['host']}"); + echo json_encode(['success' => isset($res['data']), 'error' => $res['errors'] ?? 'Fehler']); exit; +} + +if ($action === 'get_nodes') { if (!$isAdmin) exit; $stmt = $pdo->query("SELECT id, name, ip_address, type FROM nodes"); echo json_encode(['success' => true, 'data' => $stmt->fetchAll(PDO::FETCH_ASSOC)]); exit; } +if ($action === 'delete_node') { if (!$isAdmin) exit; $stmt = $pdo->prepare("DELETE FROM nodes WHERE id = ?"); $stmt->execute([$_POST['id']]); logAudit('Server gelöscht', "Node ID: {$_POST['id']}"); echo json_encode(['success' => true]); exit; } +if ($action === 'add_node') { + if (!$isAdmin) exit; + $stmt = $pdo->prepare("INSERT INTO nodes (name, ip_address, token_id, token_secret, type) VALUES (?, ?, ?, ?, ?)"); + $stmt->execute([$_POST['name'], $_POST['ip'], $_POST['user'], $_POST['pass'], $_POST['type'] ?? 'pve']); + logAudit('Server hinzugefügt', "Node: {$_POST['name']} ({$_POST['ip']})"); + echo json_encode(['success' => true]); exit; +} +if ($action === 'get_users') { if (!$isAdmin) exit; $stmt = $pdo->query("SELECT id, username, role FROM users"); echo json_encode(['success' => true, 'data' => $stmt->fetchAll(PDO::FETCH_ASSOC)]); exit; } +if ($action === 'delete_user') { if (!$isAdmin) exit; $stmt = $pdo->prepare("DELETE FROM users WHERE id = ?"); $stmt->execute([$_POST['id']]); logAudit('User gelöscht', "User ID: {$_POST['id']}"); echo json_encode(['success' => true]); exit; } +if ($action === 'create_user') { + if (!$isAdmin) exit; + $stmt = $pdo->prepare("INSERT INTO users (username, password_hash, role, permissions) VALUES (?, ?, ?, ?)"); + $stmt->execute([$_POST['username'], password_hash($_POST['password'], PASSWORD_DEFAULT), $_POST['role'], $_POST['permissions']]); + logAudit('User angelegt', "Username: {$_POST['username']} Rolle: {$_POST['role']}"); + echo json_encode(['success' => true]); exit; +} + +if ($action === 'get_pve_nodes') { + if (!$isAdmin) exit; + $stmt = $pdo->query("SELECT * FROM nodes WHERE type = 'pve'"); $resList = []; + 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']]; } } } + } + echo json_encode(['success' => true, 'data' => $resList]); exit; +} + +if ($action === 'create_vm') { + if (!$isAdmin) exit; + $stmt = $pdo->prepare("SELECT * FROM nodes WHERE id = ? AND type = 'pve'"); $stmt->execute([$_POST['node_id']]); $node = $stmt->fetch(PDO::FETCH_ASSOC); + $nextIdRes = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/cluster/nextid"); + if(!isset($nextIdRes['data'])) { echo json_encode(['success' => false, 'error' => 'Konnte keine freie VMID finden.']); exit; } + $vmid = $nextIdRes['data']; + $params = ['vmid' => $vmid, 'name' => $_POST['name'], 'memory' => $_POST['memory'], 'cores' => $_POST['cores'], 'net0' => 'virtio,bridge=vmbr0']; + $res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$_POST['host']}/qemu", "POST", $params); + logAudit('VM Erstellt', "VMID: {$vmid} Name: {$_POST['name']}"); + echo json_encode(['success' => isset($res['data']), 'vmid' => $vmid]); exit; +} + +if ($action === 'get_vm_config') { + if (!checkVmPermission($pdo, $_GET['vmid'])) exit; + $stmt = $pdo->prepare("SELECT * FROM nodes WHERE id = ?"); $stmt->execute([$_GET['node_id']]); $node = $stmt->fetch(PDO::FETCH_ASSOC); + $res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$_GET['host']}/{$_GET['type']}/{$_GET['vmid']}/config"); + echo json_encode(['success' => true, 'data' => $res['data'] ?? []]); exit; +} + +if ($action === 'update_vm_config') { + if (!checkVmPermission($pdo, $_POST['vmid'])) exit; + $stmt = $pdo->prepare("SELECT * FROM nodes WHERE id = ?"); $stmt->execute([$_POST['node_id']]); $node = $stmt->fetch(PDO::FETCH_ASSOC); + $params = []; + if(isset($_POST['memory'])) $params['memory'] = $_POST['memory']; + if(isset($_POST['cores'])) $params['cores'] = $_POST['cores']; + if(isset($_POST['net0'])) $params['net0'] = $_POST['net0']; + $res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$_POST['host']}/{$_POST['type']}/{$_POST['vmid']}/config", "POST", $params); + logAudit('VM Config geändert', "VMID: {$_POST['vmid']}"); + echo json_encode(['success' => isset($res['data'])]); exit; +} + +if ($action === 'add_vm_nic') { + if (!checkVmPermission($pdo, $_POST['vmid'])) exit; + $stmt = $pdo->prepare("SELECT * FROM nodes WHERE id = ?"); $stmt->execute([$_POST['node_id']]); $node = $stmt->fetch(PDO::FETCH_ASSOC); + $cfg = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$_POST['host']}/{$_POST['type']}/{$_POST['vmid']}/config"); + $nextNic = 0; for ($i=0; $i<10; $i++) { if (!isset($cfg['data']["net{$i}"])) { $nextNic = $i; break; } } + $res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$_POST['host']}/{$_POST['type']}/{$_POST['vmid']}/config", "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; +} + +if ($action === 'resize_vm_disk') { + if (!checkVmPermission($pdo, $_POST['vmid'])) exit; + $stmt = $pdo->prepare("SELECT * FROM nodes WHERE id = ?"); $stmt->execute([$_POST['node_id']]); $node = $stmt->fetch(PDO::FETCH_ASSOC); + $res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$_POST['host']}/{$_POST['type']}/{$_POST['vmid']}/resize", "PUT", ['disk' => $_POST['disk'], 'size' => $_POST['size']]); + logAudit('VM Disk erweitert', "VMID: {$_POST['vmid']} Disk: {$_POST['disk']} Size: {$_POST['size']}"); + echo json_encode(['success' => isset($res['data'])]); exit; +} + +if ($action === 'delete_vm') { + if (!$isAdmin) exit; + $stmt = $pdo->prepare("SELECT * FROM nodes WHERE id = ?"); $stmt->execute([$_POST['node_id']]); $node = $stmt->fetch(PDO::FETCH_ASSOC); + $res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$_POST['host']}/{$_POST['type']}/{$_POST['vmid']}", "DELETE"); + logAudit('VM Gelöscht', "VMID: {$_POST['vmid']}"); + echo json_encode(['success' => isset($res['data'])]); exit; +} + +if ($action === 'get_vm_snapshots') { + if (!checkVmPermission($pdo, $_GET['vmid'])) exit; + $stmt = $pdo->prepare("SELECT * FROM nodes WHERE id = ?"); $stmt->execute([$_GET['node_id']]); $node = $stmt->fetch(PDO::FETCH_ASSOC); + $res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$_GET['host']}/{$_GET['type']}/{$_GET['vmid']}/snapshot"); + echo json_encode(['success' => true, 'data' => $res['data'] ?? []]); exit; +} + +if ($action === 'vm_snapshot_action') { + if (!checkVmPermission($pdo, $_POST['vmid'])) exit; + $stmt = $pdo->prepare("SELECT * FROM nodes WHERE id = ?"); $stmt->execute([$_POST['node_id']]); $node = $stmt->fetch(PDO::FETCH_ASSOC); + $cmd = $_POST['cmd']; $snapname = $_POST['snapname']; + if ($cmd === 'create') { $res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$_POST['host']}/{$_POST['type']}/{$_POST['vmid']}/snapshot", "POST", ['snapname' => $snapname]); } + elseif ($cmd === 'delete') { $res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$_POST['host']}/{$_POST['type']}/{$_POST['vmid']}/snapshot/{$snapname}", "DELETE"); } + elseif ($cmd === 'rollback') { $res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$_POST['host']}/{$_POST['type']}/{$_POST['vmid']}/snapshot/{$snapname}/rollback", "POST"); } + logAudit('Snapshot ' . $cmd, "VMID: {$_POST['vmid']} Snap: {$snapname}"); + echo json_encode(['success' => isset($res['data'])]); exit; +} + +if ($action === 'get_backup_storages') { + if (!checkVmPermission($pdo, $_GET['vmid'] ?? 0)) exit; + $stmt = $pdo->prepare("SELECT * FROM nodes WHERE id = ?"); $stmt->execute([$_GET['node_id']]); $node = $stmt->fetch(PDO::FETCH_ASSOC); + $stRes = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$_GET['host']}/storage"); + $storages = []; if (isset($stRes['data'])) { foreach ($stRes['data'] as $st) { if (strpos($st['content'], 'backup') !== false) { $storages[] = $st['storage']; } } } + echo json_encode(['success' => true, 'data' => $storages]); exit; +} + +if ($action === 'get_vm_backups') { + if (!checkVmPermission($pdo, $_GET['vmid'])) exit; + $stmt = $pdo->prepare("SELECT * FROM nodes WHERE id = ?"); $stmt->execute([$_GET['node_id']]); $node = $stmt->fetch(PDO::FETCH_ASSOC); + $stRes = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$_GET['host']}/storage"); + $backups = []; + if (isset($stRes['data'])) { + foreach ($stRes['data'] as $st) { + if (strpos($st['content'], 'backup') !== false) { + $bRes = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$_GET['host']}/storage/{$st['storage']}/content?vmid={$_GET['vmid']}"); + if (isset($bRes['data'])) { foreach ($bRes['data'] as $b) { $b['storage'] = $st['storage']; $backups[] = $b; } } + } + } + } + usort($backups, function($a, $b) { return $b['ctime'] <=> $a['ctime']; }); + echo json_encode(['success' => true, 'data' => $backups]); exit; +} + +if ($action === 'create_backup') { + if (!checkVmPermission($pdo, $_POST['vmid'])) exit; + $stmt = $pdo->prepare("SELECT * FROM nodes WHERE id = ?"); $stmt->execute([$_POST['node_id']]); $node = $stmt->fetch(PDO::FETCH_ASSOC); + $res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$_POST['host']}/vzdump", "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; +} + +if ($action === 'restore_backup') { + if (!checkVmPermission($pdo, $_POST['vmid'])) exit; + $stmt = $pdo->prepare("SELECT * FROM nodes WHERE id = ?"); $stmt->execute([$_POST['node_id']]); $node = $stmt->fetch(PDO::FETCH_ASSOC); + $res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$_POST['host']}/{$_POST['type']}/{$_POST['vmid']}/status/current"); + if (isset($res['data']) && $res['data']['status'] === 'running') { echo json_encode(['success' => false, 'error' => 'VM muss gestoppt sein!']); exit; } + $resRestore = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$_POST['host']}/{$_POST['type']}", "POST", ['vmid' => $_POST['vmid'], 'archive' => $_POST['archive'], 'force' => 1]); + logAudit('Backup Restore', "VMID: {$_POST['vmid']} Archive: {$_POST['archive']}"); + echo json_encode(['success' => isset($resRestore['data'])]); exit; +} + +if ($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; +} +?> \ No newline at end of file diff --git a/src/app.js b/src/app.js new file mode 100644 index 0000000..b4982fd --- /dev/null +++ b/src/app.js @@ -0,0 +1,569 @@ +// /home/docker/pve_dashboard/src/app.js + +function formatBytes(bytes) { if (!bytes || bytes === 0) return '0 GB'; const k = 1024, sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'], i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } +function formatDate(timestamp) { if (!timestamp || timestamp === 0) return 'Nie'; const d = new Date(timestamp * 1000); return d.toLocaleDateString('de-DE') + ' ' + d.toLocaleTimeString('de-DE'); } + +if (!window.APP.isLoggedIn) { + const loginForm = document.getElementById('loginForm'); + if(loginForm) { loginForm.addEventListener('submit', async function(e) { e.preventDefault(); const btn = this.querySelector('button[type="submit"]'); const oTxt = btn.innerText; btn.innerText = 'Prüfe...'; const fd = new FormData(); fd.append('username', document.getElementById('loginUser').value); fd.append('password', document.getElementById('loginPass').value); try { const res = await (await fetch('api.php?action=login', { method: 'POST', body: fd })).json(); if(res.success) window.location.reload(); else { alert(res.error); btn.innerText = oTxt; } } catch (e) { alert('Netzwerkfehler.'); btn.innerText = oTxt; } }); } +} else if (window.APP.nodeCount === 0) { + const setupForm = document.getElementById('setupForm'); + if(setupForm) { setupForm.addEventListener('submit', async function(e) { e.preventDefault(); const btn = this.querySelector('button[type="submit"]'); const oTxt = btn.innerText; btn.innerText = 'Verbinde...'; const fd = new FormData(); fd.append('name', document.getElementById('nodeName').value); fd.append('ip', document.getElementById('nodeIp').value); fd.append('user', document.getElementById('nodeUser').value); fd.append('pass', document.getElementById('nodePass').value); fd.append('type', 'pve'); try { const res = await (await fetch('api.php?action=add_node', { method: 'POST', body: fd })).json(); if(res.success) { btn.innerText = 'Erfolgreich!'; setTimeout(() => window.location.reload(), 1000); } else { alert(res.error); btn.innerText = oTxt; } } catch (e) { alert('Netzwerkfehler.'); btn.innerText = oTxt; } }); } +} + +async function logout() { await fetch('api.php?action=logout'); window.location.reload(); } + +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')) { + const auditBtnHTML = ` Audit Log`; + userBtn.insertAdjacentHTML('afterend', auditBtnHTML); + } + } + + window.switchTab = function(tab) { + ['pve', 'pbs', 'pmg'].forEach(t => { const el = document.getElementById('tab-' + t); const nav = document.getElementById('nav-tab-' + t); if (t === tab) { el.classList.remove('hidden'); setTimeout(() => el.classList.remove('opacity-0'), 50); nav.classList.add('tab-active' + (t === 'pve' ? '' : '-' + t)); } else { el.classList.add('hidden', 'opacity-0'); nav.classList.remove('tab-active', 'tab-active-pbs', 'tab-active-pmg'); } }); + if(tab === 'pbs') fetchPbsStats(); if(tab === 'pmg') fetchPmgStats(); + } + + const ctx = document.getElementById('liveChart')?.getContext('2d'); let liveChart; + if(ctx) { liveChart = new Chart(ctx, { type: 'line', data: { labels: [], datasets: [{ label: 'CPU (%)', borderColor: '#3b82f6', backgroundColor: 'rgba(59, 130, 246, 0.1)', borderWidth: 2, tension: 0.4, fill: true, data: [] }, { label: 'RAM (%)', borderColor: '#E57000', backgroundColor: 'rgba(229, 112, 0, 0.1)', borderWidth: 2, tension: 0.4, fill: true, data: [] }] }, options: { responsive: true, maintainAspectRatio: false, animation: { duration: 500 }, scales: { x: { ticks: { color: '#9ca3af' }, grid: { color: '#33334d' } }, y: { min: 0, max: 100, ticks: { color: '#9ca3af', callback: v => v + '%' }, grid: { color: '#33334d' } } }, plugins: { legend: { labels: { color: '#e2e8f0', usePointStyle: true } } } } }); } + + 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) {} } + async function fetchTopVms() { if(!document.getElementById('top-vms-container')) return; try { const res = await (await fetch('api.php?action=get_top_vms')).json(); if(res.success && res.data) { const container = document.getElementById('top-vms-container'); container.innerHTML = ''; if(res.data.length === 0) { container.innerHTML = '

Keine aktiven VMs.

'; return; } res.data.forEach((vm, i) => { const cpuPercent = ((vm.cpu || 0) * 100).toFixed(1); const ramUsed = formatBytes(vm.mem || 0); const icon = vm.type === 'lxc' ? '📦' : '🖥️'; const numberColor = i === 0 ? 'text-red-500' : (i === 1 ? 'text-orange-400' : (i === 2 ? 'text-yellow-400' : 'text-gray-400')); container.innerHTML += `
#${i + 1}

${icon} ${vm.name}

Host: ${vm.host}

${cpuPercent}% CPU

${ramUsed} RAM

`; }); } } catch (err) {} } + async function fetchRecentJobs() { if(!document.getElementById('recent-jobs-container')) return; try { const res = await (await fetch('api.php?action=get_recent_jobs')).json(); if(res.success && res.data) { const container = document.getElementById('recent-jobs-container'); container.innerHTML = ''; if(res.data.length === 0) { container.innerHTML = '

Keine aktuellen Jobs.

'; return; } res.data.forEach(job => { const jobTypeStr = job.type || job.worker_type || 'unknown'; let statusColor = 'text-gray-400', statusIcon = '⏳', statusText = job.status || 'running...'; if(statusText.toLowerCase() === 'ok') { statusColor = 'text-green-500'; statusIcon = '✅'; } else if(statusText !== 'running...') { statusColor = 'text-red-500'; statusIcon = '❌'; } else { statusColor = 'text-blue-400'; statusIcon = '🔄'; } const date = new Date(job.starttime * 1000); const timeStr = date.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' }), dateStr = date.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' }); const isBackup = jobTypeStr.includes('sync') || jobTypeStr.includes('prune') || jobTypeStr.includes('garbage_collection') || jobTypeStr.includes('vzdump') || jobTypeStr.includes('verify'); const jobTypeColor = isBackup ? 'text-purple-400' : 'text-white'; container.innerHTML += `
${statusIcon}

${jobTypeStr}

Host: ${job.node_name}

${statusText}

${dateStr} - ${timeStr}

`; }); } } catch (err) { console.error(err); } } + async function fetchUpdates() { if(!document.getElementById('stat-updates-text')) return; try { const res = await (await fetch('api.php?action=get_updates')).json(); if(res.success) { const el = document.getElementById('stat-updates-text'), subEl = document.getElementById('stat-updates-sub'); if(res.total === 0) { el.innerText = '0 Updates'; el.className = 'text-2xl font-bold text-green-500 mt-1'; subEl.innerText = 'Alle Systeme sind aktuell.'; } else { el.innerText = res.total + ' Updates'; el.className = 'text-2xl font-bold text-red-500 mt-1'; subEl.innerText = 'Auf: ' + res.details; } } } catch (err) {} } + async function fetchPbsStats() { if(document.getElementById('tab-pbs').classList.contains('hidden')) return; try { const res = await (await fetch('api.php?action=get_pbs_stats')).json(); if(res.success) { const container = document.getElementById('pbs-datastores-container'); if(res.data.length === 0) { container.innerHTML = '
Keine PBS Server gefunden.
'; return; } container.innerHTML = ''; res.data.forEach(ds => { const total = ds.total || 0; const used = ds.used || 0; const percent = total > 0 ? ((used / total) * 100).toFixed(1) : 0; const colorClass = percent > 85 ? 'bg-red-500' : (percent > 70 ? 'bg-orange-500' : 'bg-pbs'); container.innerHTML += `

${ds.store}

PBS Host: ${ds.host}

Auslastung${percent}%
Used: ${formatBytes(used)}Total: ${formatBytes(total)}
⚙️ Verwaltung öffnen
`; }); } } catch(e) {} } + async function fetchPmgStats() { if(document.getElementById('tab-pmg').classList.contains('hidden')) return; try { const res = await (await fetch('api.php?action=get_pmg_stats')).json(); if(res.success) { const container = document.getElementById('pmg-nodes-container'); if(res.data.length === 0) { container.innerHTML = '
Keine Mail Gateways angebunden.
'; return; } container.innerHTML = ''; res.data.forEach(pmg => { const cpuPercent = ((pmg.cpu || 0) * 100).toFixed(1); const ramPercent = pmg.memory && pmg.memory.total ? ((pmg.memory.used / pmg.memory.total) * 100).toFixed(1) : 0; const diskPercent = pmg.rootfs && pmg.rootfs.total ? ((pmg.rootfs.used / pmg.rootfs.total) * 100).toFixed(1) : 0; container.innerHTML += `

${pmg.host}

Online

CPU${cpuPercent}%
RAM${ramPercent}%
System Disk${diskPercent}%
🛡️ Gateway öffnen
`; }); } } catch(e) {} } + + fetchGlobalStats(); setInterval(fetchGlobalStats, 10000); fetchTopVms(); setInterval(fetchTopVms, 10000); fetchRecentJobs(); setInterval(fetchRecentJobs, 15000); fetchUpdates(); setInterval(fetchUpdates, 60000); setInterval(fetchPbsStats, 10000); setInterval(fetchPmgStats, 10000); + + const nodeModal = document.getElementById('nodeManagerModal'); + window.openNodeManager = function() { nodeModal.classList.remove('hidden'); loadNodesIntoTable(); } + window.closeNodeManager = function() { nodeModal.classList.add('hidden'); document.getElementById('addNodeFormContainer').classList.add('hidden'); } + window.toggleAddNodeForm = function() { document.getElementById('addNodeFormContainer').classList.toggle('hidden'); } + async function loadNodesIntoTable() { try { const res = await (await fetch('api.php?action=get_nodes')).json(); if(res.success) { const tbody = document.getElementById('nodeTableBody'); tbody.innerHTML = ''; if(res.data.length === 0) { tbody.innerHTML = 'Keine Server.'; return; } res.data.forEach(node => { let typeBadge = node.type === 'pbs' ? 'PBS Backup' : (node.type === 'pmg' ? 'PMG Mail' : 'PVE Node'); tbody.innerHTML += `${node.name}${typeBadge}${node.ip_address}`; }); } } catch (err) {} } + window.deleteNode = async function(id, name) { if(!confirm(`Server '${name}' löschen?`)) return; const fd = new FormData(); fd.append('id', id); await fetch('api.php?action=delete_node', { method: 'POST', body: fd }); loadNodesIntoTable(); } + + const addNewNodeForm = document.getElementById('addNewNodeForm'); + if(addNewNodeForm) { addNewNodeForm.addEventListener('submit', async function(e) { e.preventDefault(); const btn = this.querySelector('button[type="submit"]'); const oTxt = btn.innerText; btn.innerText = 'Verbinde...'; const fd = new FormData(); fd.append('name', document.getElementById('newNodeName').value); fd.append('ip', document.getElementById('newNodeIp').value); fd.append('user', document.getElementById('newNodeUser').value); fd.append('pass', document.getElementById('newNodePass').value); fd.append('type', document.getElementById('newNodeType').value); try { const res = await (await fetch('api.php?action=add_node', { method: 'POST', body: fd })).json(); if(res.success) { addNewNodeForm.reset(); toggleAddNodeForm(); loadNodesIntoTable(); alert('Erfolgreich angebunden!'); } else alert(res.error); } catch(err) {} btn.innerText = oTxt; }); } + + const userModal = document.getElementById('userManagerModal'); + window.openUserManager = function() { userModal.classList.remove('hidden'); loadUsersIntoTable(); } + window.closeUserManager = function() { userModal.classList.add('hidden'); } + async function loadUsersIntoTable() { try { const res = await (await fetch('api.php?action=get_users')).json(); if(res.success) { const tbody = document.getElementById('userTableBody'); tbody.innerHTML = ''; res.data.forEach(user => { const isMe = user.username === window.APP.username; const deleteBtn = isMe ? `Das bist du` : ``; let roleBadge = user.role === 'admin' ? 'Admin' : (user.role === 'vm_manager' ? 'VM Manager' : 'Viewer'); tbody.innerHTML += `${user.username}${roleBadge}${deleteBtn}`; }); } } catch (err) {} } + window.handleRoleChange = async function() { const role = document.getElementById('newUserRole').value; const vmBlock = document.getElementById('vmPermissionsBlock'); if (role === 'vm_manager') { vmBlock.classList.remove('hidden'); try { const res = await (await fetch('api.php?action=get_top_vms')).json(); if (res.success && res.data) { const list = document.getElementById('vmCheckboxList'); list.innerHTML = ''; res.data.forEach(vm => { list.innerHTML += ``; }); } } catch(e) {} } else vmBlock.classList.add('hidden'); } + const addNewUserForm = document.getElementById('addNewUserForm'); + if(addNewUserForm) { addNewUserForm.addEventListener('submit', async function(e) { e.preventDefault(); const btn = this.querySelector('button[type="submit"]'); const oTxt = btn.innerText; btn.innerText = 'Speichere...'; const fd = new FormData(); fd.append('username', document.getElementById('newUsername').value); fd.append('password', document.getElementById('newUserPass').value); const role = document.getElementById('newUserRole').value; fd.append('role', role); let permissions = {}; if (role === 'vm_manager') { let allowedVms = []; document.querySelectorAll('input[name="allowed_vms[]"]:checked').forEach(cb => allowedVms.push(cb.value)); permissions.allowed_vms = allowedVms; } fd.append('permissions', JSON.stringify(permissions)); const res = await (await fetch('api.php?action=create_user', { method: 'POST', body: fd })).json(); if(res.success) { addNewUserForm.reset(); handleRoleChange(); loadUsersIntoTable(); } else alert('Fehler.'); btn.innerText = oTxt; }); } + window.deleteUser = async function(id, name) { if(!confirm(`Benutzer '${name}' löschen?`)) return; const fd = new FormData(); fd.append('id', id); await fetch('api.php?action=delete_user', { method: 'POST', body: fd }); loadUsersIntoTable(); } + + const vmModal = document.getElementById('vmManagerModal'); + window.openVmManager = function() { vmModal.classList.remove('hidden'); loadVmsIntoTable(); } + window.closeVmManager = function() { vmModal.classList.add('hidden'); } + async function loadVmsIntoTable() { + const tbody = document.getElementById('vmTableBody'); tbody.innerHTML = 'Lade VMs...'; + try { + const res = await (await fetch('api.php?action=get_all_vms')).json(); + if(res.success) { + tbody.innerHTML = ''; if(res.data.length === 0) { tbody.innerHTML = 'Keine VMs.'; return; } + res.data.forEach(vm => { + const isRunning = vm.status === 'running'; const typeStr = vm.type === 'lxc' ? '📦 LXC' : '🖥️ QEMU'; + const statusBadge = isRunning ? ' Running' : ' Stopped'; + let actionButtons = isRunning ? `` : ``; + if(isRunning) { + actionButtons += ``; + actionButtons += ``; + } + actionButtons += ``; + tbody.innerHTML += `
${vm.vmid}${vm.name}
${typeStr} - ${vm.host}${vm.maxcpu||1} Cores / ${formatBytes(vm.maxmem||0)}${statusBadge}${actionButtons}`; + }); + } + } catch (err) {} + } + window.sendVmCommand = async function(vmid, host, type, cmd, nodeId) { if(!confirm(`Maschine '${vmid}' wirklich ${cmd}?`)) return; const fd = new FormData(); fd.append('vmid', vmid); fd.append('host', host); fd.append('type', type); fd.append('cmd', cmd); fd.append('node_id', nodeId); document.getElementById('vmTableBody').style.opacity = '0.5'; try { const res = await (await fetch('api.php?action=vm_action', { method: 'POST', body: fd })).json(); if(res.success) setTimeout(loadVmsIntoTable, 2000); else alert(res.error); } catch (err) {} finally { document.getElementById('vmTableBody').style.opacity = '1'; } } + + const createVmModal = document.getElementById('createVmModal'); + window.openCreateVm = async function() { createVmModal.classList.remove('hidden'); const select = document.getElementById('createVmHost'); select.innerHTML = ''; try { const res = await (await fetch('api.php?action=get_pve_nodes')).json(); if (res.success && res.data.length > 0) { select.innerHTML = ''; res.data.forEach(node => { select.innerHTML += ``; }); } else { select.innerHTML = ''; } } catch(e) {} } + window.closeCreateVm = function() { createVmModal.classList.add('hidden'); } + const createVmForm = document.getElementById('createVmForm'); + if(createVmForm) { createVmForm.addEventListener('submit', async function(e) { e.preventDefault(); const hostDataStr = document.getElementById('createVmHost').value; if(!hostDataStr) return; const hostData = JSON.parse(hostDataStr); const btn = this.querySelector('button[type="submit"]'); const oTxt = btn.innerText; btn.innerText = 'Richte VM ein...'; const fd = new FormData(); fd.append('node_id', hostData.id); fd.append('host', hostData.host); fd.append('name', document.getElementById('createVmName').value); fd.append('memory', document.getElementById('createVmRam').value); fd.append('cores', document.getElementById('createVmCores').value); try { const res = await (await fetch('api.php?action=create_vm', { method: 'POST', body: fd })).json(); if(res.success) { alert(`Erfolgreich! VM-ID: ${res.vmid}`); createVmForm.reset(); closeCreateVm(); loadVmsIntoTable(); } else alert(res.error); } catch(e) {} finally { btn.innerText = oTxt; } }); } + + const settingsModal = document.getElementById('vmSettingsModal'); + window.openVmSettings = async function(vmid, host, type, nodeId, name) { + settingsModal.classList.remove('hidden'); document.getElementById('settingsModalTitle').innerText = 'Einstellungen: ' + name; + document.getElementById('setVmid').value = vmid; document.getElementById('setHost').value = host; document.getElementById('setType').value = type; document.getElementById('setNodeId').value = nodeId; + document.getElementById('snapshotListContainer').innerHTML = '
Lade Snapshots...
'; + document.getElementById('backupListContainer').innerHTML = '
Lade Backups...
'; + try { + const res = await (await fetch(`api.php?action=get_vm_config&vmid=${vmid}&host=${host}&type=${type}&node_id=${nodeId}`)).json(); + if (res.success && res.data) { const cfg = res.data; document.getElementById('setMemory').value = cfg.memory || ''; document.getElementById('setCores').value = cfg.cores || 1; if (cfg.net0) { document.getElementById('setRawNet0').value = cfg.net0; if (cfg.net0.includes('link_down=1')) { document.getElementById('netStatusBadge').className = 'px-2 py-1 rounded text-xs font-bold text-red-400 bg-red-500/20'; document.getElementById('netStatusBadge').innerText = 'Getrennt'; document.getElementById('btnToggleNet').innerText = 'Kabel einstecken'; document.getElementById('btnToggleNet').onclick = () => saveNetwork(false); } else { document.getElementById('netStatusBadge').className = 'px-2 py-1 rounded text-xs font-bold text-green-400 bg-green-500/20'; document.getElementById('netStatusBadge').innerText = 'Verbunden'; document.getElementById('btnToggleNet').innerText = 'Kabel ziehen'; document.getElementById('btnToggleNet').onclick = () => saveNetwork(true); } } else { document.getElementById('netStatusBadge').innerText = 'Kein net0'; document.getElementById('btnToggleNet').style.display = 'none'; } let diskName = ''; if (type === 'lxc' && cfg.rootfs) diskName = 'rootfs'; else if (cfg.scsi0) diskName = 'scsi0'; else if (cfg.virtio0) diskName = 'virtio0'; else if (cfg.ide0) diskName = 'ide0'; if (diskName) { document.getElementById('setPrimaryDisk').value = diskName; document.getElementById('diskLabelName').innerText = diskName; } else document.getElementById('diskLabelName').innerText = 'Nicht gefunden'; } + loadSnapshots(vmid, host, type, nodeId); + const stRes = await (await fetch(`api.php?action=get_backup_storages&node_id=${nodeId}&host=${host}`)).json(); + if (stRes.success) { const bSelect = document.getElementById('backupTargetStorage'); bSelect.innerHTML = ''; stRes.data.forEach(st => bSelect.innerHTML += ``); } + loadBackups(vmid, host, nodeId); + } catch (e) {} + } + window.closeVmSettings = function() { settingsModal.classList.add('hidden'); } + window.saveHardwareSettings = async function() { const fd = new FormData(); fd.append('vmid', document.getElementById('setVmid').value); fd.append('host', document.getElementById('setHost').value); fd.append('type', document.getElementById('setType').value); fd.append('node_id', document.getElementById('setNodeId').value); fd.append('memory', document.getElementById('setMemory').value); fd.append('cores', document.getElementById('setCores').value); const res = await (await fetch('api.php?action=update_vm_config', { method: 'POST', body: fd })).json(); if(res.success) { alert('Gespeichert!'); loadVmsIntoTable(); } else alert(res.error); } + window.saveNetwork = async function(disconnect) { let net = document.getElementById('setRawNet0').value; if (!net) return; if (disconnect) { if (!net.includes('link_down=1')) net += ',link_down=1'; } else { net = net.replace(',link_down=1', ''); } const fd = new FormData(); fd.append('vmid', document.getElementById('setVmid').value); fd.append('host', document.getElementById('setHost').value); fd.append('type', document.getElementById('setType').value); fd.append('node_id', document.getElementById('setNodeId').value); fd.append('net0', net); const res = await (await fetch('api.php?action=update_vm_config', { method: 'POST', body: fd })).json(); if(res.success) { alert('Netzwerkstatus geändert!'); openVmSettings(fd.get('vmid'), fd.get('host'), fd.get('type'), fd.get('node_id'), document.getElementById('settingsModalTitle').innerText.replace('Einstellungen: ', '')); } } + window.addNic = async function() { const bridge = document.getElementById('addNicBridge').value.trim(); if (!bridge) return; const fd = new FormData(); fd.append('vmid', document.getElementById('setVmid').value); fd.append('host', document.getElementById('setHost').value); fd.append('type', document.getElementById('setType').value); fd.append('node_id', document.getElementById('setNodeId').value); fd.append('bridge', bridge); try { const res = await (await fetch('api.php?action=add_vm_nic', { method: 'POST', body: fd })).json(); if(res.success) { alert(`Erfolgreich in Slot: ${res.slot}`); openVmSettings(fd.get('vmid'), fd.get('host'), fd.get('type'), fd.get('node_id'), document.getElementById('settingsModalTitle').innerText.replace('Einstellungen: ', '')); } else alert(res.error); } catch(e) {} } + window.expandDisk = async function() { const gb = document.getElementById('addDiskGb').value; const disk = document.getElementById('setPrimaryDisk').value; if (!gb || gb <= 0 || !disk) return; if (!confirm(`Disk um +${gb}GB erweitern?`)) return; const fd = new FormData(); fd.append('vmid', document.getElementById('setVmid').value); fd.append('host', document.getElementById('setHost').value); fd.append('type', document.getElementById('setType').value); fd.append('node_id', document.getElementById('setNodeId').value); fd.append('disk', disk); fd.append('size', '+' + gb + 'G'); const res = await (await fetch('api.php?action=resize_vm_disk', { method: 'POST', body: fd })).json(); if(res.success) { alert('Erweitert.'); document.getElementById('addDiskGb').value = ''; } else alert(res.error); } + window.destroyVm = async function() { const vmid = document.getElementById('setVmid').value; if (prompt(`Zahl ${vmid} eingeben zum Löschen:`) !== vmid) return; const fd = new FormData(); fd.append('vmid', vmid); fd.append('host', document.getElementById('setHost').value); fd.append('type', document.getElementById('setType').value); fd.append('node_id', document.getElementById('setNodeId').value); try { const res = await (await fetch('api.php?action=delete_vm', { method: 'POST', body: fd })).json(); if(res.success) { alert('Gelöscht.'); closeVmSettings(); loadVmsIntoTable(); } else alert(res.error); } catch(e) {} } + + async function loadSnapshots(vmid, host, type, nodeId) { + try { + const res = await (await fetch(`api.php?action=get_vm_snapshots&vmid=${vmid}&host=${host}&type=${type}&node_id=${nodeId}`)).json(); + const container = document.getElementById('snapshotListContainer'); + if (res.success && res.data) { + const snaps = res.data.filter(s => s.name !== 'current'); + if(snaps.length === 0) { container.innerHTML = '

Keine Snapshots vorhanden.

'; return; } + container.innerHTML = ''; + snaps.forEach(s => { + const timeStr = s.snaptime ? formatDate(s.snaptime) : 'Unbekannt'; + container.innerHTML += `

${s.name}

${timeStr}

`; + }); + } + } catch(e) {} + } + window.createSnapshot = async function() { const snapname = document.getElementById('newSnapName').value.trim(); if(!snapname || !snapname.match(/^[a-zA-Z0-9_-]+$/)) return alert('Bitte nur Buchstaben und Zahlen!'); actionSnapshot('create', snapname); } + window.actionSnapshot = async function(cmd, snapname) { + if(cmd === 'rollback' && !confirm(`VM wird auf '${snapname}' zurückgesetzt. Fortfahren?`)) return; + if(cmd === 'delete' && !confirm(`Snapshot '${snapname}' löschen?`)) return; + const fd = new FormData(); fd.append('vmid', document.getElementById('setVmid').value); fd.append('host', document.getElementById('setHost').value); fd.append('type', document.getElementById('setType').value); fd.append('node_id', document.getElementById('setNodeId').value); fd.append('cmd', cmd); fd.append('snapname', snapname); + try { const res = await (await fetch('api.php?action=vm_snapshot_action', { method: 'POST', body: fd })).json(); if(res.success) { alert('Aktion erfolgreich!'); document.getElementById('newSnapName').value = ''; loadSnapshots(fd.get('vmid'), fd.get('host'), fd.get('type'), fd.get('node_id')); } else alert('Fehler.'); } catch(e) {} + } + + async function loadBackups(vmid, host, nodeId) { + try { + const res = await (await fetch(`api.php?action=get_vm_backups&vmid=${vmid}&host=${host}&node_id=${nodeId}`)).json(); + const container = document.getElementById('backupListContainer'); + if (res.success && res.data) { + if(res.data.length === 0) { container.innerHTML = '

Keine Backups gefunden.

'; return; } + container.innerHTML = ''; + res.data.forEach(b => { + const timeStr = b.ctime ? formatDate(b.ctime) : 'Unbekannt'; const sizeStr = formatBytes(b.size); + container.innerHTML += `

${b.volid}

${timeStr} | ${sizeStr} | Storage: ${b.storage}

Restore ⤵
`; + }); + } + } catch(e) {} + } + window.createBackup = async function() { const storage = document.getElementById('backupTargetStorage').value; if(!storage) return alert('Storage auswählen!'); const fd = new FormData(); fd.append('vmid', document.getElementById('setVmid').value); fd.append('host', document.getElementById('setHost').value); fd.append('node_id', document.getElementById('setNodeId').value); fd.append('storage', storage); try { const res = await (await fetch('api.php?action=create_backup', { method: 'POST', body: fd })).json(); if(res.success) { alert('Backup gestartet! Siehe Letzte Jobs.'); } else alert('Fehler.'); } catch(e) {} } + window.restoreBackup = async function(archive) { + if(!confirm(`⚠️ GEFAHRENZONE ⚠️\nSicher, dass du das Archiv '${archive}' wiederherstellen willst?\nDie VM MUSS dafür GESTOPPT sein!`)) return; + const fd = new FormData(); fd.append('vmid', document.getElementById('setVmid').value); fd.append('host', document.getElementById('setHost').value); fd.append('type', document.getElementById('setType').value); fd.append('node_id', document.getElementById('setNodeId').value); fd.append('archive', archive); + try { const res = await (await fetch('api.php?action=restore_backup', { method: 'POST', body: fd })).json(); if(res.success) { alert('Restore-Task erfolgreich!'); closeVmSettings(); } else alert('Fehler: Ist die VM gestoppt?'); } catch(e) {} + } + + const topologyModal = document.getElementById('nodeTopologyModal'); + window.openNodeTopology = async function() { + topologyModal.classList.remove('hidden'); const container = document.getElementById('nodeTopologyContainer'); container.innerHTML = '
Lade Cluster-Daten...
'; + try { + const [nodesRes, vmsRes] = await Promise.all([ fetch('api.php?action=get_nodes').then(r => r.json()), fetch('api.php?action=get_all_vms').then(r => r.json()) ]); + if(nodesRes.success && vmsRes.success) { + container.innerHTML = ''; const pveNodes = nodesRes.data.filter(n => n.type === 'pve'); if(pveNodes.length === 0) return; + pveNodes.forEach(node => { + const nodeVms = vmsRes.data.filter(v => v.node_id == node.id); let vmsHtml = ''; + if(nodeVms.length > 0) { + nodeVms.sort((a, b) => { if(a.status === 'running' && b.status !== 'running') return -1; if(a.status !== 'running' && b.status === 'running') return 1; return a.name.localeCompare(b.name); }); + nodeVms.forEach(vm => { + const icon = vm.type === 'lxc' ? '📦' : '🖥️'; const isRunning = vm.status === 'running'; const statusColor = isRunning ? 'border-green-500/30 bg-green-500/10 text-green-400' : 'border-darkborder bg-darkcard text-gray-400'; const dot = isRunning ? '' : ''; + vmsHtml += `
${dot}${icon} ${vm.name}
ID: ${vm.vmid}${vm.maxcpu || 1}C / ${formatBytes(vm.maxmem || 0)}
`; + }); + } + container.innerHTML += `

${node.name}

${node.ip_address}
${vmsHtml}
`; + }); + } + } catch(e) {} + } + window.closeNodeTopology = function() { topologyModal.classList.add('hidden'); } + + let perfChartObj = null; let netChartObj = null; let liveInterval = null; + window.openLiveGraph = function(targetMode, vmid, host, nodeId, name) { + document.getElementById('liveGraphModal').classList.remove('hidden'); document.getElementById('graphModalTitle').innerText = targetMode === 'node' ? `Live Performance: Node ${name}` : `Live Performance: VM ${name} (${vmid})`; + if(perfChartObj) perfChartObj.destroy(); if(netChartObj) netChartObj.destroy(); + const ctxPerf = document.getElementById('perfChart').getContext('2d'); perfChartObj = new Chart(ctxPerf, { type: 'line', data: { labels: [], datasets: [ { label: 'CPU (%)', borderColor: '#3b82f6', backgroundColor: 'rgba(59, 130, 246, 0.1)', borderWidth: 2, tension: 0.4, fill: true, data: [] }, { label: 'RAM (%)', borderColor: '#E57000', backgroundColor: 'rgba(229, 112, 0, 0.1)', borderWidth: 2, tension: 0.4, fill: true, data: [] }]}, options: { responsive: true, maintainAspectRatio: false, animation: { duration: 0 }, scales: { x: { grid: { color: '#33334d' }, ticks: { color: '#9ca3af' } }, y: { min: 0, max: 100, grid: { color: '#33334d' }, ticks: { color: '#9ca3af' } } }, plugins: { legend: { labels: { color: '#e2e8f0' } } } } }); + const ctxNet = document.getElementById('netChart').getContext('2d'); netChartObj = new Chart(ctxNet, { type: 'line', data: { labels: [], datasets: [ { label: 'RX (MB/s)', borderColor: '#10b981', backgroundColor: 'rgba(16, 185, 129, 0.1)', borderWidth: 2, tension: 0.4, fill: true, data: [] }, { label: 'TX (MB/s)', borderColor: '#8b5cf6', backgroundColor: 'rgba(139, 92, 246, 0.1)', borderWidth: 2, tension: 0.4, fill: true, data: [] }]}, options: { responsive: true, maintainAspectRatio: false, animation: { duration: 0 }, scales: { x: { grid: { color: '#33334d' }, ticks: { color: '#9ca3af' } }, y: { min: 0, grid: { color: '#33334d' }, ticks: { color: '#9ca3af' } } }, plugins: { legend: { labels: { color: '#e2e8f0' } } } } }); + if(liveInterval) clearInterval(liveInterval); + let prevNetIn = 0; let prevNetOut = 0; let prevTime = null; + const fetchLiveData = async () => { + try { + const endpoint = targetMode === 'node' ? `api.php?action=get_node_status&host=${host}&node_id=${nodeId}` : `api.php?action=get_vm_status&vmid=${vmid}&host=${host}&type=${targetMode}&node_id=${nodeId}`; + const res = await (await fetch(endpoint)).json(); + if(res.success && res.data) { + const d = res.data; const now = Date.now(); + let cpuRaw = 0; if (d.cpu !== undefined) cpuRaw = d.cpu; else if (d.cpuinfo && d.cpuinfo.cpus) cpuRaw = 0; const cpu = (cpuRaw * 100).toFixed(1); + let ram = 0; if (d.maxmem && d.maxmem > 0) { ram = ((d.mem / d.maxmem) * 100).toFixed(1); } else if (d.memory && d.memory.total > 0) { ram = ((d.memory.used / d.memory.total) * 100).toFixed(1); } + let currentNetIn = d.netin || 0; let currentNetOut = d.netout || 0; let rxSpeed = 0; let txSpeed = 0; + if(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(); } + } + } catch(e) {} + }; + fetchLiveData(); liveInterval = setInterval(fetchLiveData, 2000); + } + window.closeLiveGraph = function() { document.getElementById('liveGraphModal').classList.add('hidden'); if(liveInterval) clearInterval(liveInterval); } + + window.openVncConsole = function(ip, node, type, vmid, name) { + const cType = type === 'qemu' ? 'kvm' : 'lxc'; + const url = `https://${ip}:8006/?console=${cType}&novnc=1&vmid=${vmid}&vmname=${name}&node=${node}`; + window.open(url, `VNC_${vmid}`, "width=1024,height=768,menubar=no,toolbar=no,location=no,status=no,resizable=yes,scrollbars=yes"); + } + window.closeVncConsole = function() {} + + window.openPbsDatastore = async function(nodeId, storeName) { + if (!nodeId) return alert('Node ID fehlt. Bitte Seite neu laden (F5).'); + document.getElementById('pbsDatastoreModal').classList.remove('hidden'); + document.getElementById('pbsModalTitle').innerText = 'Datastore: ' + storeName; + document.getElementById('pbsNodeId').value = nodeId; + document.getElementById('pbsStoreName').value = storeName; + loadPbsBackups(nodeId, storeName); + loadPbsJobs(nodeId, storeName); + loadPbsSyncJobs(nodeId); + } + window.closePbsDatastore = function() { document.getElementById('pbsDatastoreModal').classList.add('hidden'); } + + async function loadPbsBackups(nodeId, storeName) { + const container = document.getElementById('pbsBackupsContainer'); container.innerHTML = '

Durchsuche Namespaces nach Backups...

'; + try { + const res = await (await fetch(`api.php?action=pbs_get_datastore_content&node_id=${nodeId}&store=${storeName}`)).json(); + if(res.success && res.data) { + if(res.data.length === 0) { container.innerHTML = '

Keine Backups vorhanden.

'; return; } + container.innerHTML = ''; + res.data.sort((a,b) => b['backup-time'] - a['backup-time']).forEach(b => { + const timeStr = formatDate(b['backup-time']); const sizeStr = formatBytes(b.size); + const nsLabel = b.ns ? `[${b.ns}]` : `[Root]`; + container.innerHTML += ` +
+

${b['backup-type']} / ${b['backup-id']} ${nsLabel}

${timeStr} | ${sizeStr}

+ +
`; + }); + } + } catch(e) { container.innerHTML = 'Fehler beim Laden.'; } + } + window.deletePbsSnapshot = async function(btype, bid, btime, ns) { + if(!confirm(`Backup ${btype}/${bid} wirklich unwiderruflich löschen?`)) return; + const fd = new FormData(); fd.append('node_id', document.getElementById('pbsNodeId').value); fd.append('store', document.getElementById('pbsStoreName').value); fd.append('btype', btype); fd.append('bid', bid); fd.append('btime', btime); fd.append('ns', ns); + const res = await (await fetch('api.php?action=pbs_delete_snapshot', {method: 'POST', body: fd})).json(); + if(res.success) { loadPbsBackups(document.getElementById('pbsNodeId').value, document.getElementById('pbsStoreName').value); } else alert('Fehler beim Löschen.'); + } + + async function loadPbsJobs(nodeId, storeName) { + const mixCont = document.getElementById('pbsVerifyGcContainer'); mixCont.innerHTML = '

Lade System-Jobs...

'; + const table = document.getElementById('pbsJobsTable'); table.innerHTML = 'Lade Historie...'; + try { + const res = await (await fetch(`api.php?action=pbs_get_jobs&node_id=${nodeId}`)).json(); + if(res.success && res.data) { + mixCont.innerHTML = ''; table.innerHTML = ''; let mixCount = 0; + res.data.sort((a,b) => b.starttime - a.starttime).forEach(job => { + const statusColor = job.status === 'OK' ? 'text-green-500' : (job.status ? 'text-red-500' : 'text-blue-400'); + const timeStr = formatDate(job.starttime); + const runtime = job.endtime ? (Math.round(job.endtime - job.starttime) + 's') : 'Running...'; + const isSystemJob = job.worker_type === 'verify' || job.worker_type === 'garbage_collection' || job.worker_type === 'prune'; + if (isSystemJob && mixCount < 6) { + mixCont.innerHTML += `

${job.worker_type}

${timeStr}

${job.status || 'Active'}
`; + mixCount++; + } + table.innerHTML += `${timeStr}${job.worker_type}${job.status || 'Active'}${runtime}`; + }); + if(mixCount === 0) mixCont.innerHTML = '

Keine System-Jobs gefunden.

'; + if(res.data.length === 0) table.innerHTML = 'Keine Historie gefunden.'; + } + } catch(e) {} + } + + async function loadPbsSyncJobs(nodeId) { + const container = document.getElementById('pbsSyncContainer'); container.innerHTML = '

Lade Sync-Jobs...

'; + try { + const res = await (await fetch(`api.php?action=pbs_get_sync_jobs&node_id=${nodeId}`)).json(); + if(res.success && res.data) { + if(res.data.length === 0) { container.innerHTML = '

Keine Sync-Jobs eingerichtet.

'; return; } + container.innerHTML = ''; + res.data.forEach(sync => { + container.innerHTML += `

${sync.id}

Quelle: ${sync.remote || 'Lokal'} -> ${sync['remote-store']}

Ziel: ${sync.store}

Zeitplan: ${sync.schedule || 'Manuell'}
`; + }); + } + } catch(e) { container.innerHTML = 'Fehler.'; } + } + + window.openTaskLog = async function(upid, nodeId, host) { + document.getElementById('taskLogModal').classList.remove('hidden'); + document.getElementById('taskLogContent').innerText = 'Lade Log vom Server...'; + try { + const res = await (await fetch(`api.php?action=get_task_log&node_id=${nodeId}&upid=${encodeURIComponent(upid)}&host=${host}`)).json(); + if(res.success && res.data) { + let logText = ''; res.data.forEach(line => logText += line.t + '\n'); + document.getElementById('taskLogContent').innerText = logText || 'Log ist leer.'; + } else { document.getElementById('taskLogContent').innerText = 'Fehler beim Laden des Logs.'; } + } catch(e) {} + } + window.closeTaskLog = function() { document.getElementById('taskLogModal').classList.add('hidden'); } + + window.openPmgManager = function(nodeId, hostName, internalName) { + document.getElementById('pmgManagerModal').classList.remove('hidden'); + document.getElementById('pmgModalTitle').innerText = 'Mail Gateway: ' + hostName; + document.getElementById('pmgNodeId').value = nodeId; + document.getElementById('pmgHostName').value = hostName; + document.getElementById('pmgInternalName').value = internalName; + switchPmgTab('spam'); + } + window.closePmgManager = function() { document.getElementById('pmgManagerModal').classList.add('hidden'); } + + window.switchPmgTab = function(tabName) { + ['spam', 'tracking', 'queues', 'settings'].forEach(t => { + const btn = document.getElementById('btn-pmg-' + t); + const content = document.getElementById('pmg-tab-' + t); + if(btn && content) { + if (t === tabName) { + btn.classList.add('border-pmg', 'text-white'); btn.classList.remove('border-transparent', 'text-gray-400'); + content.classList.remove('hidden'); + } else { + btn.classList.remove('border-pmg', 'text-white'); btn.classList.add('border-transparent', 'text-gray-400'); + content.classList.add('hidden'); + } + } + }); + const nodeId = document.getElementById('pmgNodeId').value; + if (!nodeId) return; + if(tabName === 'spam') loadPmgSpamQueue(nodeId); + if(tabName === 'tracking') loadPmgTracking(); + if(tabName === 'queues') loadPmgQueues(); + if(tabName === 'settings') { loadPmgConfig(nodeId); loadPmgSsl(nodeId); loadPmgDomains(nodeId); loadPmgNetworks(nodeId); } + } + + window.loadPmgSpamQueue = async function(nodeId) { + const table = document.getElementById('pmgSpamTable'); + table.innerHTML = 'Lade Spam Quarantäne...'; + try { + const res = await (await fetch(`api.php?action=pmg_get_spam_queue&node_id=${nodeId}`)).json(); + if(res.success && res.data) { + table.innerHTML = ''; + if(res.data.length === 0) { table.innerHTML = '🎉 Keine Mails in der Quarantäne.'; return; } + res.data.sort((a,b) => b.time - a.time).forEach(mail => { + const timeStr = formatDate(mail.time); const subject = mail.subject || 'Kein Betreff'; const sender = mail.sender || 'Unbekannt'; const receiver = mail.receiver || 'Unbekannt'; const score = mail.spamlevel || 0; + table.innerHTML += `${timeStr}${sender}${receiver}${subject}${score}`; + }); + } else { table.innerHTML = 'Fehler beim Laden.'; } + } catch(e) { table.innerHTML = 'Netzwerkfehler.'; } + } + window.pmgSpamAction = async function(actionType, mailId) { + if(actionType === 'delete' && !confirm('Mail unwiderruflich löschen?')) return; + if(actionType === 'deliver' && !confirm('Soll diese Mail an den Empfänger zugestellt werden?')) return; + const fd = new FormData(); fd.append('node_id', document.getElementById('pmgNodeId').value); fd.append('pmg_action', actionType); fd.append('mailid', mailId); + try { + const res = await (await fetch('api.php?action=pmg_quarantine_action', {method: 'POST', body: fd})).json(); + if(res.success) loadPmgSpamQueue(document.getElementById('pmgNodeId').value); else alert('Aktion fehlgeschlagen.'); + } catch(e) { alert('Netzwerkfehler'); } + } + + window.loadPmgTracking = async function() { + const nodeId = document.getElementById('pmgNodeId').value; const internalName = document.getElementById('pmgInternalName').value; const filter = document.getElementById('pmgTrackingFilter').value.trim(); const table = document.getElementById('pmgTrackingTable'); + table.innerHTML = 'Lese Syslog (Letzte 24h)...'; + try { + const res = await (await fetch(`api.php?action=pmg_get_tracking&node_id=${nodeId}&internal_name=${internalName}&filter=${encodeURIComponent(filter)}`)).json(); + if(res.success && res.data) { + table.innerHTML = ''; + if(res.data.length === 0) { table.innerHTML = `Keine Logs für diesen Filter gefunden.`; return; } + res.data.forEach(log => { + const timestamp = log.time || log.timestamp || 0; const timeStr = timestamp ? formatDate(timestamp) : 'Unbekannt'; const sender = log.from || log.sender || '-'; const receiver = log.to || log.receiver || '-'; + let statusTxt = 'Unbekannt'; let statusColor = 'text-gray-400'; + if (log.dstatus) { + if (log.dstatus === 'A') { statusTxt = 'Delivered / Accept'; statusColor = 'text-green-500'; } else if (log.dstatus === 'N') { statusTxt = 'Rejected / Blocked'; statusColor = 'text-red-500'; } else if (log.dstatus === 'Q') { statusTxt = 'Quarantined'; statusColor = 'text-orange-500'; } else if (log.dstatus === 'B') { statusTxt = 'Bounced'; statusColor = 'text-red-500'; } else { statusTxt = log.dstatus; statusColor = 'text-blue-400'; } + } else if (log.relay) { statusTxt = 'Relayed (' + log.relay.split('[')[0] + ')'; statusColor = 'text-blue-400'; } else if (log.msgid) { statusTxt = 'In Processing (' + log.msgid + ')'; statusColor = 'text-gray-400'; } else if (log.client) { statusTxt = 'Connection from ' + log.client.split('[')[0]; statusColor = 'text-gray-500'; } else if (log.id && log.id.length > 5) { statusTxt = 'Queue ID: ' + log.id; statusColor = 'text-gray-400'; } else { statusTxt = JSON.stringify(log).substring(0, 30); } + table.innerHTML += `${timeStr}${sender}${receiver}${statusTxt}`; + }); + } else { table.innerHTML = 'Syslog konnte nicht gelesen werden.'; } + } catch(e) { table.innerHTML = 'Netzwerkfehler.'; } + } + + window.loadPmgQueues = async function() { + const nodeId = document.getElementById('pmgNodeId').value; const internalName = document.getElementById('pmgInternalName').value; const table = document.getElementById('pmgQueueTable'); const summary = document.getElementById('pmgQueuesSummary'); + table.innerHTML = 'Lese Warteschlangen aus Postfix...'; summary.innerHTML = '
Lade...
'; + try { + const res = await (await fetch(`api.php?action=pmg_get_queues&node_id=${nodeId}&internal_name=${internalName}`)).json(); + if(res.success && res.data) { + let activeCount = 0, deferCount = 0, holdCount = 0; + res.data.forEach(q => { const queueName = q.queue_name || q.queue || ''; if(queueName === 'active') activeCount++; else if(queueName === 'deferred') deferCount++; else holdCount++; }); + summary.innerHTML = `

Active Queue

${activeCount}

Deferred (Greylisted/Wartend)

${deferCount}

Other / Hold

${holdCount}

`; + table.innerHTML = ''; + if(res.data.length === 0) { table.innerHTML = `Alle Postfix-Queues sind komplett leer.`; return; } + res.data.forEach(q => { + const queueName = q.queue_name || q.queue || 'unknown'; const timestamp = q.arrival_time || q.time || 0; const timeStr = timestamp ? formatDate(timestamp) : 'Unbekannt'; const sender = q.sender || '-'; let receiver = q.receiver || q.recipients || '-'; if (Array.isArray(receiver)) receiver = receiver[0]; const reason = q.reason || q.error || '-'; const qColor = queueName === 'deferred' ? 'text-orange-400' : (queueName === 'active' ? 'text-blue-400' : 'text-gray-400'); + table.innerHTML += `${queueName}${timeStr}${sender}${receiver}${reason}`; + }); + } else { table.innerHTML = 'Queues konnten nicht gelesen werden.'; } + } catch(e) { table.innerHTML = 'Netzwerkfehler.'; } + } + + window.loadPmgConfig = async function(nodeId) { + try { + const res = await (await fetch(`api.php?action=pmg_get_config&node_id=${nodeId}`)).json(); + if(res.success && res.data) { + document.getElementById('pmgCfgRelay').value = res.data.relay || ''; + document.getElementById('pmgCfgExtPort').value = res.data.ext_port || 25; + document.getElementById('pmgCfgIntPort').value = res.data.int_port || 26; + document.getElementById('pmgCfgTls').value = res.data.tls || 0; + document.getElementById('pmgCfgTlsLog').value = res.data.tlslog || 0; + } + } catch(e) {} + } + window.savePmgConfig = async function() { + const btn = event.target; const oTxt = btn.innerText; btn.innerText = 'Speichere...'; + const fd = new FormData(); fd.append('node_id', document.getElementById('pmgNodeId').value); fd.append('relay', document.getElementById('pmgCfgRelay').value); fd.append('ext_port', document.getElementById('pmgCfgExtPort').value); fd.append('int_port', document.getElementById('pmgCfgIntPort').value); fd.append('tls', document.getElementById('pmgCfgTls').value); fd.append('tlslog', document.getElementById('pmgCfgTlsLog').value); + try { const res = await (await fetch('api.php?action=pmg_set_config', {method: 'POST', body: fd})).json(); if(res.success) { alert('Proxy Einstellungen gespeichert!'); } else { alert('Fehler beim Speichern.'); } } catch(e) { alert('Netzwerkfehler.'); } finally { btn.innerText = oTxt; } + } + + window.loadPmgDomains = async function(nodeId) { + const container = document.getElementById('pmgDomainsContainer'); container.innerHTML = '

Lade...

'; + try { + const res = await (await fetch(`api.php?action=pmg_get_domains&node_id=${nodeId}`)).json(); + if(res.success && res.data) { + if(res.data.length === 0) { container.innerHTML = '

Keine Domains.

'; return; } + container.innerHTML = ''; + res.data.forEach(d => { container.innerHTML += `
${d.domain}
`; }); + } else { container.innerHTML = '

Fehler.

'; } + } catch(e) {} + } + window.addPmgDomain = async function() { + const domain = document.getElementById('pmgNewDomain').value.trim(); if(!domain) return; + const fd = new FormData(); fd.append('node_id', document.getElementById('pmgNodeId').value); fd.append('domain', domain); + const res = await (await fetch('api.php?action=pmg_add_domain', {method: 'POST', body: fd})).json(); + if(res.success) { document.getElementById('pmgNewDomain').value = ''; loadPmgDomains(document.getElementById('pmgNodeId').value); } else alert('Fehler.'); + } + window.deletePmgDomain = async function(domain) { + if(!confirm(`Domain ${domain} löschen?`)) return; + const fd = new FormData(); fd.append('node_id', document.getElementById('pmgNodeId').value); fd.append('domain', domain); + const res = await (await fetch('api.php?action=pmg_delete_domain', {method: 'POST', body: fd})).json(); + if(res.success) loadPmgDomains(document.getElementById('pmgNodeId').value); else alert('Fehler.'); + } + + window.loadPmgNetworks = async function(nodeId) { + const container = document.getElementById('pmgNetworksContainer'); container.innerHTML = '

Lade...

'; + try { + const res = await (await fetch(`api.php?action=pmg_get_networks&node_id=${nodeId}`)).json(); + if(res.success && res.data) { + if(res.data.length === 0) { container.innerHTML = '

Keine Netzwerke.

'; return; } + container.innerHTML = ''; + res.data.forEach(n => { container.innerHTML += `
${n.cidr}
`; }); + } else { container.innerHTML = '

Fehler.

'; } + } catch(e) {} + } + window.addPmgNetwork = async function() { + const cidr = document.getElementById('pmgNewNetwork').value.trim(); if(!cidr) return; + const fd = new FormData(); fd.append('node_id', document.getElementById('pmgNodeId').value); fd.append('cidr', cidr); + const res = await (await fetch('api.php?action=pmg_add_network', {method: 'POST', body: fd})).json(); + if(res.success) { document.getElementById('pmgNewNetwork').value = ''; loadPmgNetworks(document.getElementById('pmgNodeId').value); } else alert('Fehler.'); + } + window.deletePmgNetwork = async function(cidr) { + if(!confirm(`Netzwerk ${cidr} löschen?`)) return; + const fd = new FormData(); fd.append('node_id', document.getElementById('pmgNodeId').value); fd.append('cidr', cidr); + const res = await (await fetch('api.php?action=pmg_delete_network', {method: 'POST', body: fd})).json(); + if(res.success) loadPmgNetworks(document.getElementById('pmgNodeId').value); else alert('Fehler.'); + } + + window.loadPmgSsl = async function(nodeId) { + const internalName = document.getElementById('pmgInternalName').value; const container = document.getElementById('pmgSslContainer'); container.innerHTML = '

Lade Zertifikate...

'; + try { + const res = await (await fetch(`api.php?action=pmg_get_ssl&node_id=${nodeId}&internal_name=${internalName}`)).json(); + if(res.success && res.data) { + container.innerHTML = ''; + res.data.forEach(cert => { + const validUntil = cert.notafter ? formatDate(cert.notafter) : 'Unbekannt'; const isExpired = cert.notafter && cert.notafter < (Date.now() / 1000); const statusDot = isExpired ? '' : ''; + container.innerHTML += `
${statusDot}

${cert.filename || cert.subject || 'Zertifikat'}

Aussteller: ${cert.issuer || '-'}

Subject: ${cert.subject || '-'}

Ablaufdatum: ${validUntil}
`; + }); + } else { container.innerHTML = '

Fehler beim Laden.

'; } + } catch(e) { container.innerHTML = '

Netzwerkfehler.

'; } + } + window.uploadPmgSsl = async function() { + const cert = document.getElementById('pmgSslCert').value.trim(); const key = document.getElementById('pmgSslKey').value.trim(); + if(!cert || !key) return alert('Bitte Key und Zertifikat (PEM) einfügen!'); + const btn = event.target; const oTxt = btn.innerText; btn.innerText = 'Lade hoch...'; + const fd = new FormData(); fd.append('node_id', document.getElementById('pmgNodeId').value); fd.append('internal_name', document.getElementById('pmgInternalName').value); fd.append('certificate', cert); fd.append('private_key', key); + try { const res = await (await fetch('api.php?action=pmg_upload_ssl', {method: 'POST', body: fd})).json(); if(res.success) { alert('Zertifikat hochgeladen! Dienste werden neu gestartet.'); document.getElementById('pmgSslCert').value = ''; document.getElementById('pmgSslKey').value = ''; loadPmgSsl(document.getElementById('pmgNodeId').value); } else { alert('Fehler beim Upload. (Format prüfen)'); } } catch(e) { alert('Netzwerkfehler.'); } finally { btn.innerText = oTxt; } + } + + // === SCHEDULER === + const cronModal = document.getElementById('cronManagerModal'); + window.openCronManager = async function() { + cronModal.classList.remove('hidden'); + const select = document.getElementById('cronNodeId'); select.innerHTML = ''; + try { + const res = await (await fetch('api.php?action=get_nodes')).json(); + if (res.success) { + select.innerHTML = ''; + res.data.filter(n => n.type === 'pve').forEach(node => { select.innerHTML += ``; }); + } + } catch(e) {} + loadCronJobs(); + } + window.closeCronManager = function() { cronModal.classList.add('hidden'); } + + window.checkCronAction = function() { const action = document.getElementById('cronAction').value; const vmBlock = document.getElementById('cronVmBlock'); if (action.includes('_vm')) vmBlock.classList.remove('hidden'); else vmBlock.classList.add('hidden'); } + + async function loadCronJobs() { + const tbody = document.getElementById('cronTableBody'); tbody.innerHTML = 'Lade Jobs...'; + try { + const res = await (await fetch('api.php?action=get_cron_jobs')).json(); + if (res.success) { + tbody.innerHTML = ''; + if(res.data.length === 0) { tbody.innerHTML = 'Keine geplanten Jobs.'; return; } + res.data.forEach(job => { + const lastRun = formatDate(job.last_run); const isActive = parseInt(job.is_active) === 1; + const statusDot = isActive ? ' Aktiv' : ' Pausiert'; + const targetStr = job.action_type.includes('_vm') ? `VM ${job.target_vmid}` : 'Gesamter Host'; + let actionStr = job.action_type; + if(actionStr === 'reboot_node') actionStr = 'Host Reboot'; if(actionStr === 'start_vm') actionStr = 'VM Start'; if(actionStr === 'stop_vm') actionStr = 'VM Stop'; if(actionStr === 'reboot_vm') actionStr = 'VM Reboot'; + tbody.innerHTML += `
${statusDot}

${job.name}

${actionStr}

${job.node_name}

Ziel: ${targetStr}

${job.cron_schedule}${lastRun}`; + }); + } + } catch(e) {} + } + + const addCronForm = document.getElementById('addCronJobForm'); + if(addCronForm) { + addCronForm.addEventListener('submit', async function(e) { + e.preventDefault(); const btn = this.querySelector('button[type="submit"]'); const oTxt = btn.innerText; btn.innerText = 'Speichere...'; + const fd = new FormData(); fd.append('name', document.getElementById('cronName').value); fd.append('node_id', document.getElementById('cronNodeId').value); fd.append('action_type', document.getElementById('cronAction').value); fd.append('target_vmid', document.getElementById('cronTargetVmid').value); fd.append('cron_schedule', document.getElementById('cronSchedule').value); + try { const res = await (await fetch('api.php?action=add_cron_job', {method: 'POST', body: fd})).json(); if(res.success) { addCronForm.reset(); document.getElementById('cronSchedule').value = '0 3 * * *'; checkCronAction(); loadCronJobs(); } else alert('Fehler.'); } catch(e) {} finally { btn.innerText = oTxt; } + }); + } + + 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(); } + + // === NEU: AUDIT LOG === + const auditModal = document.getElementById('auditLogModal'); + window.openAuditLog = async function() { + auditModal.classList.remove('hidden'); + const tbody = document.getElementById('auditLogTableBody'); + tbody.innerHTML = 'Lade Protokoll...'; + try { + const res = await (await fetch('api.php?action=get_audit_logs')).json(); + if (res.success) { + tbody.innerHTML = ''; + if(res.data.length === 0) { tbody.innerHTML = 'Noch keine Einträge.'; return; } + res.data.forEach(log => { + // Konvertiere SQLite DATETIME in lokales deutsches Format + const d = new Date(log.timestamp + 'Z'); + const timeStr = d.toLocaleDateString('de-DE') + ' ' + d.toLocaleTimeString('de-DE'); + const isSystem = log.username === 'System'; + + tbody.innerHTML += ` + + ${timeStr} + ${log.username} + ${log.action} + ${log.target || '-'} + `; + }); + } + } catch(e) { tbody.innerHTML = 'Fehler beim Laden.'; } + } + window.closeAuditLog = function() { auditModal.classList.add('hidden'); } +} \ No newline at end of file diff --git a/src/cron.php b/src/cron.php new file mode 100644 index 0000000..cf63dae --- /dev/null +++ b/src/cron.php @@ -0,0 +1,100 @@ + 0 && ((int)$current % $step) === 0; + } + return false; +} + +// Mini-Proxmox-Fetcher für das Cron-Skript +function cronProxmoxData($ip, $tokenId, $tokenSecret, $endpoint, $method = "POST", $postData = null) { + $headers = ["Authorization: PVEAPIToken={$tokenId}={$tokenSecret}"]; + $ch = curl_init("https://{$ip}:8006{$endpoint}"); + $options = [ + CURLOPT_RETURNTRANSFER => true, CURLOPT_SSL_VERIFYPEER => false, + CURLOPT_SSL_VERIFYHOST => false, CURLOPT_TIMEOUT => 15, + CURLOPT_HTTPHEADER => $headers, CURLOPT_CUSTOMREQUEST => $method + ]; + 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); +} + +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); + +foreach ($tasks as $task) { + if (isCronMatch($task['cron_schedule'])) { + echo "-> Führe Task ID {$task['id']} ('{$task['name']}') aus...\n"; + + $nStmt = $pdo->prepare("SELECT * FROM nodes WHERE id = ?"); + $nStmt->execute([$task['node_id']]); + $node = $nStmt->fetch(PDO::FETCH_ASSOC); + + 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'])) { + $nName = $nData['data'][0]['node']; + cronProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$nName}/status", 'POST', ['command' => 'reboot']); + $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) { + if ((string)$vm['vmid'] === (string)$task['target_vmid']) { + $cmd = str_replace('_vm', '', $task['action_type']); + $type = $vm['type'] === 'lxc' ? 'lxc' : 'qemu'; + cronProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$vm['node']}/{$type}/{$vm['vmid']}/status/{$cmd}", 'POST'); + $success = true; + break; + } + } + } + } + + // 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']]); + echo " Erfolgreich an API gesendet.\n"; + } else { + echo " Fehler beim Ausführen!\n"; + } + } + } +} +echo "[" . date('Y-m-d H:i:s') . "] Scheduler-Check beendet.\n"; +?> \ No newline at end of file diff --git a/src/db.php b/src/db.php new file mode 100644 index 0000000..3210fcc --- /dev/null +++ b/src/db.php @@ -0,0 +1,41 @@ +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + + $pdo->exec("CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT NOT NULL UNIQUE, + password_hash TEXT NOT NULL, + role TEXT DEFAULT 'viewer', + permissions TEXT DEFAULT '{}', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + )"); + + $pdo->exec("CREATE TABLE IF NOT EXISTS nodes ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + ip_address TEXT NOT NULL, + token_id TEXT NOT NULL, + token_secret TEXT NOT NULL, + type TEXT DEFAULT 'pve' + )"); + + // Automatisches Datenbank-Upgrade für den PBS! (Fügt Spalte 'type' hinzu, falls sie fehlt) + try { + $pdo->exec("ALTER TABLE nodes ADD COLUMN type TEXT DEFAULT 'pve'"); + } catch (PDOException $e) { + // Ignorieren, wenn die Spalte bereits existiert + } + + $stmt = $pdo->query("SELECT COUNT(*) FROM users"); + if ($stmt->fetchColumn() == 0) { + $hash = password_hash('admin', PASSWORD_DEFAULT); + $pdo->exec("INSERT INTO users (username, password_hash, role) VALUES ('admin', '$hash', 'admin')"); + } +} catch (PDOException $e) { + die("Datenbank-Fehler: " . $e->getMessage()); +} +?> \ No newline at end of file diff --git a/src/index.php b/src/index.php new file mode 100644 index 0000000..b7a7e2a --- /dev/null +++ b/src/index.php @@ -0,0 +1,130 @@ +query("SELECT COUNT(*) FROM nodes"); +$nodeCount = $stmt->fetchColumn(); +?> + + + + + + Proxmox Unified Console + + + + + + + +
+
+
+
PX
+

Proxmox Unified Console

+
+ +
+ Hallo, + +
+ +
System Status: Sicher
+ +
+
+ +
+ +

Login

+ +

Willkommen! 👋

Verbinde deinen ersten Host, um zu starten.

+ +
+ + + +
+
+ + +
+
+

Cluster CPU Cores

Lade...

+

Globaler RAM

Lade...

+

Datacenter Storage

Lade...

+

System Updates (APT)

Lade...

Prüfe Updates...
+
+

Live Cluster Auslastung

Live Sync
+

🔥 Top 5 Ressourcen-Fresser

Lädt Live-Daten von Proxmox API...

+
+ + + + + + + + +

Letzte Jobs

Lädt Job-Historie...

+
+
+
+ + +
+ + + + + + + + \ No newline at end of file diff --git a/src/modals.php b/src/modals.php new file mode 100644 index 0000000..8eb983c --- /dev/null +++ b/src/modals.php @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file