6 Commits
6 changed files with 253 additions and 550 deletions
+6 -5
View File
@@ -60,19 +60,20 @@ services:
- ./data:/var/www/data - ./data:/var/www/data
environment: environment:
- TZ=Europe/Berlin - TZ=Europe/Berlin
```
2. Starten: 2. Starten:
```
Bash Bash
docker compose up -d docker compose up -d
```
3. ⚠️ WICHTIG: Rechte für SQLite anpassen: 3. ⚠️ WICHTIG: Rechte für SQLite anpassen:
Da der Container als Benutzer www-data (UID 33) läuft, der neu erstellte Volume-Ordner auf dem Host aber oft root gehört, muss dem Ordner die Schreibberechtigung erteilt werden. Führe im Verzeichnis der Compose-Datei aus: Da der Container als Benutzer www-data (UID 33) läuft, der neu erstellte Volume-Ordner auf dem Host aber oft root gehört, muss dem Ordner die Schreibberechtigung erteilt werden. Führe im Verzeichnis der Compose-Datei aus:
Bash Bash
```
sudo chown -R 33:33 ./data sudo chown -R 33:33 ./data
sudo chmod -R 775 ./data sudo chmod -R 775 ./data
```
4. Login: 4. Login:
Rufe https://<DEINE-IP>:8443 in deinem Browser auf (Zertifikatswarnung ignorieren). Rufe https://<DEINE-IP>:8443 in deinem Browser auf (Zertifikatswarnung ignorieren).
@@ -95,4 +96,4 @@ Rufe https://<DEINE-IP>:8443 in deinem Browser auf (Zertifikatswarnung ignoriere
Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
📄 License 📄 License
MIT MIT
+52 -7
View File
@@ -18,7 +18,6 @@ if ($action === 'get_stats') {
$nData = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes"); $nData = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes");
if (isset($nData['data'])) { if (isset($nData['data'])) {
foreach ($nData['data'] as $nInfo) { foreach ($nData['data'] as $nInfo) {
// Duplikate (z.B. in Clustern) vermeiden
if (!isset($seenNodes[$nInfo['node']])) { if (!isset($seenNodes[$nInfo['node']])) {
$seenNodes[$nInfo['node']] = true; $seenNodes[$nInfo['node']] = true;
@@ -48,7 +47,6 @@ if ($action === 'get_stats') {
} }
} }
// VMs von allen angebundenen Hosts ziehen und Duplikate filtern
$vms = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/cluster/resources?type=vm"); $vms = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/cluster/resources?type=vm");
if (isset($vms['data'])) { if (isset($vms['data'])) {
foreach($vms['data'] as $vm) { foreach($vms['data'] as $vm) {
@@ -324,13 +322,14 @@ if ($action === 'restore_backup') {
if ($action === 'get_node_status' || $action === 'get_vm_status') { if ($action === 'get_node_status' || $action === 'get_vm_status') {
$stmt = $pdo->prepare("SELECT * FROM nodes WHERE id = ?"); $stmt->execute([$_GET['node_id']]); $node = $stmt->fetch(PDO::FETCH_ASSOC); $stmt = $pdo->prepare("SELECT * FROM nodes WHERE id = ?"); $stmt->execute([$_GET['node_id']]); $node = $stmt->fetch(PDO::FETCH_ASSOC);
$type = $node['type'] ?? 'pve';
if ($action === 'get_node_status') { if ($action === 'get_node_status') {
$nData = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes"); $nData = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes", $type);
$internalName = $nData['data'][0]['node'] ?? 'pve'; $internalName = $nData['data'][0]['node'] ?? ($type === 'pbs' ? 'localhost' : 'pve');
$res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$internalName}/status"); $res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$internalName}/status", $type);
$rrd = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$internalName}/rrddata?timeframe=hour&cf=AVERAGE"); $rrd = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$internalName}/rrddata?timeframe=hour&cf=AVERAGE", $type);
$netin = 0; $netout = 0; $netin = 0; $netout = 0;
if (isset($rrd['data']) && is_array($rrd['data'])) { if (isset($rrd['data']) && is_array($rrd['data'])) {
@@ -351,8 +350,54 @@ if ($action === 'get_node_status' || $action === 'get_vm_status') {
echo json_encode(['success' => true, 'data' => $data]); exit; echo json_encode(['success' => true, 'data' => $data]); exit;
} else { } else {
$endpoint = "/api2/json/nodes/{$_GET['host']}/{$_GET['type']}/{$_GET['vmid']}/status/current"; $endpoint = "/api2/json/nodes/{$_GET['host']}/{$_GET['type']}/{$_GET['vmid']}/status/current";
$res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], $endpoint); $res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], $endpoint, $type);
echo json_encode(['success' => true, 'data' => $res['data'] ?? []]); exit; echo json_encode(['success' => true, 'data' => $res['data'] ?? []]); exit;
} }
} }
// === RRD Historie (24h / 7 Tage) für alle Node Typen ===
if ($action === 'get_historical_rrd') {
if (!checkVmPermission($pdo, $_GET['vmid'] ?? 0)) exit;
$stmt = $pdo->prepare("SELECT * FROM nodes WHERE id = ?"); $stmt->execute([$_GET['node_id']]); $node = $stmt->fetch(PDO::FETCH_ASSOC);
$timeframe = $_GET['timeframe'] === 'week' ? 'week' : 'day';
$type = $node['type'] ?? 'pve';
if ($_GET['target_mode'] === 'node') {
$nData = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes", $type);
$internalName = $nData['data'][0]['node'] ?? 'localhost';
$endpoint = "/api2/json/nodes/{$internalName}/rrddata?timeframe={$timeframe}&cf=AVERAGE";
} else {
$endpoint = "/api2/json/nodes/{$_GET['host']}/{$_GET['target_mode']}/{$_GET['vmid']}/rrddata?timeframe={$timeframe}&cf=AVERAGE";
}
$res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], $endpoint, $type);
echo json_encode(['success' => true, 'data' => $res['data'] ?? []]); exit;
}
// === APT Update Manager ===
if ($action === 'get_update_details') {
if (!$isAdmin) exit;
$stmt = $pdo->query("SELECT * FROM nodes WHERE type != 'pmg'"); $updateList = [];
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $node) {
$nData = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes", $node['type']);
if (isset($nData['data'])) {
foreach ($nData['data'] as $nInfo) {
$internalName = $nInfo['node'] ?? 'localhost';
$apt = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$internalName}/apt/update", $node['type']);
if (isset($apt['data']) && count($apt['data']) > 0) {
$updateList[] = [ 'node_id' => $node['id'], 'host' => $internalName, 'display_name' => $node['name'] . ' (' . $internalName . ')', 'type' => $node['type'], 'packages' => $apt['data'] ];
}
}
}
}
echo json_encode(['success' => true, 'data' => $updateList]); exit;
}
if ($action === 'trigger_apt_upgrade') {
if (!$isAdmin) exit;
$stmt = $pdo->prepare("SELECT * FROM nodes WHERE id = ?"); $stmt->execute([$_POST['node_id']]); $node = $stmt->fetch(PDO::FETCH_ASSOC);
$res = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$_POST['host']}/apt/upgrade", $node['type'], 'POST');
logAudit('APT Upgrade gestartet', "Node: {$_POST['host']}");
echo json_encode(['success' => isset($res['data']), 'upid' => $res['data'] ?? '', 'error' => $res['errors'] ?? 'API Fehler']); exit;
}
?> ?>
+129 -476
View File
@@ -11,10 +11,7 @@ if (!window.APP.isLoggedIn) {
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; } }); } 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() { async function logout() { await fetch('api.php?action=logout'); window.location.href = window.location.pathname + '?t=' + Date.now(); }
await fetch('api.php?action=logout');
window.location.href = window.location.pathname + '?t=' + Date.now();
}
if (window.APP.isLoggedIn && window.APP.nodeCount > 0) { if (window.APP.isLoggedIn && window.APP.nodeCount > 0) {
@@ -33,23 +30,11 @@ if (window.APP.isLoggedIn && window.APP.nodeCount > 0) {
const pwdForm = document.getElementById('changePasswordForm'); const pwdForm = document.getElementById('changePasswordForm');
if(pwdForm) { if(pwdForm) {
pwdForm.addEventListener('submit', async function(e) { pwdForm.addEventListener('submit', async function(e) {
e.preventDefault(); e.preventDefault(); const oldP = document.getElementById('oldPassword').value; const newP = document.getElementById('newPassword').value; const confirmP = document.getElementById('newPasswordConfirm').value;
const oldP = document.getElementById('oldPassword').value;
const newP = document.getElementById('newPassword').value;
const confirmP = document.getElementById('newPasswordConfirm').value;
if (newP !== confirmP) { alert('Die neuen Passwörter stimmen nicht überein!'); return; } if (newP !== confirmP) { alert('Die neuen Passwörter stimmen nicht überein!'); return; }
const btn = this.querySelector('button[type="submit"]'); const oTxt = btn.innerText; btn.innerText = 'Speichere...'; const btn = this.querySelector('button[type="submit"]'); const oTxt = btn.innerText; btn.innerText = 'Speichere...';
const fd = new FormData(); fd.append('old_password', oldP); fd.append('new_password', newP); const fd = new FormData(); fd.append('old_password', oldP); fd.append('new_password', newP);
try { const res = await (await fetch('api.php?action=change_password', {method: 'POST', body: fd})).json(); if(res.success) { alert('Passwort erfolgreich geändert! Bitte neu anmelden.'); closePasswordModal(); logout(); } else { alert(res.error || 'Fehler beim Ändern des Passworts.'); } } catch(err) { alert('Netzwerkfehler.'); } finally { btn.innerText = oTxt; }
try {
const res = await (await fetch('api.php?action=change_password', {method: 'POST', body: fd})).json();
if(res.success) {
alert('Passwort erfolgreich geändert! Bitte neu anmelden.');
closePasswordModal(); logout();
} else { alert(res.error || 'Fehler beim Ändern des Passworts.'); }
} catch(err) { alert('Netzwerkfehler.'); } finally { btn.innerText = oTxt; }
}); });
} }
@@ -72,26 +57,12 @@ if (window.APP.isLoggedIn && window.APP.nodeCount > 0) {
const res = await (await fetch('api.php?action=get_stats')).json(); const res = await (await fetch('api.php?action=get_stats')).json();
if(res.success && res.data) { if(res.success && res.data) {
const d = res.data; const d = res.data;
document.getElementById('stat-cpu-text').innerText = `${d.cpu_percent}% (${d.cpu_cores} Cores)`; document.getElementById('stat-cpu-text').innerText = `${d.cpu_percent}% (${d.cpu_cores} Cores)`; document.getElementById('stat-cpu-bar').style.width = `${d.cpu_percent}%`;
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 ramPercent = (d.ram_used / d.ram_total) * 100 || 0; 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}%`;
document.getElementById('stat-ram-text').innerText = `${formatBytes(d.ram_used)} / ${formatBytes(d.ram_total)}`;
document.getElementById('stat-ram-bar').style.width = `${ramPercent}%`;
let diskPercent = (d.disk_used / d.disk_total) * 100 || 0;
document.getElementById('stat-disk-text').innerText = `${formatBytes(d.disk_used)} / ${formatBytes(d.disk_total)}`;
document.getElementById('stat-disk-bar').style.width = `${diskPercent}%`;
// Neue Übersichtskachel füttern
if (d.cluster_stats) { if (d.cluster_stats) {
const elNodesOn = document.getElementById('stat-nodes-online'); const elNodesOn = document.getElementById('stat-nodes-online');
if (elNodesOn) { if (elNodesOn) { elNodesOn.innerText = d.cluster_stats.nodes_online; elNodesOn.className = (d.cluster_stats.nodes_online < d.cluster_stats.nodes_total) ? 'text-red-500' : 'text-green-500'; }
elNodesOn.innerText = d.cluster_stats.nodes_online;
if (d.cluster_stats.nodes_online < d.cluster_stats.nodes_total) {
elNodesOn.className = 'text-red-500';
} else {
elNodesOn.className = 'text-green-500';
}
}
if(document.getElementById('stat-nodes-total')) document.getElementById('stat-nodes-total').innerText = d.cluster_stats.nodes_total; if(document.getElementById('stat-nodes-total')) document.getElementById('stat-nodes-total').innerText = d.cluster_stats.nodes_total;
if(document.getElementById('stat-vms-total')) document.getElementById('stat-vms-total').innerText = d.cluster_stats.vms_total; if(document.getElementById('stat-vms-total')) document.getElementById('stat-vms-total').innerText = d.cluster_stats.vms_total;
if(document.getElementById('stat-vms-run')) document.getElementById('stat-vms-run').innerText = d.cluster_stats.vms_running; if(document.getElementById('stat-vms-run')) document.getElementById('stat-vms-run').innerText = d.cluster_stats.vms_running;
@@ -104,9 +75,7 @@ if (window.APP.isLoggedIn && window.APP.nodeCount > 0) {
liveChart.data.labels.push(timeStr); liveChart.data.labels.push(timeStr);
liveChart.data.datasets[0].data.push(d.cpu_percent); liveChart.data.datasets[0].data.push(d.cpu_percent);
liveChart.data.datasets[1].data.push(ramPercent.toFixed(1)); liveChart.data.datasets[1].data.push(ramPercent.toFixed(1));
if (liveChart.data.labels.length > 15) { if (liveChart.data.labels.length > 15) { liveChart.data.labels.shift(); liveChart.data.datasets[0].data.shift(); liveChart.data.datasets[1].data.shift(); }
liveChart.data.labels.shift(); liveChart.data.datasets[0].data.shift(); liveChart.data.datasets[1].data.shift();
}
liveChart.update(); liveChart.update();
if(liveNetChart && d.nodes_net) { if(liveNetChart && d.nodes_net) {
@@ -115,35 +84,31 @@ if (window.APP.isLoggedIn && window.APP.nodeCount > 0) {
liveNetChart.data.labels.push(timeStr); liveNetChart.data.labels.push(timeStr);
if (liveNetChart.data.labels.length > 15) liveNetChart.data.labels.shift(); if (liveNetChart.data.labels.length > 15) liveNetChart.data.labels.shift();
const colors = ['#10b981', '#8b5cf6', '#f59e0b', '#ef4444', '#06b6d4']; const colors = ['#10b981', '#8b5cf6', '#f59e0b', '#ef4444', '#06b6d4'];
d.nodes_net.forEach((n, idx) => { d.nodes_net.forEach((n, idx) => {
let rxSpeed = n.netin / (1024 * 1024); let rxSpeed = n.netin / (1024 * 1024); let txSpeed = n.netout / (1024 * 1024); let totalSpeed = (rxSpeed + txSpeed).toFixed(2);
let txSpeed = n.netout / (1024 * 1024);
let totalSpeed = (rxSpeed + txSpeed).toFixed(2);
let ds = liveNetChart.data.datasets.find(ds => ds.label === n.name); let ds = liveNetChart.data.datasets.find(ds => ds.label === n.name);
if (!ds) { if (!ds) { const c = colors[idx % colors.length]; ds = { label: n.name, borderColor: c, backgroundColor: c + '1a', borderWidth: 2, tension: 0.4, fill: true, data: new Array(Math.max(0, liveNetChart.data.labels.length - 1)).fill(0) }; liveNetChart.data.datasets.push(ds); }
const c = colors[idx % colors.length]; ds.data.push(totalSpeed); if (ds.data.length > 15) ds.data.shift();
ds = { label: n.name, borderColor: c, backgroundColor: c + '1a', borderWidth: 2, tension: 0.4, fill: true, data: new Array(Math.max(0, liveNetChart.data.labels.length - 1)).fill(0) };
liveNetChart.data.datasets.push(ds);
}
ds.data.push(totalSpeed);
if (ds.data.length > 15) ds.data.shift();
}); });
liveNetChart.update(); liveNetChart.update();
} }
prevGlobalTime = nowTs; prevGlobalTime = nowTs;
} }
} }
} }
} catch (err) {} } catch (err) {}
} }
async function fetchTopVms() { if(!document.getElementById('top-vms-container')) return; try { const res = await (await fetch('api.php?action=get_top_vms')).json(); if(res.success && res.data) { const container = document.getElementById('top-vms-container'); container.innerHTML = ''; if(res.data.length === 0) { container.innerHTML = '<p class="text-gray-400 text-sm">Keine aktiven VMs.</p>'; return; } res.data.forEach((vm, i) => { const cpuPercent = ((vm.cpu || 0) * 100).toFixed(1); const ramUsed = formatBytes(vm.mem || 0); const icon = vm.type === 'lxc' ? '📦' : '🖥️'; const numberColor = i === 0 ? 'text-red-500' : (i === 1 ? 'text-orange-400' : (i === 2 ? 'text-yellow-400' : 'text-gray-400')); container.innerHTML += `<div class="bg-darkbg border border-darkborder rounded-lg p-3 flex justify-between items-center transition-transform hover:scale-[1.02] cursor-default"><div class="flex items-center gap-3"><span class="font-bold text-xl ${numberColor}">#${i + 1}</span><div><h4 class="text-white font-semibold text-sm truncate w-32">${icon} ${vm.name}</h4><p class="text-xs text-gray-500">Host: ${vm.host}</p></div></div><div class="text-right"><p class="text-proxmox font-bold text-sm">${cpuPercent}% CPU</p><p class="text-xs text-gray-400">${ramUsed} RAM</p></div></div>`; }); } } catch (err) {} } async function fetchTopVms() { if(!document.getElementById('top-vms-container')) return; try { const res = await (await fetch('api.php?action=get_top_vms')).json(); if(res.success && res.data) { const container = document.getElementById('top-vms-container'); container.innerHTML = ''; if(res.data.length === 0) { container.innerHTML = '<p class="text-gray-400 text-sm">Keine aktiven VMs.</p>'; return; } res.data.forEach((vm, i) => { const cpuPercent = ((vm.cpu || 0) * 100).toFixed(1); const ramUsed = formatBytes(vm.mem || 0); const icon = vm.type === 'lxc' ? '📦' : '🖥️'; const numberColor = i === 0 ? 'text-red-500' : (i === 1 ? 'text-orange-400' : (i === 2 ? 'text-yellow-400' : 'text-gray-400')); container.innerHTML += `<div class="bg-darkbg border border-darkborder rounded-lg p-3 flex justify-between items-center transition-transform hover:scale-[1.02] cursor-default"><div class="flex items-center gap-3"><span class="font-bold text-xl ${numberColor}">#${i + 1}</span><div><h4 class="text-white font-semibold text-sm truncate w-32">${icon} ${vm.name}</h4><p class="text-xs text-gray-500">Host: ${vm.host}</p></div></div><div class="text-right"><p class="text-proxmox font-bold text-sm">${cpuPercent}% CPU</p><p class="text-xs text-gray-400">${ramUsed} RAM</p></div></div>`; }); } } catch (err) {} }
async function fetchRecentJobs() { if(!document.getElementById('recent-jobs-container')) return; try { const res = await (await fetch('api.php?action=get_recent_jobs')).json(); if(res.success && res.data) { const container = document.getElementById('recent-jobs-container'); container.innerHTML = ''; if(res.data.length === 0) { container.innerHTML = '<p class="text-gray-400 text-sm">Keine aktuellen Jobs.</p>'; return; } res.data.forEach(job => { const jobTypeStr = job.type || job.worker_type || 'unknown'; let statusColor = 'text-gray-400', statusIcon = '⏳', statusText = job.status || 'running...'; if(statusText.toLowerCase() === 'ok') { statusColor = 'text-green-500'; statusIcon = '✅'; } else if(statusText !== 'running...') { statusColor = 'text-red-500'; statusIcon = '❌'; } else { statusColor = 'text-blue-400'; statusIcon = '🔄'; } const date = new Date(job.starttime * 1000); const timeStr = date.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' }), dateStr = date.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' }); const isBackup = jobTypeStr.includes('sync') || jobTypeStr.includes('prune') || jobTypeStr.includes('garbage_collection') || jobTypeStr.includes('vzdump') || jobTypeStr.includes('verify'); const jobTypeColor = isBackup ? 'text-purple-400' : 'text-white'; container.innerHTML += `<div class="bg-darkbg border border-darkborder rounded-lg p-3 flex justify-between items-center transition-colors hover:bg-darkborder/50"><div class="flex items-center gap-3"><div class="text-lg">${statusIcon}</div><div class="max-w-[120px]"><p class="${jobTypeColor} font-medium text-sm capitalize truncate" title="${jobTypeStr}">${jobTypeStr}</p><p class="text-xs text-gray-500 truncate" title="${job.node_name}">Host: <span class="text-proxmox">${job.node_name}</span></p></div></div><div class="text-right"><p class="${statusColor} font-bold text-sm uppercase">${statusText}</p><p class="text-xs text-gray-500">${dateStr} - ${timeStr}</p></div></div>`; }); } } catch (err) { console.error(err); } } async function fetchRecentJobs() { if(!document.getElementById('recent-jobs-container')) return; try { const res = await (await fetch('api.php?action=get_recent_jobs')).json(); if(res.success && res.data) { const container = document.getElementById('recent-jobs-container'); container.innerHTML = ''; if(res.data.length === 0) { container.innerHTML = '<p class="text-gray-400 text-sm">Keine aktuellen Jobs.</p>'; return; } res.data.forEach(job => { const jobTypeStr = job.type || job.worker_type || 'unknown'; let statusColor = 'text-gray-400', statusIcon = '⏳', statusText = job.status || 'running...'; if(statusText.toLowerCase() === 'ok') { statusColor = 'text-green-500'; statusIcon = '✅'; } else if(statusText !== 'running...') { statusColor = 'text-red-500'; statusIcon = '❌'; } else { statusColor = 'text-blue-400'; statusIcon = '🔄'; } const date = new Date(job.starttime * 1000); const timeStr = date.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' }), dateStr = date.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' }); const isBackup = jobTypeStr.includes('sync') || jobTypeStr.includes('prune') || jobTypeStr.includes('garbage_collection') || jobTypeStr.includes('vzdump') || jobTypeStr.includes('verify'); const jobTypeColor = isBackup ? 'text-purple-400' : 'text-white'; container.innerHTML += `<div class="bg-darkbg border border-darkborder rounded-lg p-3 flex justify-between items-center transition-colors hover:bg-darkborder/50"><div class="flex items-center gap-3"><div class="text-lg">${statusIcon}</div><div class="max-w-[120px]"><p class="${jobTypeColor} font-medium text-sm capitalize truncate" title="${jobTypeStr}">${jobTypeStr}</p><p class="text-xs text-gray-500 truncate" title="${job.node_name}">Host: <span class="text-proxmox">${job.node_name}</span></p></div></div><div class="text-right"><p class="${statusColor} font-bold text-sm uppercase">${statusText}</p><p class="text-xs text-gray-500">${dateStr} - ${timeStr}</p></div></div>`; }); } } catch (err) {} }
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 = '<div class="col-span-full text-center text-gray-500 p-10 bg-darkcard rounded-lg border border-darkborder">Keine PBS Server gefunden.</div>'; 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 += `<div class="bg-darkcard border border-darkborder rounded-xl p-5 shadow-lg hover:border-pbs cursor-pointer transition-colors" onclick="openPbsDatastore(${ds.node_id}, '${ds.store}')"><div class="flex justify-between items-start mb-4"><div><h3 class="text-lg font-bold text-white">${ds.store}</h3><p class="text-xs text-gray-400">PBS Host: ${ds.host}</p></div><div class="p-2 bg-purple-500/10 rounded-lg"><svg class="w-6 h-6 text-pbs" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4"></path></svg></div></div><div class="mb-2 flex justify-between text-sm"><span class="text-gray-400">Auslastung</span><span class="text-white font-bold">${percent}%</span></div><div class="w-full bg-darkbg rounded-full h-2 mb-3"><div class="${colorClass} h-2 rounded-full transition-all duration-1000" style="width: ${percent}%"></div></div><div class="flex justify-between text-xs text-gray-500"><span>Used: ${formatBytes(used)}</span><span>Total: ${formatBytes(total)}</span></div><div class="mt-4 border-t border-darkborder pt-3 text-center"><span class="text-pbs font-bold text-sm">⚙️ Verwaltung öffnen</span></div></div>`; }); } } catch(e) {} } window.fetchUpdates = async function() { 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 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 = '<div class="col-span-full text-center text-gray-500 p-10 bg-darkcard rounded-lg border border-darkborder">Keine Mail Gateways angebunden.</div>'; 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 += `<div class="bg-darkcard border border-darkborder rounded-xl p-5 shadow-lg hover:border-pmg cursor-pointer transition-colors" onclick="openPmgManager(${pmg.node_id}, '${pmg.host}', '${pmg.internal_name}')"><div class="flex justify-between items-start mb-4"><div><h3 class="text-lg font-bold text-white">${pmg.host}</h3><p class="text-xs text-green-400">Online</p></div><div class="p-2 bg-blue-500/10 rounded-lg"><svg class="w-6 h-6 text-pmg" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path></svg></div></div><div class="mb-1 flex justify-between text-xs"><span class="text-gray-400">CPU</span><span class="text-white font-bold">${cpuPercent}%</span></div><div class="w-full bg-darkbg rounded-full h-1.5 mb-3"><div class="bg-blue-500 h-1.5 rounded-full" style="width: ${cpuPercent}%"></div></div><div class="mb-1 flex justify-between text-xs"><span class="text-gray-400">RAM</span><span class="text-white font-bold">${ramPercent}%</span></div><div class="w-full bg-darkbg rounded-full h-1.5 mb-3"><div class="bg-proxmox h-1.5 rounded-full" style="width: ${ramPercent}%"></div></div><div class="mb-1 flex justify-between text-xs"><span class="text-gray-400">System Disk</span><span class="text-white font-bold">${diskPercent}%</span></div><div class="w-full bg-darkbg rounded-full h-1.5"><div class="bg-emerald-500 h-1.5 rounded-full" style="width: ${diskPercent}%"></div></div><div class="mt-4 border-t border-darkborder pt-3 text-center"><span class="text-pmg font-bold text-sm">🛡️ Gateway öffnen</span></div></div>`; }); } } catch(e) {} } window.openUpdateManager = async function() { document.getElementById('updateManagerModal').classList.remove('hidden'); const content = document.getElementById('updateManagerContent'); content.innerHTML = '<div class="text-center py-10 text-gray-500 animate-pulse">Prüfe Updates...</div>'; try { const res = await (await fetch('api.php?action=get_update_details')).json(); if (res.success) { content.innerHTML = ''; if(res.data.length === 0) { content.innerHTML = '<div class="text-center py-10 text-green-500 font-bold">🎉 Alle Systeme sind auf dem neuesten Stand!</div>'; return; } res.data.forEach(node => { let pkgsHtml = ''; node.packages.forEach(p => { pkgsHtml += `<div class="flex justify-between text-xs py-1 border-b border-darkborder/50"><span class="text-gray-300">${p.Title}</span><span class="text-gray-500">${p.OldVersion} ➔ <span class="text-proxmox">${p.Version}</span></span></div>`; }); content.innerHTML += `<div class="bg-darkcard border border-darkborder rounded-xl p-5 mb-4 shadow-lg"><div class="flex justify-between items-center mb-4 border-b border-darkborder pb-2"><h3 class="text-white font-bold flex items-center gap-2"><svg class="w-5 h-5 text-proxmox" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"></path></svg> ${node.display_name} <span class="text-xs bg-red-500/20 text-red-400 px-2 py-0.5 rounded ml-2">${node.packages.length} Updates</span></h3><button onclick="triggerAptUpgrade(${node.node_id}, '${node.host}')" class="bg-proxmox hover:bg-orange-600 text-white font-bold py-1.5 px-4 rounded text-sm transition-colors">Installieren & Upgraden</button></div><div class="max-h-60 overflow-y-auto pr-2">${pkgsHtml}</div></div>`; }); } } catch(e) {} }
window.closeUpdateManager = function() { document.getElementById('updateManagerModal').classList.add('hidden'); fetchUpdates(); }
window.triggerAptUpgrade = async function(nodeId, host) { if(!confirm(`Updates auf ${host} jetzt installieren?`)) return; const fd = new FormData(); fd.append('node_id', nodeId); fd.append('host', host); try { const res = await (await fetch('api.php?action=trigger_apt_upgrade', {method: 'POST', body: fd})).json(); if(res.success && res.upid) { openTaskLog(res.upid, nodeId, host); } else alert('Fehler: ' + res.error); } catch(e) {} }
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 = '<div class="col-span-full text-center text-gray-500 p-10 bg-darkcard rounded-lg border border-darkborder">Keine PBS Server gefunden.</div>'; 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 += `<div class="bg-darkcard border border-darkborder rounded-xl p-5 shadow-lg hover:border-pbs cursor-pointer transition-colors" onclick="openPbsDatastore(${ds.node_id}, '${ds.store}')"><div class="flex justify-between items-start mb-4"><div><h3 class="text-lg font-bold text-white">${ds.store}</h3><p class="text-xs text-gray-400">PBS Host: ${ds.host}</p></div><div class="p-2 bg-purple-500/10 rounded-lg"><svg class="w-6 h-6 text-pbs" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4"></path></svg></div></div><div class="mb-2 flex justify-between text-sm"><span class="text-gray-400">Auslastung</span><span class="text-white font-bold">${percent}%</span></div><div class="w-full bg-darkbg rounded-full h-2 mb-3"><div class="${colorClass} h-2 rounded-full transition-all duration-1000" style="width: ${percent}%"></div></div><div class="flex justify-between text-xs text-gray-500"><span>Used: ${formatBytes(used)}</span><span>Total: ${formatBytes(total)}</span></div></div>`; }); } } 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 = '<div class="col-span-full text-center text-gray-500 p-10 bg-darkcard rounded-lg border border-darkborder">Keine Mail Gateways angebunden.</div>'; 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 += `<div class="bg-darkcard border border-darkborder rounded-xl p-5 shadow-lg hover:border-pmg cursor-pointer transition-colors" onclick="openPmgManager(${pmg.node_id}, '${pmg.host}', '${pmg.internal_name}')"><div class="flex justify-between items-start mb-4"><div><h3 class="text-lg font-bold text-white">${pmg.host}</h3><p class="text-xs text-green-400">Online</p></div><div class="p-2 bg-blue-500/10 rounded-lg"><svg class="w-6 h-6 text-pmg" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path></svg></div></div><div class="mb-1 flex justify-between text-xs"><span class="text-gray-400">CPU</span><span class="text-white font-bold">${cpuPercent}%</span></div><div class="w-full bg-darkbg rounded-full h-1.5 mb-3"><div class="bg-blue-500 h-1.5 rounded-full" style="width: ${cpuPercent}%"></div></div><div class="mb-1 flex justify-between text-xs"><span class="text-gray-400">RAM</span><span class="text-white font-bold">${ramPercent}%</span></div><div class="w-full bg-darkbg rounded-full h-1.5 mb-3"><div class="bg-proxmox h-1.5 rounded-full" style="width: ${ramPercent}%"></div></div><div class="mb-1 flex justify-between text-xs"><span class="text-gray-400">System Disk</span><span class="text-white font-bold">${diskPercent}%</span></div><div class="w-full bg-darkbg rounded-full h-1.5"><div class="bg-emerald-500 h-1.5 rounded-full" style="width: ${diskPercent}%"></div></div></div>`; }); } } catch(e) {} }
fetchGlobalStats(); setInterval(fetchGlobalStats, 10000); fetchTopVms(); setInterval(fetchTopVms, 10000); fetchRecentJobs(); setInterval(fetchRecentJobs, 15000); fetchUpdates(); setInterval(fetchUpdates, 60000); setInterval(fetchPbsStats, 10000); setInterval(fetchPmgStats, 10000); fetchGlobalStats(); setInterval(fetchGlobalStats, 10000); fetchTopVms(); setInterval(fetchTopVms, 10000); fetchRecentJobs(); setInterval(fetchRecentJobs, 15000); fetchUpdates(); setInterval(fetchUpdates, 60000); setInterval(fetchPbsStats, 10000); setInterval(fetchPmgStats, 10000);
@@ -201,8 +166,7 @@ if (window.APP.isLoggedIn && window.APP.nodeCount > 0) {
window.openVmSettings = async function(vmid, host, type, nodeId, name) { window.openVmSettings = async function(vmid, host, type, nodeId, name) {
settingsModal.classList.remove('hidden'); document.getElementById('settingsModalTitle').innerText = 'Einstellungen: ' + 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('setVmid').value = vmid; document.getElementById('setHost').value = host; document.getElementById('setType').value = type; document.getElementById('setNodeId').value = nodeId;
document.getElementById('snapshotListContainer').innerHTML = '<div class="text-gray-500 text-sm italic">Lade Snapshots...</div>'; document.getElementById('snapshotListContainer').innerHTML = '<div class="text-gray-500 text-sm italic">Lade Snapshots...</div>'; document.getElementById('backupListContainer').innerHTML = '<div class="text-gray-500 text-sm italic">Lade Backups...</div>';
document.getElementById('backupListContainer').innerHTML = '<div class="text-gray-500 text-sm italic">Lade Backups...</div>';
try { try {
const res = await (await fetch(`api.php?action=get_vm_config&vmid=${vmid}&host=${host}&type=${type}&node_id=${nodeId}`)).json(); 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'; } 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'; }
@@ -219,439 +183,145 @@ if (window.APP.isLoggedIn && window.APP.nodeCount > 0) {
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.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) {} } 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) { 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 = '<p class="text-gray-500 text-sm italic">Keine Snapshots vorhanden.</p>'; return; } container.innerHTML = ''; snaps.forEach(s => { const timeStr = s.snaptime ? formatDate(s.snaptime) : 'Unbekannt'; container.innerHTML += `<div class="flex justify-between items-center bg-darkcard p-2 border border-darkborder rounded"><div><p class="text-white text-sm font-bold">${s.name}</p><p class="text-xs text-gray-500">${timeStr}</p></div><div class="flex gap-2"><button onclick="actionSnapshot('rollback', '${s.name}')" class="text-orange-400 hover:text-orange-300 text-xs font-bold px-2 border-r border-darkborder">Rollback</button><button onclick="actionSnapshot('delete', '${s.name}')" class="text-red-500 hover:text-red-400 text-xs font-bold px-2">Löschen</button></div></div>`; }); } } catch(e) {} }
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 = '<p class="text-gray-500 text-sm italic">Keine Snapshots vorhanden.</p>'; return; }
container.innerHTML = '';
snaps.forEach(s => {
const timeStr = s.snaptime ? formatDate(s.snaptime) : 'Unbekannt';
container.innerHTML += `<div class="flex justify-between items-center bg-darkcard p-2 border border-darkborder rounded"><div><p class="text-white text-sm font-bold">${s.name}</p><p class="text-xs text-gray-500">${timeStr}</p></div><div class="flex gap-2"><button onclick="actionSnapshot('rollback', '${s.name}')" class="text-orange-400 hover:text-orange-300 text-xs font-bold px-2 border-r border-darkborder">Rollback</button><button onclick="actionSnapshot('delete', '${s.name}')" class="text-red-500 hover:text-red-400 text-xs font-bold px-2">Löschen</button></div></div>`;
});
}
} 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.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) { 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) {} }
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) { 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 = '<p class="text-gray-500 text-sm italic">Keine Backups gefunden.</p>'; return; } container.innerHTML = ''; res.data.forEach(b => { const timeStr = b.ctime ? formatDate(b.ctime) : 'Unbekannt'; const sizeStr = formatBytes(b.size); container.innerHTML += `<div class="flex justify-between items-center bg-darkcard p-2 border border-darkborder rounded hover:border-pbs transition-colors cursor-pointer" onclick="restoreBackup('${b.volid}')"><div class="truncate pr-2"><p class="text-white text-xs font-bold truncate" title="${b.volid}">${b.volid}</p><p class="text-xs text-gray-500">${timeStr} | ${sizeStr} | Storage: ${b.storage}</p></div><span class="text-pbs text-xs font-bold whitespace-nowrap">Restore ⤵</span></div>`; }); } } catch(e) {} }
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 = '<p class="text-gray-500 text-sm italic">Keine Backups gefunden.</p>'; return; }
container.innerHTML = '';
res.data.forEach(b => {
const timeStr = b.ctime ? formatDate(b.ctime) : 'Unbekannt'; const sizeStr = formatBytes(b.size);
container.innerHTML += `<div class="flex justify-between items-center bg-darkcard p-2 border border-darkborder rounded hover:border-pbs transition-colors cursor-pointer" onclick="restoreBackup('${b.volid}')"><div class="truncate pr-2"><p class="text-white text-xs font-bold truncate" title="${b.volid}">${b.volid}</p><p class="text-xs text-gray-500">${timeStr} | ${sizeStr} | Storage: ${b.storage}</p></div><span class="text-pbs text-xs font-bold whitespace-nowrap">Restore ⤵</span></div>`;
});
}
} 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.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) { 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) {} }
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'); const topologyModal = document.getElementById('nodeTopologyModal');
window.openNodeTopology = async function() { window.openNodeTopology = async function() { topologyModal.classList.remove('hidden'); const container = document.getElementById('nodeTopologyContainer'); container.innerHTML = '<div class="text-center text-gray-500 py-10 animate-pulse">Lade Cluster-Daten...</div>'; 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 ? '<span class="w-2 h-2 rounded-full bg-green-500 animate-pulse shrink-0"></span>' : '<span class="w-2 h-2 rounded-full bg-gray-600 shrink-0"></span>'; vmsHtml += `<div class="flex flex-col p-3 rounded-lg border ${statusColor} transition-transform hover:scale-[1.02]"><div class="flex items-center gap-2 mb-1">${dot}<span class="font-bold text-sm truncate" title="${vm.name}">${icon} ${vm.name}</span></div><div class="flex justify-between text-xs opacity-75"><span>ID: ${vm.vmid}</span><span>${vm.maxcpu || 1}C / ${formatBytes(vm.maxmem || 0)}</span></div></div>`; }); } container.innerHTML += `<div class="bg-darkcard border border-darkborder rounded-xl p-5 shadow-lg"><div class="flex justify-between items-center mb-4 border-b border-darkborder pb-3"><h3 class="text-lg font-bold text-white flex items-center gap-2"><svg class="w-5 h-5 text-proxmox" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"></path></svg>${node.name}</h3><div class="flex gap-3"><button onclick="openLiveGraph('node', 0, '${node.name}', ${node.id}, '${node.name}')" class="text-blue-400 hover:text-white transition-colors text-sm" title="Node Performance">📈 Live Graph</button><span class="text-xs font-bold text-gray-400 bg-darkbg px-3 py-1 rounded-full border border-darkborder">${node.ip_address}</span></div></div><div class="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4">${vmsHtml}</div></div>`; }); } } catch(e) {} }
topologyModal.classList.remove('hidden'); const container = document.getElementById('nodeTopologyContainer'); container.innerHTML = '<div class="text-center text-gray-500 py-10 animate-pulse">Lade Cluster-Daten...</div>';
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 ? '<span class="w-2 h-2 rounded-full bg-green-500 animate-pulse shrink-0"></span>' : '<span class="w-2 h-2 rounded-full bg-gray-600 shrink-0"></span>';
vmsHtml += `<div class="flex flex-col p-3 rounded-lg border ${statusColor} transition-transform hover:scale-[1.02]"><div class="flex items-center gap-2 mb-1">${dot}<span class="font-bold text-sm truncate" title="${vm.name}">${icon} ${vm.name}</span></div><div class="flex justify-between text-xs opacity-75"><span>ID: ${vm.vmid}</span><span>${vm.maxcpu || 1}C / ${formatBytes(vm.maxmem || 0)}</span></div></div>`;
});
}
container.innerHTML += `<div class="bg-darkcard border border-darkborder rounded-xl p-5 shadow-lg"><div class="flex justify-between items-center mb-4 border-b border-darkborder pb-3"><h3 class="text-lg font-bold text-white flex items-center gap-2"><svg class="w-5 h-5 text-proxmox" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"></path></svg>${node.name}</h3><div class="flex gap-3"><button onclick="openLiveGraph('node', 0, '${node.name}', ${node.id}, '${node.name}')" class="text-blue-400 hover:text-white transition-colors text-sm" title="Node Performance">📈 Live Graph</button><span class="text-xs font-bold text-gray-400 bg-darkbg px-3 py-1 rounded-full border border-darkborder">${node.ip_address}</span></div></div><div class="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4">${vmsHtml}</div></div>`;
});
}
} catch(e) {}
}
window.closeNodeTopology = function() { topologyModal.classList.add('hidden'); } window.closeNodeTopology = function() { topologyModal.classList.add('hidden'); }
let perfChartObj = null; let netChartObj = null; let liveInterval = null; // RRD UND LIVE GRAPH ENGINE
let perfChartObj = null; let netChartObj = null; let liveInterval = null; let currentGraphParams = {};
window.openLiveGraph = function(targetMode, vmid, host, nodeId, name) { 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})`; document.getElementById('liveGraphModal').classList.remove('hidden');
if(perfChartObj) perfChartObj.destroy(); if(netChartObj) netChartObj.destroy(); currentGraphParams = { mode: targetMode, vmid: vmid, host: host, nodeId: nodeId, name: name };
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' } } } } }); switchGraphTab('live');
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' } } } } }); }
window.switchGraphTab = async function(tab) {
if(liveInterval) clearInterval(liveInterval); if(liveInterval) clearInterval(liveInterval);
let prevNetIn = 0; let prevNetOut = 0; let prevTime = null; ['live', 'day', 'week'].forEach(t => {
const fetchLiveData = async () => { const b = document.getElementById('btn-graph-' + t);
try { if(t === tab) { b.classList.add('border-proxmox', 'text-white'); b.classList.remove('border-transparent', 'text-gray-400'); }
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}`; else { b.classList.remove('border-proxmox', 'text-white'); b.classList.add('border-transparent', 'text-gray-400'); }
const res = await (await fetch(endpoint)).json(); });
if(res.success && res.data) {
const d = res.data; const now = Date.now(); document.getElementById('graphModalTitle').innerText = `${tab === 'live' ? 'Live' : (tab === 'day' ? '24h' : '7 Tage')} Performance: ${currentGraphParams.name}`;
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); const dot = document.getElementById('graphStatusDot');
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); } if(tab === 'live') { dot.classList.add('animate-pulse', 'bg-green-500'); dot.classList.remove('bg-blue-500'); document.getElementById('graphSubText').innerText = 'Metriken werden live abgefragt.'; }
else { dot.classList.remove('animate-pulse', 'bg-green-500'); dot.classList.add('bg-blue-500'); document.getElementById('graphSubText').innerText = 'Historische RRD-Daten via Proxmox API.'; }
let currentNetIn = d.netin || 0; let currentNetOut = d.netout || 0;
let rxSpeed = 0; let txSpeed = 0; 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, pointRadius: tab==='live'?3:0, data: [] }, { label: 'RAM (%)', borderColor: '#E57000', backgroundColor: 'rgba(229, 112, 0, 0.1)', borderWidth: 2, tension: 0.4, fill: true, pointRadius: tab==='live'?3:0, data: [] }]}, options: { responsive: true, maintainAspectRatio: false, animation: { duration: tab==='live'?0:500 }, scales: { x: { grid: { color: '#33334d' }, ticks: { color: '#9ca3af' } }, y: { min: 0, max: 100, grid: { color: '#33334d' }, ticks: { color: '#9ca3af' } } }, plugins: { legend: { labels: { color: '#e2e8f0' } } } } });
if (d.is_rrd_net) { 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, pointRadius: tab==='live'?3:0, data: [] }, { label: 'TX (MB/s)', borderColor: '#8b5cf6', backgroundColor: 'rgba(139, 92, 246, 0.1)', borderWidth: 2, tension: 0.4, fill: true, pointRadius: tab==='live'?3:0, data: [] }]}, options: { responsive: true, maintainAspectRatio: false, animation: { duration: tab==='live'?0:500 }, scales: { x: { grid: { color: '#33334d' }, ticks: { color: '#9ca3af' } }, y: { min: 0, grid: { color: '#33334d' }, ticks: { color: '#9ca3af' } } }, plugins: { legend: { labels: { color: '#e2e8f0' } } } } });
rxSpeed = (currentNetIn / (1024 * 1024)).toFixed(2);
txSpeed = (currentNetOut / (1024 * 1024)).toFixed(2); if(tab === 'live') {
} else { let prevNetIn = 0; let prevNetOut = 0; let prevTime = null;
if(prevTime !== null) { const fetchLiveData = async () => {
const timeSec = (now - prevTime) / 1000; try {
if (timeSec > 0) { const endpoint = currentGraphParams.mode === 'node' ? `api.php?action=get_node_status&host=${currentGraphParams.host}&node_id=${currentGraphParams.nodeId}` : `api.php?action=get_vm_status&vmid=${currentGraphParams.vmid}&host=${currentGraphParams.host}&type=${currentGraphParams.mode}&node_id=${currentGraphParams.nodeId}`;
rxSpeed = Math.max(0, ((currentNetIn - prevNetIn) / timeSec / (1024 * 1024))).toFixed(2); const res = await (await fetch(endpoint)).json();
txSpeed = Math.max(0, ((currentNetOut - prevNetOut) / timeSec / (1024 * 1024))).toFixed(2); 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 (d.is_rrd_net) { rxSpeed = (currentNetIn / (1024 * 1024)).toFixed(2); txSpeed = (currentNetOut / (1024 * 1024)).toFixed(2); } else { if(prevTime !== null) { const timeSec = (now - prevTime) / 1000; if (timeSec > 0) { rxSpeed = Math.max(0, ((currentNetIn - prevNetIn) / timeSec / (1024 * 1024))).toFixed(2); txSpeed = Math.max(0, ((currentNetOut - prevNetOut) / timeSec / (1024 * 1024))).toFixed(2); } } }
prevNetIn = currentNetIn; prevNetOut = currentNetOut; prevTime = now;
const timeStr = new Date().toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
perfChartObj.data.labels.push(timeStr); perfChartObj.data.datasets[0].data.push(cpu); perfChartObj.data.datasets[1].data.push(ram); if(perfChartObj.data.labels.length > 30) { perfChartObj.data.labels.shift(); perfChartObj.data.datasets[0].data.shift(); perfChartObj.data.datasets[1].data.shift(); } perfChartObj.update();
if(prevTime !== null || d.is_rrd_net) { netChartObj.data.labels.push(timeStr); netChartObj.data.datasets[0].data.push(rxSpeed); netChartObj.data.datasets[1].data.push(txSpeed); if(netChartObj.data.labels.length > 30) { netChartObj.data.labels.shift(); netChartObj.data.datasets[0].data.shift(); netChartObj.data.datasets[1].data.shift(); } netChartObj.update(); }
} }
} catch(e) {}
prevNetIn = currentNetIn; prevNetOut = currentNetOut; prevTime = now; };
const timeStr = new Date().toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', second: '2-digit' }); fetchLiveData(); liveInterval = setInterval(fetchLiveData, 2000);
} else {
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(); try {
if(prevTime !== null || d.is_rrd_net) { netChartObj.data.labels.push(timeStr); netChartObj.data.datasets[0].data.push(rxSpeed); netChartObj.data.datasets[1].data.push(txSpeed); if(netChartObj.data.labels.length > 30) { netChartObj.data.labels.shift(); netChartObj.data.datasets[0].data.shift(); netChartObj.data.datasets[1].data.shift(); } netChartObj.update(); } const res = await (await fetch(`api.php?action=get_historical_rrd&timeframe=${tab}&target_mode=${currentGraphParams.mode}&vmid=${currentGraphParams.vmid}&host=${currentGraphParams.host}&node_id=${currentGraphParams.nodeId}`)).json();
if(res.success && res.data) {
res.data.forEach(d => {
const date = new Date(d.time * 1000); const l = tab === 'day' ? date.toLocaleTimeString('de-DE',{hour:'2-digit',minute:'2-digit'}) : date.toLocaleDateString('de-DE',{weekday:'short', hour:'2-digit'});
perfChartObj.data.labels.push(l); perfChartObj.data.datasets[0].data.push((d.cpu || 0) * 100);
let ram = 0; if (d.maxmem > 0) { ram = ((d.mem / d.maxmem) * 100).toFixed(1); } perfChartObj.data.datasets[1].data.push(ram);
netChartObj.data.labels.push(l); netChartObj.data.datasets[0].data.push((d.netin || 0) / (1024*1024)); netChartObj.data.datasets[1].data.push((d.netout || 0) / (1024*1024));
});
perfChartObj.update(); netChartObj.update();
} }
} catch(e) {} } catch(e) {}
}; }
fetchLiveData(); liveInterval = setInterval(fetchLiveData, 2000);
} }
window.closeLiveGraph = function() { document.getElementById('liveGraphModal').classList.add('hidden'); if(liveInterval) clearInterval(liveInterval); } window.closeLiveGraph = function() { document.getElementById('liveGraphModal').classList.add('hidden'); if(liveInterval) clearInterval(liveInterval); }
window.openVncConsole = function(ip, node, type, vmid, name) { 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"); }
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) { 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); }
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'); } window.closePbsDatastore = function() { document.getElementById('pbsDatastoreModal').classList.add('hidden'); }
async function loadPbsBackups(nodeId, storeName) { const container = document.getElementById('pbsBackupsContainer'); container.innerHTML = '<p class="text-gray-500 animate-pulse">Durchsuche Namespaces nach Backups...</p>'; 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 = '<p class="text-gray-500 text-sm">Keine Backups vorhanden.</p>'; 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 ? `<span class="text-pbs font-normal ml-2">[${b.ns}]</span>` : `<span class="text-gray-500 font-normal ml-2">[Root]</span>`; container.innerHTML += `<div class="flex justify-between items-center bg-darkbg p-2 border border-darkborder rounded mb-2 hover:border-pbs transition-colors"><div><p class="text-white text-xs font-bold">${b['backup-type']} / ${b['backup-id']} ${nsLabel}</p><p class="text-xs text-gray-500">${timeStr} | ${sizeStr}</p></div><button onclick="deletePbsSnapshot('${b['backup-type']}', '${b['backup-id']}', ${b['backup-time']}, '${b.ns || ''}')" class="text-red-500 hover:text-white text-xs font-bold px-3 py-1 bg-red-500/10 hover:bg-red-500 rounded transition-colors">Löschen</button></div>`; }); } } 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 = '<p class="text-gray-500 animate-pulse">Lade System-Jobs...</p>'; const table = document.getElementById('pbsJobsTable'); table.innerHTML = '<tr><td colspan="5" class="text-center py-4 text-gray-500">Lade Historie...</td></tr>'; 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 += `<div class="flex justify-between items-center bg-darkbg p-2 border border-darkborder rounded mb-1"><div><p class="text-white text-xs font-bold uppercase">${job.worker_type}</p><p class="text-xs text-gray-500">${timeStr}</p></div><div class="flex items-center gap-2"><span class="${statusColor} font-bold text-xs uppercase mr-2">${job.status || 'Active'}</span><button onclick="openTaskLog('${job.upid}', ${nodeId}, 'localhost')" class="text-gray-400 hover:text-white text-xs bg-darkcard px-2 py-1 rounded border border-darkborder transition-colors">📄 Log</button></div></div>`; mixCount++; } table.innerHTML += `<tr class="hover:bg-darkbg transition-colors border-b border-darkborder/50"><td class="px-4 py-2">${timeStr}</td><td class="px-4 py-2 font-medium text-white">${job.worker_type}</td><td class="px-4 py-2 ${statusColor} font-bold uppercase">${job.status || 'Active'}</td><td class="px-4 py-2">${runtime}</td><td class="px-4 py-2 text-right"><button onclick="openTaskLog('${job.upid}', ${nodeId}, 'localhost')" class="text-pbs hover:text-white font-bold text-xs transition-colors">Ansehen</button></td></tr>`; }); if(mixCount === 0) mixCont.innerHTML = '<p class="text-gray-500 text-xs italic">Keine System-Jobs gefunden.</p>'; if(res.data.length === 0) table.innerHTML = '<tr><td colspan="5" class="text-center py-4 text-gray-500">Keine Historie gefunden.</td></tr>'; } } catch(e) {} }
async function loadPbsSyncJobs(nodeId) { const container = document.getElementById('pbsSyncContainer'); container.innerHTML = '<p class="text-gray-500 animate-pulse">Lade Sync-Jobs...</p>'; 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 = '<p class="text-gray-500 text-sm">Keine Sync-Jobs eingerichtet.</p>'; return; } container.innerHTML = ''; res.data.forEach(sync => { container.innerHTML += `<div class="bg-darkbg p-3 border border-darkborder rounded mb-2 hover:border-blue-500 transition-colors"><h4 class="text-white text-sm font-bold truncate">${sync.id}</h4><p class="text-xs text-gray-400 mt-1">Quelle: <span class="text-blue-400">${sync.remote || 'Lokal'} -> ${sync['remote-store']}</span></p><p class="text-xs text-gray-400">Ziel: <span class="text-emerald-400">${sync.store}</span></p><div class="mt-2 text-xs text-gray-500 bg-darkcard p-1.5 rounded inline-block">Zeitplan: ${sync.schedule || 'Manuell'}</div></div>`; }); } } catch(e) { container.innerHTML = 'Fehler.'; } }
async function loadPbsBackups(nodeId, storeName) { 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) {} }
const container = document.getElementById('pbsBackupsContainer'); container.innerHTML = '<p class="text-gray-500 animate-pulse">Durchsuche Namespaces nach Backups...</p>';
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 = '<p class="text-gray-500 text-sm">Keine Backups vorhanden.</p>'; 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 ? `<span class="text-pbs font-normal ml-2">[${b.ns}]</span>` : `<span class="text-gray-500 font-normal ml-2">[Root]</span>`;
container.innerHTML += `
<div class="flex justify-between items-center bg-darkbg p-2 border border-darkborder rounded mb-2 hover:border-pbs transition-colors">
<div><p class="text-white text-xs font-bold">${b['backup-type']} / ${b['backup-id']} ${nsLabel}</p><p class="text-xs text-gray-500">${timeStr} | ${sizeStr}</p></div>
<button onclick="deletePbsSnapshot('${b['backup-type']}', '${b['backup-id']}', ${b['backup-time']}, '${b.ns || ''}')" class="text-red-500 hover:text-white text-xs font-bold px-3 py-1 bg-red-500/10 hover:bg-red-500 rounded transition-colors">Löschen</button>
</div>`;
});
}
} 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 = '<p class="text-gray-500 animate-pulse">Lade System-Jobs...</p>';
const table = document.getElementById('pbsJobsTable'); table.innerHTML = '<tr><td colspan="5" class="text-center py-4 text-gray-500">Lade Historie...</td></tr>';
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 += `<div class="flex justify-between items-center bg-darkbg p-2 border border-darkborder rounded mb-1"><div><p class="text-white text-xs font-bold uppercase">${job.worker_type}</p><p class="text-xs text-gray-500">${timeStr}</p></div><div class="flex items-center gap-2"><span class="${statusColor} font-bold text-xs uppercase mr-2">${job.status || 'Active'}</span><button onclick="openTaskLog('${job.upid}', ${nodeId}, 'localhost')" class="text-gray-400 hover:text-white text-xs bg-darkcard px-2 py-1 rounded border border-darkborder transition-colors">📄 Log</button></div></div>`;
mixCount++;
}
table.innerHTML += `<tr class="hover:bg-darkbg transition-colors border-b border-darkborder/50"><td class="px-4 py-2">${timeStr}</td><td class="px-4 py-2 font-medium text-white">${job.worker_type}</td><td class="px-4 py-2 ${statusColor} font-bold uppercase">${job.status || 'Active'}</td><td class="px-4 py-2">${runtime}</td><td class="px-4 py-2 text-right"><button onclick="openTaskLog('${job.upid}', ${nodeId}, 'localhost')" class="text-pbs hover:text-white font-bold text-xs transition-colors">Ansehen</button></td></tr>`;
});
if(mixCount === 0) mixCont.innerHTML = '<p class="text-gray-500 text-xs italic">Keine System-Jobs gefunden.</p>';
if(res.data.length === 0) table.innerHTML = '<tr><td colspan="5" class="text-center py-4 text-gray-500">Keine Historie gefunden.</td></tr>';
}
} catch(e) {}
}
async function loadPbsSyncJobs(nodeId) {
const container = document.getElementById('pbsSyncContainer'); container.innerHTML = '<p class="text-gray-500 animate-pulse">Lade Sync-Jobs...</p>';
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 = '<p class="text-gray-500 text-sm">Keine Sync-Jobs eingerichtet.</p>'; return; }
container.innerHTML = '';
res.data.forEach(sync => {
container.innerHTML += `<div class="bg-darkbg p-3 border border-darkborder rounded mb-2 hover:border-blue-500 transition-colors"><h4 class="text-white text-sm font-bold truncate">${sync.id}</h4><p class="text-xs text-gray-400 mt-1">Quelle: <span class="text-blue-400">${sync.remote || 'Lokal'} -> ${sync['remote-store']}</span></p><p class="text-xs text-gray-400">Ziel: <span class="text-emerald-400">${sync.store}</span></p><div class="mt-2 text-xs text-gray-500 bg-darkcard p-1.5 rounded inline-block">Zeitplan: ${sync.schedule || 'Manuell'}</div></div>`;
});
}
} 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.closeTaskLog = function() { document.getElementById('taskLogModal').classList.add('hidden'); }
window.openPmgManager = function(nodeId, hostName, internalName) { 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'); }
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.closePmgManager = function() { document.getElementById('pmgManagerModal').classList.add('hidden'); }
window.switchPmgTab = function(tabName) { 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); } }
['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) { window.loadPmgSpamQueue = async function(nodeId) { const table = document.getElementById('pmgSpamTable'); table.innerHTML = '<tr><td colspan="6" class="text-center py-6 text-gray-500 animate-pulse">Lade Spam Quarantäne...</td></tr>'; 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 = '<tr><td colspan="6" class="text-center py-6 text-gray-500">🎉 Keine Mails in der Quarantäne.</td></tr>'; 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 += `<tr class="hover:bg-darkcard/50 transition-colors border-b border-darkborder/50"><td class="px-4 py-3 whitespace-nowrap">${timeStr}</td><td class="px-4 py-3 text-white truncate max-w-[200px]" title="${sender}">${sender}</td><td class="px-4 py-3 text-gray-300 truncate max-w-[200px]" title="${receiver}">${receiver}</td><td class="px-4 py-3 truncate max-w-[250px]" title="${subject}">${subject}</td><td class="px-4 py-3 text-red-400 font-bold text-center">${score}</td><td class="px-4 py-3 text-right whitespace-nowrap"><button onclick="pmgSpamAction('deliver', '${mail.id}')" class="text-green-500 hover:text-white px-2 py-1.5 bg-green-500/10 hover:bg-green-500 rounded text-xs font-bold transition-colors mr-2">Deliver</button><button onclick="pmgSpamAction('delete', '${mail.id}')" class="text-red-500 hover:text-white px-2 py-1.5 bg-red-500/10 hover:bg-red-500 rounded text-xs font-bold transition-colors">Delete</button></td></tr>`; }); } else { table.innerHTML = '<tr><td colspan="6" class="text-center py-6 text-red-500">Fehler beim Laden.</td></tr>'; } } catch(e) { table.innerHTML = '<tr><td colspan="6" class="text-center py-6 text-red-500">Netzwerkfehler.</td></tr>'; } }
const table = document.getElementById('pmgSpamTable'); 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'); } }
table.innerHTML = '<tr><td colspan="6" class="text-center py-6 text-gray-500 animate-pulse">Lade Spam Quarantäne...</td></tr>';
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 = '<tr><td colspan="6" class="text-center py-6 text-gray-500">🎉 Keine Mails in der Quarantäne.</td></tr>'; 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 += `<tr class="hover:bg-darkcard/50 transition-colors border-b border-darkborder/50"><td class="px-4 py-3 whitespace-nowrap">${timeStr}</td><td class="px-4 py-3 text-white truncate max-w-[200px]" title="${sender}">${sender}</td><td class="px-4 py-3 text-gray-300 truncate max-w-[200px]" title="${receiver}">${receiver}</td><td class="px-4 py-3 truncate max-w-[250px]" title="${subject}">${subject}</td><td class="px-4 py-3 text-red-400 font-bold text-center">${score}</td><td class="px-4 py-3 text-right whitespace-nowrap"><button onclick="pmgSpamAction('deliver', '${mail.id}')" class="text-green-500 hover:text-white px-2 py-1.5 bg-green-500/10 hover:bg-green-500 rounded text-xs font-bold transition-colors mr-2">Deliver</button><button onclick="pmgSpamAction('delete', '${mail.id}')" class="text-red-500 hover:text-white px-2 py-1.5 bg-red-500/10 hover:bg-red-500 rounded text-xs font-bold transition-colors">Delete</button></td></tr>`;
});
} else { table.innerHTML = '<tr><td colspan="6" class="text-center py-6 text-red-500">Fehler beim Laden.</td></tr>'; }
} catch(e) { table.innerHTML = '<tr><td colspan="6" class="text-center py-6 text-red-500">Netzwerkfehler.</td></tr>'; }
}
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() { 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 = '<tr><td colspan="4" class="text-center py-6 text-gray-500 animate-pulse">Lese Syslog (Letzte 24h)...</td></tr>'; 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 = `<tr><td colspan="4" class="text-center py-6 text-gray-500">Keine Logs für diesen Filter gefunden.</td></tr>`; 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 += `<tr class="hover:bg-darkcard/50 transition-colors border-b border-darkborder/50"><td class="px-4 py-3 whitespace-nowrap text-gray-400">${timeStr}</td><td class="px-4 py-3 text-white truncate max-w-[200px]">${sender}</td><td class="px-4 py-3 text-gray-300 truncate max-w-[200px]">${receiver}</td><td class="px-4 py-3 font-bold uppercase ${statusColor} truncate max-w-[300px]" title="${statusTxt}">${statusTxt}</td></tr>`; }); } else { table.innerHTML = '<tr><td colspan="4" class="text-center py-6 text-red-500">Syslog konnte nicht gelesen werden.</td></tr>'; } } catch(e) { table.innerHTML = '<tr><td colspan="4" class="text-center py-6 text-red-500">Netzwerkfehler.</td></tr>'; } }
const nodeId = document.getElementById('pmgNodeId').value; const internalName = document.getElementById('pmgInternalName').value; const filter = document.getElementById('pmgTrackingFilter').value.trim(); const table = document.getElementById('pmgTrackingTable'); 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 = '<tr><td colspan="5" class="text-center py-6 text-gray-500 animate-pulse">Lese Warteschlangen aus Postfix...</td></tr>'; summary.innerHTML = '<div class="col-span-3 text-center text-gray-500 animate-pulse">Lade...</div>'; 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 = `<div class="bg-darkcard border ${activeCount > 0 ? 'border-blue-500/50 shadow-[0_0_15px_rgba(59,130,246,0.1)]' : 'border-darkborder'} rounded-xl p-5 text-center transition-all"><p class="text-gray-400 text-sm font-bold uppercase mb-1">Active Queue</p><h4 class="text-3xl font-bold ${activeCount > 0 ? 'text-blue-500' : 'text-white'}">${activeCount}</h4></div><div class="bg-darkcard border ${deferCount > 0 ? 'border-orange-500/50 shadow-[0_0_15px_rgba(249,115,22,0.1)]' : 'border-darkborder'} rounded-xl p-5 text-center transition-all"><p class="text-gray-400 text-sm font-bold uppercase mb-1">Deferred (Greylisted/Wartend)</p><h4 class="text-3xl font-bold ${deferCount > 0 ? 'text-orange-500' : 'text-white'}">${deferCount}</h4></div><div class="bg-darkcard border ${holdCount > 0 ? 'border-red-500/50 shadow-[0_0_15px_rgba(239,68,68,0.1)]' : 'border-darkborder'} rounded-xl p-5 text-center"><p class="text-gray-400 text-sm font-bold uppercase mb-1">Other / Hold</p><h4 class="text-3xl font-bold ${holdCount > 0 ? 'text-red-500' : 'text-white'}">${holdCount}</h4></div>`; table.innerHTML = ''; if(res.data.length === 0) { table.innerHTML = `<tr><td colspan="5" class="text-center py-6 text-gray-500">Alle Postfix-Queues sind komplett leer.</td></tr>`; 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 += `<tr class="hover:bg-darkcard/50 transition-colors border-b border-darkborder/50"><td class="px-4 py-3 font-bold uppercase ${qColor}">${queueName}</td><td class="px-4 py-3 whitespace-nowrap text-gray-400">${timeStr}</td><td class="px-4 py-3 text-white truncate max-w-[200px]" title="${sender}">${sender}</td><td class="px-4 py-3 text-gray-300 truncate max-w-[200px]" title="${receiver}">${receiver}</td><td class="px-4 py-3 text-xs text-gray-500 truncate max-w-[300px]" title="${reason}">${reason}</td></tr>`; }); } else { table.innerHTML = '<tr><td colspan="5" class="text-center py-6 text-red-500">Queues konnten nicht gelesen werden.</td></tr>'; } } catch(e) { table.innerHTML = '<tr><td colspan="5" class="text-center py-6 text-red-500">Netzwerkfehler.</td></tr>'; } }
table.innerHTML = '<tr><td colspan="4" class="text-center py-6 text-gray-500 animate-pulse">Lese Syslog (Letzte 24h)...</td></tr>'; 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) {} }
try { 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; } }
const res = await (await fetch(`api.php?action=pmg_get_tracking&node_id=${nodeId}&internal_name=${internalName}&filter=${encodeURIComponent(filter)}`)).json(); window.loadPmgDomains = async function(nodeId) { const container = document.getElementById('pmgDomainsContainer'); container.innerHTML = '<p class="text-gray-500 text-sm animate-pulse">Lade...</p>'; 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 = '<p class="text-gray-500 text-sm">Keine Domains.</p>'; return; } container.innerHTML = ''; res.data.forEach(d => { container.innerHTML += `<div class="flex justify-between items-center bg-darkbg p-2 border border-darkborder rounded mb-1"><span class="text-white text-sm font-bold">${d.domain}</span><button onclick="deletePmgDomain('${d.domain}')" class="text-red-500 hover:text-red-400 text-xs font-bold">Löschen</button></div>`; }); } else { container.innerHTML = '<p class="text-red-500 text-sm">Fehler.</p>'; } } catch(e) {} }
if(res.success && res.data) { 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.'); }
table.innerHTML = ''; 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.'); }
if(res.data.length === 0) { table.innerHTML = `<tr><td colspan="4" class="text-center py-6 text-gray-500">Keine Logs für diesen Filter gefunden.</td></tr>`; return; } window.loadPmgNetworks = async function(nodeId) { const container = document.getElementById('pmgNetworksContainer'); container.innerHTML = '<p class="text-gray-500 text-sm animate-pulse">Lade...</p>'; 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 = '<p class="text-gray-500 text-sm">Keine Netzwerke.</p>'; return; } container.innerHTML = ''; res.data.forEach(n => { container.innerHTML += `<div class="flex justify-between items-center bg-darkbg p-2 border border-darkborder rounded mb-1"><span class="text-white text-sm font-bold">${n.cidr}</span><button onclick="deletePmgNetwork('${n.cidr}')" class="text-red-500 hover:text-red-400 text-xs font-bold">Löschen</button></div>`; }); } else { container.innerHTML = '<p class="text-red-500 text-sm">Fehler.</p>'; } } catch(e) {} }
res.data.forEach(log => { 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.'); }
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 || '-'; 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.'); }
let statusTxt = 'Unbekannt'; let statusColor = 'text-gray-400'; window.loadPmgSsl = async function(nodeId) { const internalName = document.getElementById('pmgInternalName').value; const container = document.getElementById('pmgSslContainer'); container.innerHTML = '<p class="text-gray-500 animate-pulse text-sm">Lade Zertifikate...</p>'; 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 ? '<span class="w-2 h-2 rounded-full bg-red-500 shrink-0"></span>' : '<span class="w-2 h-2 rounded-full bg-green-500 shrink-0"></span>'; container.innerHTML += `<div class="bg-darkbg border border-darkborder rounded p-3 text-sm flex gap-3"><div class="mt-1">${statusDot}</div><div class="flex-1 overflow-hidden"><h4 class="text-white font-bold mb-1 break-all">${cert.filename || cert.subject || 'Zertifikat'}</h4><p class="text-gray-400 text-xs mb-1"><span class="font-bold text-gray-500">Aussteller:</span> ${cert.issuer || '-'}</p><p class="text-gray-400 text-xs truncate" title="${cert.subject}"><span class="font-bold text-gray-500">Subject:</span> ${cert.subject || '-'}</p><div class="mt-2 text-xs font-bold ${isExpired ? 'text-red-400' : 'text-emerald-400'} bg-darkcard inline-block px-2 py-1 rounded border border-darkborder">Ablaufdatum: ${validUntil}</div></div></div>`; }); } else { container.innerHTML = '<p class="text-red-500 text-sm">Fehler beim Laden.</p>'; } } catch(e) { container.innerHTML = '<p class="text-red-500 text-sm">Netzwerkfehler.</p>'; } }
if (log.dstatus) { 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; } }
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 += `<tr class="hover:bg-darkcard/50 transition-colors border-b border-darkborder/50"><td class="px-4 py-3 whitespace-nowrap text-gray-400">${timeStr}</td><td class="px-4 py-3 text-white truncate max-w-[200px]">${sender}</td><td class="px-4 py-3 text-gray-300 truncate max-w-[200px]">${receiver}</td><td class="px-4 py-3 font-bold uppercase ${statusColor} truncate max-w-[300px]" title="${statusTxt}">${statusTxt}</td></tr>`;
});
} else { table.innerHTML = '<tr><td colspan="4" class="text-center py-6 text-red-500">Syslog konnte nicht gelesen werden.</td></tr>'; }
} catch(e) { table.innerHTML = '<tr><td colspan="4" class="text-center py-6 text-red-500">Netzwerkfehler.</td></tr>'; }
}
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 = '<tr><td colspan="5" class="text-center py-6 text-gray-500 animate-pulse">Lese Warteschlangen aus Postfix...</td></tr>'; summary.innerHTML = '<div class="col-span-3 text-center text-gray-500 animate-pulse">Lade...</div>';
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 = `<div class="bg-darkcard border ${activeCount > 0 ? 'border-blue-500/50 shadow-[0_0_15px_rgba(59,130,246,0.1)]' : 'border-darkborder'} rounded-xl p-5 text-center transition-all"><p class="text-gray-400 text-sm font-bold uppercase mb-1">Active Queue</p><h4 class="text-3xl font-bold ${activeCount > 0 ? 'text-blue-500' : 'text-white'}">${activeCount}</h4></div><div class="bg-darkcard border ${deferCount > 0 ? 'border-orange-500/50 shadow-[0_0_15px_rgba(249,115,22,0.1)]' : 'border-darkborder'} rounded-xl p-5 text-center transition-all"><p class="text-gray-400 text-sm font-bold uppercase mb-1">Deferred (Greylisted/Wartend)</p><h4 class="text-3xl font-bold ${deferCount > 0 ? 'text-orange-500' : 'text-white'}">${deferCount}</h4></div><div class="bg-darkcard border ${holdCount > 0 ? 'border-red-500/50 shadow-[0_0_15px_rgba(239,68,68,0.1)]' : 'border-darkborder'} rounded-xl p-5 text-center"><p class="text-gray-400 text-sm font-bold uppercase mb-1">Other / Hold</p><h4 class="text-3xl font-bold ${holdCount > 0 ? 'text-red-500' : 'text-white'}">${holdCount}</h4></div>`;
table.innerHTML = '';
if(res.data.length === 0) { table.innerHTML = `<tr><td colspan="5" class="text-center py-6 text-gray-500">Alle Postfix-Queues sind komplett leer.</td></tr>`; 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 += `<tr class="hover:bg-darkcard/50 transition-colors border-b border-darkborder/50"><td class="px-4 py-3 font-bold uppercase ${qColor}">${queueName}</td><td class="px-4 py-3 whitespace-nowrap text-gray-400">${timeStr}</td><td class="px-4 py-3 text-white truncate max-w-[200px]" title="${sender}">${sender}</td><td class="px-4 py-3 text-gray-300 truncate max-w-[200px]" title="${receiver}">${receiver}</td><td class="px-4 py-3 text-xs text-gray-500 truncate max-w-[300px]" title="${reason}">${reason}</td></tr>`;
});
} else { table.innerHTML = '<tr><td colspan="5" class="text-center py-6 text-red-500">Queues konnten nicht gelesen werden.</td></tr>'; }
} catch(e) { table.innerHTML = '<tr><td colspan="5" class="text-center py-6 text-red-500">Netzwerkfehler.</td></tr>'; }
}
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 = '<p class="text-gray-500 text-sm animate-pulse">Lade...</p>';
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 = '<p class="text-gray-500 text-sm">Keine Domains.</p>'; return; }
container.innerHTML = '';
res.data.forEach(d => { container.innerHTML += `<div class="flex justify-between items-center bg-darkbg p-2 border border-darkborder rounded mb-1"><span class="text-white text-sm font-bold">${d.domain}</span><button onclick="deletePmgDomain('${d.domain}')" class="text-red-500 hover:text-red-400 text-xs font-bold">Löschen</button></div>`; });
} else { container.innerHTML = '<p class="text-red-500 text-sm">Fehler.</p>'; }
} 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 = '<p class="text-gray-500 text-sm animate-pulse">Lade...</p>';
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 = '<p class="text-gray-500 text-sm">Keine Netzwerke.</p>'; return; }
container.innerHTML = '';
res.data.forEach(n => { container.innerHTML += `<div class="flex justify-between items-center bg-darkbg p-2 border border-darkborder rounded mb-1"><span class="text-white text-sm font-bold">${n.cidr}</span><button onclick="deletePmgNetwork('${n.cidr}')" class="text-red-500 hover:text-red-400 text-xs font-bold">Löschen</button></div>`; });
} else { container.innerHTML = '<p class="text-red-500 text-sm">Fehler.</p>'; }
} 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 = '<p class="text-gray-500 animate-pulse text-sm">Lade Zertifikate...</p>';
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 ? '<span class="w-2 h-2 rounded-full bg-red-500 shrink-0"></span>' : '<span class="w-2 h-2 rounded-full bg-green-500 shrink-0"></span>';
container.innerHTML += `<div class="bg-darkbg border border-darkborder rounded p-3 text-sm flex gap-3"><div class="mt-1">${statusDot}</div><div class="flex-1 overflow-hidden"><h4 class="text-white font-bold mb-1 break-all">${cert.filename || cert.subject || 'Zertifikat'}</h4><p class="text-gray-400 text-xs mb-1"><span class="font-bold text-gray-500">Aussteller:</span> ${cert.issuer || '-'}</p><p class="text-gray-400 text-xs truncate" title="${cert.subject}"><span class="font-bold text-gray-500">Subject:</span> ${cert.subject || '-'}</p><div class="mt-2 text-xs font-bold ${isExpired ? 'text-red-400' : 'text-emerald-400'} bg-darkcard inline-block px-2 py-1 rounded border border-darkborder">Ablaufdatum: ${validUntil}</div></div></div>`;
});
} else { container.innerHTML = '<p class="text-red-500 text-sm">Fehler beim Laden.</p>'; }
} catch(e) { container.innerHTML = '<p class="text-red-500 text-sm">Netzwerkfehler.</p>'; }
}
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; }
}
const cronModal = document.getElementById('cronManagerModal'); const cronModal = document.getElementById('cronManagerModal');
window.openCronManager = async function() { window.openCronManager = async function() { cronModal.classList.remove('hidden'); const select = document.getElementById('cronNodeId'); select.innerHTML = '<option value="">Lade Hosts...</option>'; try { const res = await (await fetch('api.php?action=get_nodes')).json(); if (res.success) { select.innerHTML = '<option value="">-- PVE Node auswählen --</option>'; res.data.filter(n => n.type === 'pve').forEach(node => { select.innerHTML += `<option value="${node.id}">${node.name} (${node.ip_address})</option>`; }); } } catch(e) {} loadCronJobs(); }
cronModal.classList.remove('hidden');
const select = document.getElementById('cronNodeId'); select.innerHTML = '<option value="">Lade Hosts...</option>';
try {
const res = await (await fetch('api.php?action=get_nodes')).json();
if (res.success) {
select.innerHTML = '<option value="">-- PVE Node auswählen --</option>';
res.data.filter(n => n.type === 'pve').forEach(node => { select.innerHTML += `<option value="${node.id}">${node.name} (${node.ip_address})</option>`; });
}
} catch(e) {}
loadCronJobs();
}
window.closeCronManager = function() { cronModal.classList.add('hidden'); } 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'); } 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 = '<tr><td colspan="6" class="text-center text-gray-500 py-4 animate-pulse">Lade Jobs...</td></tr>'; 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 = '<tr><td colspan="6" class="text-center text-gray-500 py-4">Keine geplanten Jobs.</td></tr>'; return; } res.data.forEach(job => { const lastRun = formatDate(job.last_run); const isActive = parseInt(job.is_active) === 1; const statusDot = isActive ? '<span class="w-2 h-2 rounded-full bg-green-500 shadow-[0_0_5px_#22c55e]"></span> Aktiv' : '<span class="w-2 h-2 rounded-full bg-gray-600"></span> 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 += `<tr class="hover:bg-darkcard/50 transition-colors border-b border-darkborder/50"><td class="px-4 py-3 whitespace-nowrap cursor-pointer" onclick="toggleCronJob(${job.id}, ${isActive ? 0 : 1})"><div class="flex items-center gap-2 text-xs font-bold text-gray-300 hover:text-white">${statusDot}</div></td><td class="px-4 py-3"><p class="text-white font-bold">${job.name}</p><p class="text-xs text-proxmox font-bold">${actionStr}</p></td><td class="px-4 py-3"><p class="text-gray-300">${job.node_name}</p><p class="text-xs text-gray-500">Ziel: ${targetStr}</p></td><td class="px-4 py-3 font-mono text-blue-400 font-bold tracking-widest">${job.cron_schedule}</td><td class="px-4 py-3 text-gray-400 whitespace-nowrap">${lastRun}</td><td class="px-4 py-3 text-right"><button onclick="deleteCronJob(${job.id})" class="text-red-500 hover:text-white px-2 py-1 bg-red-500/10 hover:bg-red-500 rounded text-xs font-bold transition-colors">Löschen</button></td></tr>`; }); } } catch(e) {} }
async function loadCronJobs() {
const tbody = document.getElementById('cronTableBody'); tbody.innerHTML = '<tr><td colspan="6" class="text-center text-gray-500 py-4 animate-pulse">Lade Jobs...</td></tr>'; window.toggleCronDate = function() { const d = document.getElementById('cronDays').value; const i = document.getElementById('cronSpecificDate'); if (d === 'specific') { i.classList.remove('hidden'); i.required = true; } else { i.classList.add('hidden'); i.required = false; } }
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 = '<tr><td colspan="6" class="text-center text-gray-500 py-4">Keine geplanten Jobs.</td></tr>'; return; }
res.data.forEach(job => {
const lastRun = formatDate(job.last_run); const isActive = parseInt(job.is_active) === 1;
const statusDot = isActive ? '<span class="w-2 h-2 rounded-full bg-green-500 shadow-[0_0_5px_#22c55e]"></span> Aktiv' : '<span class="w-2 h-2 rounded-full bg-gray-600"></span> 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 += `<tr class="hover:bg-darkcard/50 transition-colors border-b border-darkborder/50"><td class="px-4 py-3 whitespace-nowrap cursor-pointer" onclick="toggleCronJob(${job.id}, ${isActive ? 0 : 1})"><div class="flex items-center gap-2 text-xs font-bold text-gray-300 hover:text-white">${statusDot}</div></td><td class="px-4 py-3"><p class="text-white font-bold">${job.name}</p><p class="text-xs text-proxmox font-bold">${actionStr}</p></td><td class="px-4 py-3"><p class="text-gray-300">${job.node_name}</p><p class="text-xs text-gray-500">Ziel: ${targetStr}</p></td><td class="px-4 py-3 font-mono text-blue-400 font-bold tracking-widest">${job.cron_schedule}</td><td class="px-4 py-3 text-gray-400 whitespace-nowrap">${lastRun}</td><td class="px-4 py-3 text-right"><button onclick="deleteCronJob(${job.id})" class="text-red-500 hover:text-white px-2 py-1 bg-red-500/10 hover:bg-red-500 rounded text-xs font-bold transition-colors">Löschen</button></td></tr>`;
});
}
} catch(e) {}
}
const addCronForm = document.getElementById('addCronJobForm'); const addCronForm = document.getElementById('addCronJobForm');
if(addCronForm) { if(addCronForm) {
addCronForm.addEventListener('submit', async function(e) { addCronForm.addEventListener('submit', async function(e) {
e.preventDefault(); const btn = this.querySelector('button[type="submit"]'); const oTxt = btn.innerText; btn.innerText = 'Speichere...'; 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; } const timeVal = document.getElementById('cronTime').value;
const daysVal = document.getElementById('cronDays').value;
const [hour, minute] = timeVal.split(':');
let generatedCronStr = '';
if (daysVal === 'specific') {
const dateVal = document.getElementById('cronSpecificDate').value;
if (!dateVal) { btn.innerText = oTxt; return alert('Bitte Datum wählen!'); }
const dParts = dateVal.split('-'); // YYYY-MM-DD
generatedCronStr = `${parseInt(minute, 10)} ${parseInt(hour, 10)} ${parseInt(dParts[2], 10)} ${parseInt(dParts[1], 10)} *`;
} else {
generatedCronStr = `${parseInt(minute, 10)} ${parseInt(hour, 10)} * * ${daysVal}`;
}
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', generatedCronStr);
try { const res = await (await fetch('api.php?action=add_cron_job', {method: 'POST', body: fd})).json(); if(res.success) { addCronForm.reset(); document.getElementById('cronTime').value = '03:00'; toggleCronDate(); checkCronAction(); loadCronJobs(); } else alert('Fehler.'); } catch(e) {} finally { btn.innerText = oTxt; }
}); });
} }
@@ -659,23 +329,6 @@ if (window.APP.isLoggedIn && window.APP.nodeCount > 0) {
window.deleteCronJob = async function(id) { if(!confirm('Diesen geplanten Job wirklich löschen?')) return; const fd = new FormData(); fd.append('id', id); await fetch('api.php?action=delete_cron_job', {method: 'POST', body: fd}); loadCronJobs(); } window.deleteCronJob = async function(id) { if(!confirm('Diesen geplanten Job wirklich löschen?')) return; const fd = new FormData(); fd.append('id', id); await fetch('api.php?action=delete_cron_job', {method: 'POST', body: fd}); loadCronJobs(); }
const auditModal = document.getElementById('auditLogModal'); const auditModal = document.getElementById('auditLogModal');
window.openAuditLog = async function() { window.openAuditLog = async function() { auditModal.classList.remove('hidden'); const tbody = document.getElementById('auditLogTableBody'); tbody.innerHTML = '<tr><td colspan="4" class="text-center text-gray-500 py-4 animate-pulse">Lade Protokoll...</td></tr>'; 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 = '<tr><td colspan="4" class="text-center text-gray-500 py-4">Noch keine Einträge.</td></tr>'; return; } res.data.forEach(log => { const d = new Date(log.timestamp + 'Z'); const timeStr = d.toLocaleDateString('de-DE') + ' ' + d.toLocaleTimeString('de-DE'); const isSystem = log.username === 'System'; tbody.innerHTML += `<tr class="hover:bg-darkcard/50 transition-colors border-b border-darkborder/50"><td class="px-4 py-3 text-gray-400 whitespace-nowrap text-xs">${timeStr}</td><td class="px-4 py-3"><span class="${isSystem ? 'text-gray-500' : 'text-blue-400 font-bold'}">${log.username}</span></td><td class="px-4 py-3 font-bold text-white">${log.action}</td><td class="px-4 py-3 text-gray-300 text-xs">${log.target || '-'}</td></tr>`; }); } } catch(e) { tbody.innerHTML = '<tr><td colspan="4" class="text-center text-red-500 py-4">Fehler beim Laden.</td></tr>'; } }
auditModal.classList.remove('hidden');
const tbody = document.getElementById('auditLogTableBody');
tbody.innerHTML = '<tr><td colspan="4" class="text-center text-gray-500 py-4 animate-pulse">Lade Protokoll...</td></tr>';
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 = '<tr><td colspan="4" class="text-center text-gray-500 py-4">Noch keine Einträge.</td></tr>'; return; }
res.data.forEach(log => {
const d = new Date(log.timestamp + 'Z');
const timeStr = d.toLocaleDateString('de-DE') + ' ' + d.toLocaleTimeString('de-DE');
const isSystem = log.username === 'System';
tbody.innerHTML += `<tr class="hover:bg-darkcard/50 transition-colors border-b border-darkborder/50"><td class="px-4 py-3 text-gray-400 whitespace-nowrap text-xs">${timeStr}</td><td class="px-4 py-3"><span class="${isSystem ? 'text-gray-500' : 'text-blue-400 font-bold'}">${log.username}</span></td><td class="px-4 py-3 font-bold text-white">${log.action}</td><td class="px-4 py-3 text-gray-300 text-xs">${log.target || '-'}</td></tr>`;
});
}
} catch(e) { tbody.innerHTML = '<tr><td colspan="4" class="text-center text-red-500 py-4">Fehler beim Laden.</td></tr>'; }
}
window.closeAuditLog = function() { auditModal.classList.add('hidden'); } window.closeAuditLog = function() { auditModal.classList.add('hidden'); }
} }
+11 -3
View File
@@ -2,9 +2,14 @@
// /home/docker/pve_dashboard/src/cron.php // /home/docker/pve_dashboard/src/cron.php
require_once __DIR__ . '/db.php'; require_once __DIR__ . '/db.php';
// FIX 1: Harte Zeitzone für die PHP-CLI (überschreibt Docker UTC)
date_default_timezone_set('Europe/Berlin');
function isCronMatch($cron, $time = null) { function isCronMatch($cron, $time = null) {
if ($time === null) $time = time(); if ($time === null) $time = time();
$cronParts = explode(' ', trim($cron));
// FIX 2: Ignoriert beliebig viele Leerzeichen zwischen den Werten
$cronParts = preg_split('/\s+/', trim($cron));
if (count($cronParts) !== 5) return false; if (count($cronParts) !== 5) return false;
list($min, $hour, $day, $month, $weekday) = $cronParts; list($min, $hour, $day, $month, $weekday) = $cronParts;
@@ -18,7 +23,10 @@ function isCronMatch($cron, $time = null) {
function matchCronPart($part, $current) { function matchCronPart($part, $current) {
if ($part === '*') return true; if ($part === '*') return true;
if ($part === (string)(int)$current) return true;
// FIX 3: Sauberer Integer-Vergleich für führende Nullen ("00" wird zu 0)
if (is_numeric($part) && (int)$part === (int)$current) return true;
if (strpos($part, '*/') === 0) { if (strpos($part, '*/') === 0) {
$step = (int)substr($part, 2); $step = (int)substr($part, 2);
return $step > 0 && ((int)$current % $step) === 0; return $step > 0 && ((int)$current % $step) === 0;
@@ -26,7 +34,7 @@ function matchCronPart($part, $current) {
return false; return false;
} }
// Mini-Proxmox-Fetcher für das Cron-Skript (Mit Token-Auth für PVE!) // Mini-Proxmox-Fetcher für das Cron-Skript
function cronProxmoxData($ip, $tokenId, $tokenSecret, $endpoint, $method = "POST", $postData = null) { function cronProxmoxData($ip, $tokenId, $tokenSecret, $endpoint, $method = "POST", $postData = null) {
$headers = ["Authorization: PVEAPIToken={$tokenId}={$tokenSecret}"]; $headers = ["Authorization: PVEAPIToken={$tokenId}={$tokenSecret}"];
$ch = curl_init("https://{$ip}:8006{$endpoint}"); $ch = curl_init("https://{$ip}:8006{$endpoint}");
+1 -1
View File
@@ -120,7 +120,7 @@ $nodeCount = $stmt->fetchColumn();
<div class="bg-darkcard border border-darkborder rounded-xl p-5 shadow-lg"><div class="flex justify-between items-start mb-4"><div><p class="text-gray-400 text-sm font-medium">Cluster CPU Cores</p><h3 id="stat-cpu-text" class="text-2xl font-bold text-white mt-1">Lade...</h3></div><div class="p-2 bg-blue-500/10 rounded-lg"><svg class="w-6 h-6 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z"></path></svg></div></div><div class="w-full bg-darkbg rounded-full h-2"><div id="stat-cpu-bar" class="bg-blue-500 h-2 rounded-full" style="width: 0%"></div></div></div> <div class="bg-darkcard border border-darkborder rounded-xl p-5 shadow-lg"><div class="flex justify-between items-start mb-4"><div><p class="text-gray-400 text-sm font-medium">Cluster CPU Cores</p><h3 id="stat-cpu-text" class="text-2xl font-bold text-white mt-1">Lade...</h3></div><div class="p-2 bg-blue-500/10 rounded-lg"><svg class="w-6 h-6 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z"></path></svg></div></div><div class="w-full bg-darkbg rounded-full h-2"><div id="stat-cpu-bar" class="bg-blue-500 h-2 rounded-full" style="width: 0%"></div></div></div>
<div class="bg-darkcard border border-darkborder rounded-xl p-5 shadow-lg"><div class="flex justify-between items-start mb-4"><div><p class="text-gray-400 text-sm font-medium">Globaler RAM</p><h3 id="stat-ram-text" class="text-2xl font-bold text-white mt-1">Lade...</h3></div><div class="p-2 bg-proxmox/10 rounded-lg"><svg class="w-6 h-6 text-proxmox" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17v-2m3 2v-4m3 4v-6m2 10H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path></svg></div></div><div class="w-full bg-darkbg rounded-full h-2"><div id="stat-ram-bar" class="bg-proxmox h-2 rounded-full" style="width: 0%"></div></div></div> <div class="bg-darkcard border border-darkborder rounded-xl p-5 shadow-lg"><div class="flex justify-between items-start mb-4"><div><p class="text-gray-400 text-sm font-medium">Globaler RAM</p><h3 id="stat-ram-text" class="text-2xl font-bold text-white mt-1">Lade...</h3></div><div class="p-2 bg-proxmox/10 rounded-lg"><svg class="w-6 h-6 text-proxmox" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17v-2m3 2v-4m3 4v-6m2 10H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path></svg></div></div><div class="w-full bg-darkbg rounded-full h-2"><div id="stat-ram-bar" class="bg-proxmox h-2 rounded-full" style="width: 0%"></div></div></div>
<div class="bg-darkcard border border-darkborder rounded-xl p-5 shadow-lg"><div class="flex justify-between items-start mb-4"><div><p class="text-gray-400 text-sm font-medium">Datacenter Storage</p><h3 id="stat-disk-text" class="text-2xl font-bold text-white mt-1">Lade...</h3></div><div class="p-2 bg-emerald-500/10 rounded-lg"><svg class="w-6 h-6 text-emerald-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4"></path></svg></div></div><div class="w-full bg-darkbg rounded-full h-2"><div id="stat-disk-bar" class="bg-emerald-500 h-2 rounded-full" style="width: 0%"></div></div></div> <div class="bg-darkcard border border-darkborder rounded-xl p-5 shadow-lg"><div class="flex justify-between items-start mb-4"><div><p class="text-gray-400 text-sm font-medium">Datacenter Storage</p><h3 id="stat-disk-text" class="text-2xl font-bold text-white mt-1">Lade...</h3></div><div class="p-2 bg-emerald-500/10 rounded-lg"><svg class="w-6 h-6 text-emerald-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4"></path></svg></div></div><div class="w-full bg-darkbg rounded-full h-2"><div id="stat-disk-bar" class="bg-emerald-500 h-2 rounded-full" style="width: 0%"></div></div></div>
<div class="bg-darkcard border border-darkborder rounded-xl p-5 shadow-lg flex flex-col justify-center"><div class="flex justify-between items-start mb-1"><div><p class="text-gray-400 text-sm font-medium">System Updates (APT)</p><h3 id="stat-updates-text" class="text-2xl font-bold text-white mt-1">Lade...</h3></div><div class="p-2 bg-purple-500/10 rounded-lg"><svg class="w-6 h-6 text-purple-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path></svg></div></div><div id="stat-updates-sub" class="text-xs text-gray-500 mt-1 truncate">Prüfe Updates...</div></div> <div onclick="openUpdateManager()" class="bg-darkcard border border-darkborder rounded-xl p-5 shadow-lg flex flex-col justify-center cursor-pointer hover:border-purple-500 transition-colors"><div class="flex justify-between items-start mb-1"><div><p class="text-gray-400 text-sm font-medium">System Updates (APT)</p><h3 id="stat-updates-text" class="text-2xl font-bold text-white mt-1">Lade...</h3></div><div class="p-2 bg-purple-500/10 rounded-lg"><svg class="w-6 h-6 text-purple-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path></svg></div></div><div id="stat-updates-sub" class="text-xs text-gray-500 mt-1 truncate">Prüfe Updates...</div></div>
</div> </div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6"> <div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
+54 -58
View File
File diff suppressed because one or more lines are too long