diff --git a/src/app.js b/src/app.js
index 7d2b2e7..601a4bd 100644
--- a/src/app.js
+++ b/src/app.js
@@ -18,7 +18,6 @@ let liveInterval = null;
let currentGraphParams = {};
let prevGlobalTime = null;
-// Initialisierung der kleinen Dashboard-Charts (sofern Container vorhanden)
document.addEventListener('DOMContentLoaded', () => {
const ctx = document.getElementById('liveChart')?.getContext('2d');
if(ctx) { liveChart = new Chart(ctx, { type: 'line', data: { labels: [], datasets: [{ label: 'CPU (%)', borderColor: '#3b82f6', backgroundColor: 'rgba(59, 130, 246, 0.1)', borderWidth: 2, tension: 0.4, fill: true, data: [] }, { label: 'RAM (%)', borderColor: '#E57000', backgroundColor: 'rgba(229, 112, 0, 0.1)', borderWidth: 2, tension: 0.4, fill: true, data: [] }] }, options: { responsive: true, maintainAspectRatio: false, animation: { duration: 500 }, scales: { x: { ticks: { color: '#9ca3af' }, grid: { color: '#33334d' } }, y: { min: 0, max: 100, ticks: { color: '#9ca3af', callback: v => v + '%' }, grid: { color: '#33334d' } } }, plugins: { legend: { labels: { color: '#e2e8f0', usePointStyle: true } } } } }); }
@@ -28,304 +27,300 @@ document.addEventListener('DOMContentLoaded', () => {
});
// ---------------------------------------------------------
-// 3. ALLE WINDOW.* FUNKTIONEN (Modals & Actions)
+// 3. LOGIN & SETUP ROUTING (Mit Debugging)
// ---------------------------------------------------------
-// Wir binden ALLES zwingend an "window.", damit HTML-onclick immer darauf zugreifen kann!
-
-window.switchTab = function(tab) {
- ['pve', 'pbs', 'pmg', 'node-view'].forEach(t => {
- const el = document.getElementById('tab-' + t);
- const nav = document.getElementById('nav-tab-' + t);
- if (t === tab) {
- if(el) { el.classList.remove('hidden'); setTimeout(() => el.classList.remove('opacity-0'), 50); }
- if(nav) nav.classList.add('tab-active' + (t==='pve'?'':(t==='pbs'?'-pbs':(t==='pmg'?'-pmg':''))));
- } else {
- if(el) el.classList.add('hidden', 'opacity-0');
- if(nav) nav.classList.remove('tab-active', 'tab-active-pbs', 'tab-active-pmg');
- }
- });
- document.querySelectorAll('[id^="nav-node-"]').forEach(el => el.classList.remove('text-white', 'font-bold'));
- if(tab === 'pbs') window.fetchPbsStats();
- if(tab === 'pmg') window.fetchPmgStats();
-}
-
-window.openNodeView = async function(nodeId, host) {
- window.switchTab('node-view');
- document.querySelectorAll('[id^="nav-node-"]').forEach(el => el.classList.remove('text-white', 'font-bold'));
- const activeNav = document.getElementById(`nav-node-${host}`);
- if(activeNav) activeNav.classList.add('text-white', 'font-bold');
-
- const titleEl = document.getElementById('nodeViewTitle');
- if(titleEl) titleEl.innerText = 'Host: ' + host;
-
- const headerBtn = document.getElementById('nodeViewHeaderBtn');
- const backupBtn = document.getElementById('nodeViewBackupBtn');
-
- if(headerBtn) {
- const newHeaderBtn = headerBtn.cloneNode(true); headerBtn.parentNode.replaceChild(newHeaderBtn, headerBtn);
- newHeaderBtn.addEventListener('click', () => window.openLiveGraph('node', 0, host, nodeId, host));
- }
- if(backupBtn) {
- const newBackupBtn = backupBtn.cloneNode(true); backupBtn.parentNode.replaceChild(newBackupBtn, backupBtn);
- newBackupBtn.addEventListener('click', () => window.openNodeBackups(nodeId, host));
- }
-
- window.currentSelectedNodeId = nodeId; window.currentSelectedHost = host;
-
- const storContainer = document.getElementById('nodeStoragesContainer');
- if(storContainer) storContainer.innerHTML = '
Lade Storages...
';
-
- const vmsBody = document.getElementById('nodeVmsTableBody');
- if(vmsBody) vmsBody.innerHTML = '| Lade VMs... |
';
-
- try {
- const res = await (await fetch(`api.php?action=get_node_dashboard&node_id=${nodeId}&host=${host}`)).json();
- if(res.success) {
- if(storContainer) {
- storContainer.innerHTML = '';
- res.storages.forEach(st => {
- const pct = st.total > 0 ? ((st.used / st.total) * 100).toFixed(1) : 0;
- storContainer.innerHTML += `
-
${st.storage}${pct}%
-
-
${st.content}
`;
- });
- }
-
- if(vmsBody) {
- vmsBody.innerHTML = '';
- if(res.vms.length === 0) vmsBody.innerHTML = '| Keine VMs auf diesem Host. |
';
- res.vms.forEach(vm => {
- const isRunning = vm.status === 'running'; const typeStr = vm.type === 'lxc' ? '📦 LXC' : '🖥️ QEMU';
- const statusBadge = isRunning ? 'Online' : 'Offline';
- let actionButtons = isRunning ? `` : ``;
- actionButtons += ``;
- vmsBody.innerHTML += `| ${vm.vmid} | ${vm.name} | ${typeStr} | ${statusBadge} | ${actionButtons} |
`;
- });
- }
- }
- } catch(e) {}
-}
-
-window.openLiveGraph = function(targetMode, vmid, host, nodeId, name) {
- const m = document.getElementById('liveGraphModal');
- if(!m) return;
- m.classList.remove('hidden');
- currentGraphParams = { mode: targetMode, vmid: vmid, host: host, nodeId: nodeId, name: name };
- window.switchGraphTab('live');
-}
-window.closeLiveGraph = function() { const m = document.getElementById('liveGraphModal'); if(m) m.classList.add('hidden'); if(liveInterval) clearInterval(liveInterval); }
-
-window.switchGraphTab = async function(tab) {
- if(liveInterval) clearInterval(liveInterval);
- ['live', 'day', 'week'].forEach(t => {
- const b = document.getElementById('btn-graph-' + t);
- if(b) {
- if(t === tab) { b.classList.add('border-proxmox', 'text-white'); b.classList.remove('border-transparent', 'text-gray-400'); }
- else { b.classList.remove('border-proxmox', 'text-white'); b.classList.add('border-transparent', 'text-gray-400'); }
- }
- });
-
- const titleEl = document.getElementById('graphModalTitle');
- if(titleEl) titleEl.innerText = `${tab === 'live' ? 'Live' : (tab === 'day' ? '24h' : '7 Tage')} Performance: ${currentGraphParams.name}`;
-
- const dot = document.getElementById('graphStatusDot');
- const subText = document.getElementById('graphSubText');
- if(dot && subText) {
- if(tab === 'live') { dot.classList.add('animate-pulse', 'bg-green-500'); dot.classList.remove('bg-blue-500'); subText.innerText = 'Metriken werden live abgefragt.'; }
- else { dot.classList.remove('animate-pulse', 'bg-green-500'); dot.classList.add('bg-blue-500'); subText.innerText = 'Historische RRD-Daten via Proxmox API.'; }
- }
-
- if(perfChartObj) perfChartObj.destroy(); if(netChartObj) netChartObj.destroy();
-
- const ctxPerfEl = document.getElementById('perfChart');
- if(ctxPerfEl) {
- const ctxPerf = ctxPerfEl.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' } } } } });
- }
-
- const ctxNetEl = document.getElementById('netChart');
- if(ctxNetEl) {
- const ctxNet = ctxNetEl.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' } } } } });
- }
-
- if(tab === 'live') {
- let prevNetIn = 0; let prevNetOut = 0; let prevTime = null;
- const fetchLiveData = async () => {
+if (!window.APP.isLoggedIn) {
+ const loginForm = document.getElementById('loginForm');
+ if(loginForm) {
+ loginForm.addEventListener('submit', async function(e) {
+ e.preventDefault();
+ const btn = this.querySelector('button[type="submit"]');
+ const oTxt = btn.innerText;
+ btn.innerText = 'Prüfe...';
+ const fd = new FormData();
+ fd.append('username', document.getElementById('loginUser').value);
+ fd.append('password', document.getElementById('loginPass').value);
try {
- const 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}`;
- const res = await (await fetch(endpoint)).json();
- if(res.success && res.data) {
- const d = res.data; const now = Date.now();
- let cpuRaw = 0; if (d.cpu !== undefined) cpuRaw = d.cpu; else if (d.cpuinfo && d.cpuinfo.cpus) cpuRaw = 0; const cpu = (cpuRaw * 100).toFixed(1);
- let ram = 0; if (d.maxmem && d.maxmem > 0) { ram = ((d.mem / d.maxmem) * 100).toFixed(1); } else if (d.memory && d.memory.total > 0) { ram = ((d.memory.used / d.memory.total) * 100).toFixed(1); }
-
- let currentNetIn = d.netin || 0; let currentNetOut = d.netout || 0;
- let rxSpeed = 0; let txSpeed = 0;
- if (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' });
-
- if(perfChartObj) { 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(netChartObj && (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(); }
+ // Wir laden den Response erst als reinen Text, um eventuelle PHP-Fehler abzufangen
+ const response = await fetch('api.php?action=login', { method: 'POST', body: fd });
+ const text = await response.text();
+ try {
+ const res = JSON.parse(text);
+ if(res.success) {
+ btn.innerText = 'Erfolgreich! Lade...';
+ // Cache-Busting zwingt den Browser zum Neuladen
+ window.location.href = window.location.pathname + '?t=' + Date.now();
+ } else {
+ alert(res.error);
+ btn.innerText = oTxt;
+ }
+ } catch(parseErr) {
+ alert('API Fehler (Kein JSON). PHP Output: ' + text);
+ btn.innerText = oTxt;
}
- } catch(e) {}
- };
- fetchLiveData(); liveInterval = setInterval(fetchLiveData, 2000);
- } else {
- try {
- 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'});
- if(perfChartObj) { 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); }
- if(netChartObj) { 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)); }
- });
- if(perfChartObj) perfChartObj.update(); if(netChartObj) netChartObj.update();
- }
- } catch(e) {}
- }
-}
-
-window.openNodeBackups = async function(nodeId, host) {
- const m = document.getElementById('nodeBackupsModal');
- if(!m) return;
- m.classList.remove('hidden');
- document.getElementById('nodeBackupsTitle').innerText = 'Backup Jobs: ' + host;
- const tbody = document.getElementById('nodeBackupsTableBody');
- if(!tbody) return;
- tbody.innerHTML = '| Lade Jobs... |
';
- try {
- const res = await (await fetch(`api.php?action=get_node_backup_jobs&node_id=${nodeId}&host=${host}`)).json();
- if(res.success) {
- tbody.innerHTML = '';
- if(res.data.length === 0) tbody.innerHTML = '| Keine Backup Jobs definiert. |
';
- res.data.forEach(j => {
- tbody.innerHTML += `
- | ${j.id} |
- ${j.schedule || 'Manuell'} |
- ${j.storage} |
- ${j.vmid || 'Alle VMs'} |
- Read-Only via API |
-
`;
- });
- }
- } catch(e) {}
-}
-window.closeNodeBackups = function() { const m = document.getElementById('nodeBackupsModal'); if(m) m.classList.add('hidden'); }
-
-window.openCreateVm = async function() {
- const m = document.getElementById('createVmModal'); if(!m) return;
- m.classList.remove('hidden');
- const select = document.getElementById('createVmHost');
- if(select) {
- select.innerHTML = '';
- try {
- const res = await (await fetch('api.php?action=get_pve_nodes')).json();
- if (res.success && res.data.length > 0) {
- select.innerHTML = '';
- res.data.forEach(node => {
- const isSelected = (window.currentSelectedHost && node.host === window.currentSelectedHost) ? 'selected' : '';
- select.innerHTML += ``;
- });
- } else { select.innerHTML = ''; }
- } catch(e) {}
- }
-}
-window.closeCreateVm = function() { const m = document.getElementById('createVmModal'); if(m) m.classList.add('hidden'); }
-
-window.openVmSettings = async function(vmid, host, type, nodeId, name) {
- const m = document.getElementById('vmSettingsModal'); if(!m) return;
- m.classList.remove('hidden');
- const titleEl = document.getElementById('settingsModalTitle'); if(titleEl) titleEl.innerText = 'Einstellungen: ' + name;
-
- if(document.getElementById('setVmid')) document.getElementById('setVmid').value = vmid;
- if(document.getElementById('setHost')) document.getElementById('setHost').value = host;
- if(document.getElementById('setType')) document.getElementById('setType').value = type;
- if(document.getElementById('setNodeId')) document.getElementById('setNodeId').value = nodeId;
-
- if(document.getElementById('snapshotListContainer')) document.getElementById('snapshotListContainer').innerHTML = 'Lade Snapshots...
';
- if(document.getElementById('backupListContainer')) document.getElementById('backupListContainer').innerHTML = 'Lade Backups...
';
-
- if(type === 'qemu' && document.getElementById('setIso')) {
- document.getElementById('setIso').innerHTML = '';
- fetch(`api.php?action=get_isos&node_id=${nodeId}&host=${host}`).then(r=>r.json()).then(isoRes => {
- const s = document.getElementById('setIso');
- if(s) {
- s.innerHTML = '';
- if(isoRes.success) { isoRes.data.forEach(iso => s.innerHTML += ``); }
+ } catch (e) {
+ alert('Netzwerkfehler.');
+ btn.innerText = oTxt;
}
});
}
-
- try {
- const res = await (await fetch(`api.php?action=get_vm_config&vmid=${vmid}&host=${host}&type=${type}&node_id=${nodeId}`)).json();
- if (res.success && res.data) {
- const cfg = res.data;
- if(document.getElementById('setMemory')) document.getElementById('setMemory').value = cfg.memory || '';
- if(document.getElementById('setCores')) document.getElementById('setCores').value = cfg.cores || 1;
- if(document.getElementById('setOnboot')) document.getElementById('setOnboot').checked = (cfg.onboot == 1);
- if(cfg.ide2 && cfg.ide2 !== 'none,media=cdrom' && document.getElementById('setIso')) { const parts = cfg.ide2.split(','); document.getElementById('setIso').value = parts[0]; }
-
- if (cfg.net0) {
- if(document.getElementById('setRawNet0')) document.getElementById('setRawNet0').value = cfg.net0;
- const badge = document.getElementById('netStatusBadge');
- const btn = document.getElementById('btnToggleNet');
- if(badge && btn) {
- if (cfg.net0.includes('link_down=1')) {
- badge.className = 'px-2 py-1 rounded text-xs font-bold text-red-400 bg-red-500/20'; badge.innerText = 'Getrennt';
- btn.innerText = 'Kabel einstecken'; btn.onclick = () => window.saveNetwork(false);
- } else {
- badge.className = 'px-2 py-1 rounded text-xs font-bold text-green-400 bg-green-500/20'; badge.innerText = 'Verbunden';
- btn.innerText = 'Kabel ziehen'; btn.onclick = () => window.saveNetwork(true);
- }
- }
- } else {
- if(document.getElementById('netStatusBadge')) document.getElementById('netStatusBadge').innerText = 'Kein net0';
- if(document.getElementById('btnToggleNet')) 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) {
- if(document.getElementById('setPrimaryDisk')) document.getElementById('setPrimaryDisk').value = diskName;
- if(document.getElementById('diskLabelName')) document.getElementById('diskLabelName').innerText = diskName;
- } else {
- if(document.getElementById('diskLabelName')) document.getElementById('diskLabelName').innerText = 'Nicht gefunden';
- }
- }
-
- // Helfer-Funktionen aufrufen (Definitionen siehe unten)
- if(typeof window.loadSnapshots === 'function') window.loadSnapshots(vmid, host, type, nodeId);
-
- const stRes = await (await fetch(`api.php?action=get_backup_storages&node_id=${nodeId}&host=${host}`)).json();
- if (stRes.success && document.getElementById('backupTargetStorage')) {
- const bSelect = document.getElementById('backupTargetStorage');
- bSelect.innerHTML = '';
- stRes.data.forEach(st => bSelect.innerHTML += ``);
- }
-
- if(typeof window.loadBackups === 'function') window.loadBackups(vmid, host, nodeId);
- } catch (e) {}
-}
-window.closeVmSettings = function() { const m = document.getElementById('vmSettingsModal'); if(m) m.classList.add('hidden'); }
-
-window.sendVmCommand = async function(vmid, host, type, cmd, nodeId) {
- if(!confirm(`Maschine '${vmid}' wirklich ${cmd}?`)) return;
- const fd = new FormData(); fd.append('vmid', vmid); fd.append('host', host); fd.append('type', type); fd.append('cmd', cmd); fd.append('node_id', nodeId);
- try {
- const res = await (await fetch('api.php?action=vm_action', { method: 'POST', body: fd })).json();
- if(res.success) { setTimeout(() => { if(window.currentSelectedHost === host && !document.getElementById('tab-node-view').classList.contains('hidden')) { window.openNodeView(nodeId, host); } else if(typeof window.loadVmsIntoTable === 'function') { window.loadVmsIntoTable(); } }, 2000); }
- else alert(res.error);
- } catch (err) {}
+} else if (window.APP.nodeCount === 0) {
+ const setupForm = document.getElementById('setupForm');
+ if(setupForm) { setupForm.addEventListener('submit', async function(e) { e.preventDefault(); const btn = this.querySelector('button[type="submit"]'); const oTxt = btn.innerText; btn.innerText = 'Verbinde...'; const fd = new FormData(); fd.append('name', document.getElementById('nodeName').value); fd.append('ip', document.getElementById('nodeIp').value); fd.append('user', document.getElementById('nodeUser').value); fd.append('pass', document.getElementById('nodePass').value); fd.append('type', 'pve'); try { const res = await (await fetch('api.php?action=add_node', { method: 'POST', body: fd })).json(); if(res.success) { btn.innerText = 'Erfolgreich!'; setTimeout(() => window.location.reload(), 1000); } else { alert(res.error); btn.innerText = oTxt; } } catch (e) { alert('Netzwerkfehler.'); btn.innerText = oTxt; } }); }
}
// ---------------------------------------------------------
-// 4. MAIN INIT LOGIC (Nur wenn eingeloggt)
+// 4. MAIN APP LOGIC (Nur wenn eingeloggt und Nodes da sind)
// ---------------------------------------------------------
if (window.APP.isLoggedIn && window.APP.nodeCount > 0) {
+
+ // UI Helpers (Audit Log Button Injection)
+ if (window.APP.username === 'admin' || document.querySelector('a[onclick="openUserManager()"]')) {
+ const userBtn = document.querySelector('a[onclick="openUserManager()"]');
+ if (userBtn && !document.getElementById('btnAuditLog')) {
+ const auditBtnHTML = ` Audit Log`;
+ userBtn.insertAdjacentHTML('afterend', auditBtnHTML);
+ }
+ }
- // Global Stats Polling
+ // Password Modal
+ window.openPasswordModal = function() { const m = document.getElementById('passwordModal'); if(m) { m.classList.remove('hidden'); document.getElementById('changePasswordForm').reset(); } }
+ window.closePasswordModal = function() { const m = document.getElementById('passwordModal'); if(m) m.classList.add('hidden'); }
+ const pwdForm = document.getElementById('changePasswordForm');
+ if(pwdForm) {
+ pwdForm.addEventListener('submit', async function(e) {
+ e.preventDefault(); 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; }
+ 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);
+ 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; }
+ });
+ }
+
+ // Sidebar Nodes laden
+ async function loadSidebarNodes() {
+ const container = document.getElementById('sidebar-pve-nodes'); if(!container) return;
+ try {
+ const res = await (await fetch('api.php?action=get_pve_nodes')).json();
+ if(res.success && res.data) {
+ container.innerHTML = '';
+ res.data.forEach(n => {
+ container.innerHTML += `🖥️ ${n.display}`;
+ });
+ }
+ } catch(e) {}
+ }
+ loadSidebarNodes();
+
+ // Tab Switching System
+ window.switchTab = function(tab) {
+ ['pve', 'pbs', 'pmg', 'node-view'].forEach(t => {
+ const el = document.getElementById('tab-' + t);
+ const nav = document.getElementById('nav-tab-' + t);
+ if (t === tab) {
+ if(el) { el.classList.remove('hidden'); setTimeout(() => el.classList.remove('opacity-0'), 50); }
+ if(nav) nav.classList.add('tab-active' + (t==='pve'?'':(t==='pbs'?'-pbs':(t==='pmg'?'-pmg':''))));
+ } else {
+ if(el) el.classList.add('hidden', 'opacity-0');
+ if(nav) nav.classList.remove('tab-active', 'tab-active-pbs', 'tab-active-pmg');
+ }
+ });
+ document.querySelectorAll('[id^="nav-node-"]').forEach(el => el.classList.remove('text-white', 'font-bold'));
+ if(tab === 'pbs') window.fetchPbsStats();
+ if(tab === 'pmg') window.fetchPmgStats();
+ }
+
+ // Node-Specific Dashboard (PVE)
+ window.openNodeView = async function(nodeId, host) {
+ window.switchTab('node-view');
+ document.querySelectorAll('[id^="nav-node-"]').forEach(el => el.classList.remove('text-white', 'font-bold'));
+ const activeNav = document.getElementById(`nav-node-${host}`);
+ if(activeNav) activeNav.classList.add('text-white', 'font-bold');
+
+ const titleEl = document.getElementById('nodeViewTitle');
+ if(titleEl) titleEl.innerText = 'Host: ' + host;
+
+ const headerBtn = document.getElementById('nodeViewHeaderBtn');
+ const backupBtn = document.getElementById('nodeViewBackupBtn');
+
+ if(headerBtn) {
+ const newHeaderBtn = headerBtn.cloneNode(true); headerBtn.parentNode.replaceChild(newHeaderBtn, headerBtn);
+ newHeaderBtn.addEventListener('click', () => window.openLiveGraph('node', 0, host, nodeId, host));
+ }
+ if(backupBtn) {
+ const newBackupBtn = backupBtn.cloneNode(true); backupBtn.parentNode.replaceChild(newBackupBtn, backupBtn);
+ newBackupBtn.addEventListener('click', () => window.openNodeBackups(nodeId, host));
+ }
+
+ window.currentSelectedNodeId = nodeId; window.currentSelectedHost = host;
+
+ const storContainer = document.getElementById('nodeStoragesContainer');
+ if(storContainer) storContainer.innerHTML = 'Lade Storages...
';
+
+ const vmsBody = document.getElementById('nodeVmsTableBody');
+ if(vmsBody) vmsBody.innerHTML = '| Lade VMs... |
';
+
+ try {
+ const res = await (await fetch(`api.php?action=get_node_dashboard&node_id=${nodeId}&host=${host}`)).json();
+ if(res.success) {
+ if(storContainer) {
+ storContainer.innerHTML = '';
+ res.storages.forEach(st => {
+ const pct = st.total > 0 ? ((st.used / st.total) * 100).toFixed(1) : 0;
+ storContainer.innerHTML += `
+
${st.storage}${pct}%
+
+
${st.content}
`;
+ });
+ }
+
+ if(vmsBody) {
+ vmsBody.innerHTML = '';
+ if(res.vms.length === 0) vmsBody.innerHTML = '| Keine VMs auf diesem Host. |
';
+ res.vms.forEach(vm => {
+ const isRunning = vm.status === 'running'; const typeStr = vm.type === 'lxc' ? '📦 LXC' : '🖥️ QEMU';
+ const statusBadge = isRunning ? 'Online' : 'Offline';
+ let actionButtons = isRunning ? `` : ``;
+ actionButtons += ``;
+ vmsBody.innerHTML += `| ${vm.vmid} | ${vm.name} | ${typeStr} | ${statusBadge} | ${actionButtons} |
`;
+ });
+ }
+ }
+ } catch(e) {}
+ }
+
+ // Graph & Performance Modal
+ window.openLiveGraph = function(targetMode, vmid, host, nodeId, name) {
+ const m = document.getElementById('liveGraphModal');
+ if(!m) return;
+ m.classList.remove('hidden');
+ currentGraphParams = { mode: targetMode, vmid: vmid, host: host, nodeId: nodeId, name: name };
+ window.switchGraphTab('live');
+ }
+ window.closeLiveGraph = function() { const m = document.getElementById('liveGraphModal'); if(m) m.classList.add('hidden'); if(liveInterval) clearInterval(liveInterval); }
+
+ window.switchGraphTab = async function(tab) {
+ if(liveInterval) clearInterval(liveInterval);
+ ['live', 'day', 'week'].forEach(t => {
+ const b = document.getElementById('btn-graph-' + t);
+ if(b) {
+ if(t === tab) { b.classList.add('border-proxmox', 'text-white'); b.classList.remove('border-transparent', 'text-gray-400'); }
+ else { b.classList.remove('border-proxmox', 'text-white'); b.classList.add('border-transparent', 'text-gray-400'); }
+ }
+ });
+
+ const titleEl = document.getElementById('graphModalTitle');
+ if(titleEl) titleEl.innerText = `${tab === 'live' ? 'Live' : (tab === 'day' ? '24h' : '7 Tage')} Performance: ${currentGraphParams.name}`;
+
+ const dot = document.getElementById('graphStatusDot');
+ const subText = document.getElementById('graphSubText');
+ if(dot && subText) {
+ if(tab === 'live') { dot.classList.add('animate-pulse', 'bg-green-500'); dot.classList.remove('bg-blue-500'); subText.innerText = 'Metriken werden live abgefragt.'; }
+ else { dot.classList.remove('animate-pulse', 'bg-green-500'); dot.classList.add('bg-blue-500'); subText.innerText = 'Historische RRD-Daten via Proxmox API.'; }
+ }
+
+ if(perfChartObj) perfChartObj.destroy(); if(netChartObj) netChartObj.destroy();
+
+ const ctxPerfEl = document.getElementById('perfChart');
+ if(ctxPerfEl) {
+ const ctxPerf = ctxPerfEl.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' } } } } });
+ }
+
+ const ctxNetEl = document.getElementById('netChart');
+ if(ctxNetEl) {
+ const ctxNet = ctxNetEl.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' } } } } });
+ }
+
+ if(tab === 'live') {
+ let prevNetIn = 0; let prevNetOut = 0; let prevTime = null;
+ const fetchLiveData = async () => {
+ try {
+ 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}`;
+ const res = await (await fetch(endpoint)).json();
+ if(res.success && res.data) {
+ const d = res.data; const now = Date.now();
+ let cpuRaw = 0; if (d.cpu !== undefined) cpuRaw = d.cpu; else if (d.cpuinfo && d.cpuinfo.cpus) cpuRaw = 0; const cpu = (cpuRaw * 100).toFixed(1);
+ let ram = 0; if (d.maxmem && d.maxmem > 0) { ram = ((d.mem / d.maxmem) * 100).toFixed(1); } else if (d.memory && d.memory.total > 0) { ram = ((d.memory.used / d.memory.total) * 100).toFixed(1); }
+
+ let currentNetIn = d.netin || 0; let currentNetOut = d.netout || 0;
+ let rxSpeed = 0; let txSpeed = 0;
+ if (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' });
+
+ if(perfChartObj) { 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(netChartObj && (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) {}
+ };
+ fetchLiveData(); liveInterval = setInterval(fetchLiveData, 2000);
+ } else {
+ try {
+ 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'});
+ if(perfChartObj) { 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); }
+ if(netChartObj) { 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)); }
+ });
+ if(perfChartObj) perfChartObj.update(); if(netChartObj) netChartObj.update();
+ }
+ } catch(e) {}
+ }
+ }
+
+ // Node Backups Modal
+ window.openNodeBackups = async function(nodeId, host) {
+ const m = document.getElementById('nodeBackupsModal');
+ if(!m) return;
+ m.classList.remove('hidden');
+ document.getElementById('nodeBackupsTitle').innerText = 'Backup Jobs: ' + host;
+ const tbody = document.getElementById('nodeBackupsTableBody');
+ if(!tbody) return;
+ tbody.innerHTML = '| Lade Jobs... |
';
+ try {
+ const res = await (await fetch(`api.php?action=get_node_backup_jobs&node_id=${nodeId}&host=${host}`)).json();
+ if(res.success) {
+ tbody.innerHTML = '';
+ if(res.data.length === 0) tbody.innerHTML = '| Keine Backup Jobs definiert. |
';
+ res.data.forEach(j => {
+ tbody.innerHTML += `
+ | ${j.id} |
+ ${j.schedule || 'Manuell'} |
+ ${j.storage} |
+ ${j.vmid || 'Alle VMs'} |
+ Read-Only via API |
+
`;
+ });
+ }
+ } catch(e) {}
+ }
+ window.closeNodeBackups = function() { const m = document.getElementById('nodeBackupsModal'); if(m) m.classList.add('hidden'); }
+
+ // VM Erstellung
+ window.openCreateVm = async function() {
+ const m = document.getElementById('createVmModal'); if(!m) return;
+ m.classList.remove('hidden');
+ const select = document.getElementById('createVmHost');
+ if(select) {
+ select.innerHTML = '';
+ try {
+ const res = await (await fetch('api.php?action=get_pve_nodes')).json();
+ if (res.success && res.data.length > 0) {
+ select.innerHTML = '';
+ res.data.forEach(node => {
+ const isSelected = (window.currentSelectedHost && node.host === window.currentSelectedHost) ? 'selected' : '';
+ select.innerHTML += ``;
+ });
+ } else { select.innerHTML = ''; }
+ } catch(e) {}
+ }
+ }
+ window.closeCreateVm = function() { const m = document.getElementById('createVmModal'); if(m) m.classList.add('hidden'); }
+
+ // Global Stats Polling (Cluster Dashboard)
window.fetchGlobalStats = async function() {
if(document.getElementById('tab-pve') && document.getElementById('tab-pve').classList.contains('hidden')) return;
try {
@@ -374,65 +369,142 @@ if (window.APP.isLoggedIn && window.APP.nodeCount > 0) {
} catch (err) {}
}
- // Zusätzliche API Polling Funktionen
window.fetchTopVms = async function() { if(!document.getElementById('top-vms-container') || (document.getElementById('tab-pve') && document.getElementById('tab-pve').classList.contains('hidden'))) return; try { const res = await (await fetch('api.php?action=get_top_vms')).json(); if(res.success && res.data) { const container = document.getElementById('top-vms-container'); container.innerHTML = ''; if(res.data.length === 0) { container.innerHTML = 'Keine aktiven VMs.
'; return; } res.data.forEach((vm, i) => { const cpuPercent = ((vm.cpu || 0) * 100).toFixed(1); const ramUsed = formatBytes(vm.mem || 0); const icon = vm.type === 'lxc' ? '📦' : '🖥️'; const numberColor = i === 0 ? 'text-red-500' : (i === 1 ? 'text-orange-400' : (i === 2 ? 'text-yellow-400' : 'text-gray-400')); container.innerHTML += ``; }); } } catch (err) {} }
window.fetchRecentJobs = async function() { if(!document.getElementById('recent-jobs-container')) return; try { const res = await (await fetch('api.php?action=get_recent_jobs')).json(); if(res.success && res.data) { const container = document.getElementById('recent-jobs-container'); container.innerHTML = ''; if(res.data.length === 0) { container.innerHTML = 'Keine aktuellen Jobs.
'; return; } res.data.forEach(job => { const jobTypeStr = job.type || job.worker_type || 'unknown'; let statusColor = 'text-gray-400', statusIcon = '⏳', statusText = job.status || 'running...'; if(statusText.toLowerCase() === 'ok') { statusColor = 'text-green-500'; statusIcon = '✅'; } else if(statusText !== 'running...') { statusColor = 'text-red-500'; statusIcon = '❌'; } else { statusColor = 'text-blue-400'; statusIcon = '🔄'; } const date = new Date(job.starttime * 1000); const timeStr = date.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' }), dateStr = date.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' }); const isBackup = jobTypeStr.includes('sync') || jobTypeStr.includes('prune') || jobTypeStr.includes('garbage_collection') || jobTypeStr.includes('vzdump') || jobTypeStr.includes('verify'); const jobTypeColor = isBackup ? 'text-purple-400' : 'text-white'; container.innerHTML += `${statusIcon}
${jobTypeStr}
Host: ${job.node_name}
${statusText}
${dateStr} - ${timeStr}
`; }); } } catch (err) {} }
- // Start Intervals
- setInterval(window.fetchGlobalStats, 10000);
- setInterval(window.fetchTopVms, 10000);
- setInterval(window.fetchRecentJobs, 15000);
-
- // Fallback Initial Calls
- window.fetchGlobalStats();
- window.fetchTopVms();
- window.fetchRecentJobs();
-
- // Event Listener für Formulare (Create VM)
- const createVmForm = document.getElementById('createVmForm');
- if(createVmForm) {
- createVmForm.addEventListener('submit', async function(e) {
- e.preventDefault();
- const hostDataStr = document.getElementById('createVmHost').value;
- if(!hostDataStr) return;
- const hostData = JSON.parse(hostDataStr);
- const btn = this.querySelector('button[type="submit"]'); const oTxt = btn.innerText; btn.innerText = 'Richte VM ein...';
- const fd = new FormData(); fd.append('node_id', hostData.id); fd.append('host', hostData.host); fd.append('name', document.getElementById('createVmName').value); fd.append('memory', document.getElementById('createVmRam').value); fd.append('cores', document.getElementById('createVmCores').value);
- try {
- const res = await (await fetch('api.php?action=create_vm', { method: 'POST', body: fd })).json();
- if(res.success) {
- alert(`Erfolgreich! VM-ID: ${res.vmid}`); createVmForm.reset(); window.closeCreateVm();
- if(window.currentSelectedHost === hostData.host && !document.getElementById('tab-node-view').classList.contains('hidden')) {
- window.openNodeView(hostData.id, hostData.host);
- }
- } else alert(res.error);
- } catch(e) {} finally { btn.innerText = oTxt; }
- });
+ window.sendVmCommand = async function(vmid, host, type, cmd, nodeId) {
+ if(!confirm(`Maschine '${vmid}' wirklich ${cmd}?`)) return;
+ const fd = new FormData(); fd.append('vmid', vmid); fd.append('host', host); fd.append('type', type); fd.append('cmd', cmd); fd.append('node_id', nodeId);
+ try {
+ const res = await (await fetch('api.php?action=vm_action', { method: 'POST', body: fd })).json();
+ if(res.success) { setTimeout(() => { if(window.currentSelectedHost === host && !document.getElementById('tab-node-view').classList.contains('hidden')) { window.openNodeView(nodeId, host); } else if(typeof window.loadVmsIntoTable === 'function') { window.loadVmsIntoTable(); } }, 2000); }
+ else alert(res.error);
+ } catch (err) {}
}
- // --- ALLE WEITEREN PBS / PMG / ADMIN MODAL FUNKTIONEN ---
- // (Diese Funktionen werden erst aufgerufen, wenn die entsprechenden Menüs geklickt werden,
- // daher definieren wir sie einfach am Ende, falls sie existieren.)
-
- window.openUserManager = function() { const m = document.getElementById('userManagerModal'); if(m) { m.classList.remove('hidden'); if(typeof loadUsersIntoTable === 'function') loadUsersIntoTable(); } }
- window.closeUserManager = function() { const m = document.getElementById('userManagerModal'); if(m) m.classList.add('hidden'); }
-
- window.openNodeTopology = function() { const m = document.getElementById('nodeTopologyModal'); if(m) m.classList.remove('hidden'); /* ... Lade Logik ... */ }
- window.closeNodeTopology = function() { const m = document.getElementById('nodeTopologyModal'); if(m) m.classList.add('hidden'); }
+ // VM Settings Modal
+ window.openVmSettings = async function(vmid, host, type, nodeId, name) {
+ const m = document.getElementById('vmSettingsModal'); if(!m) return;
+ m.classList.remove('hidden');
+ const titleEl = document.getElementById('settingsModalTitle'); if(titleEl) titleEl.innerText = 'Einstellungen: ' + name;
+
+ if(document.getElementById('setVmid')) document.getElementById('setVmid').value = vmid;
+ if(document.getElementById('setHost')) document.getElementById('setHost').value = host;
+ if(document.getElementById('setType')) document.getElementById('setType').value = type;
+ if(document.getElementById('setNodeId')) document.getElementById('setNodeId').value = nodeId;
+
+ if(document.getElementById('snapshotListContainer')) document.getElementById('snapshotListContainer').innerHTML = 'Lade Snapshots...
';
+ if(document.getElementById('backupListContainer')) document.getElementById('backupListContainer').innerHTML = 'Lade Backups...
';
+
+ if(type === 'qemu' && document.getElementById('setIso')) {
+ document.getElementById('setIso').innerHTML = '';
+ fetch(`api.php?action=get_isos&node_id=${nodeId}&host=${host}`).then(r=>r.json()).then(isoRes => {
+ const s = document.getElementById('setIso');
+ if(s) {
+ s.innerHTML = '';
+ if(isoRes.success) { isoRes.data.forEach(iso => s.innerHTML += ``); }
+ }
+ });
+ }
- window.openAuditLog = function() { const m = document.getElementById('auditLogModal'); if(m) m.classList.remove('hidden'); }
- window.closeAuditLog = function() { const m = document.getElementById('auditLogModal'); if(m) m.classList.add('hidden'); }
+ try {
+ const res = await (await fetch(`api.php?action=get_vm_config&vmid=${vmid}&host=${host}&type=${type}&node_id=${nodeId}`)).json();
+ if (res.success && res.data) {
+ const cfg = res.data;
+ if(document.getElementById('setMemory')) document.getElementById('setMemory').value = cfg.memory || '';
+ if(document.getElementById('setCores')) document.getElementById('setCores').value = cfg.cores || 1;
+ if(document.getElementById('setOnboot')) document.getElementById('setOnboot').checked = (cfg.onboot == 1);
+ if(cfg.ide2 && cfg.ide2 !== 'none,media=cdrom' && document.getElementById('setIso')) { const parts = cfg.ide2.split(','); document.getElementById('setIso').value = parts[0]; }
+
+ if (cfg.net0) {
+ if(document.getElementById('setRawNet0')) document.getElementById('setRawNet0').value = cfg.net0;
+ const badge = document.getElementById('netStatusBadge');
+ const btn = document.getElementById('btnToggleNet');
+ if(badge && btn) {
+ if (cfg.net0.includes('link_down=1')) {
+ badge.className = 'px-2 py-1 rounded text-xs font-bold text-red-400 bg-red-500/20'; badge.innerText = 'Getrennt';
+ btn.innerText = 'Kabel einstecken'; btn.onclick = () => window.saveNetwork(false);
+ } else {
+ badge.className = 'px-2 py-1 rounded text-xs font-bold text-green-400 bg-green-500/20'; badge.innerText = 'Verbunden';
+ btn.innerText = 'Kabel ziehen'; btn.onclick = () => window.saveNetwork(true);
+ }
+ }
+ } else {
+ if(document.getElementById('netStatusBadge')) document.getElementById('netStatusBadge').innerText = 'Kein net0';
+ if(document.getElementById('btnToggleNet')) 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) {
+ if(document.getElementById('setPrimaryDisk')) document.getElementById('setPrimaryDisk').value = diskName;
+ if(document.getElementById('diskLabelName')) document.getElementById('diskLabelName').innerText = diskName;
+ } else {
+ if(document.getElementById('diskLabelName')) document.getElementById('diskLabelName').innerText = 'Nicht gefunden';
+ }
+ }
+
+ if(typeof window.loadSnapshots === 'function') window.loadSnapshots(vmid, host, type, nodeId);
+
+ const stRes = await (await fetch(`api.php?action=get_backup_storages&node_id=${nodeId}&host=${host}`)).json();
+ if (stRes.success && document.getElementById('backupTargetStorage')) {
+ const bSelect = document.getElementById('backupTargetStorage');
+ bSelect.innerHTML = '';
+ stRes.data.forEach(st => bSelect.innerHTML += ``);
+ }
+
+ if(typeof window.loadBackups === 'function') window.loadBackups(vmid, host, nodeId);
+ } catch (e) {}
+ }
+ window.closeVmSettings = function() { const m = document.getElementById('vmSettingsModal'); if(m) m.classList.add('hidden'); }
+ window.saveHardwareSettings = async function() {
+ const fd = new FormData();
+ fd.append('vmid', document.getElementById('setVmid').value); fd.append('host', document.getElementById('setHost').value); fd.append('type', document.getElementById('setType').value); fd.append('node_id', document.getElementById('setNodeId').value);
+ fd.append('memory', document.getElementById('setMemory').value); fd.append('cores', document.getElementById('setCores').value);
+ fd.append('onboot', document.getElementById('setOnboot').checked ? 1 : 0);
+ if(document.getElementById('setType').value === 'qemu') { fd.append('ide2', document.getElementById('setIso').value); }
+
+ const res = await (await fetch('api.php?action=update_vm_config', { method: 'POST', body: fd })).json();
+ if(res.success) { alert('Gespeichert!'); if(window.currentSelectedHost) window.openNodeView(fd.get('node_id'), fd.get('host')); } else alert(res.error);
+ }
- window.openCronManager = function() { const m = document.getElementById('cronManagerModal'); if(m) m.classList.remove('hidden'); }
- window.closeCronManager = function() { const m = document.getElementById('cronManagerModal'); if(m) m.classList.add('hidden'); }
-
- window.openPmgManager = function(nodeId, hostName, internalName) { const m = document.getElementById('pmgManagerModal'); if(m) m.classList.remove('hidden'); }
- window.closePmgManager = function() { const m = document.getElementById('pmgManagerModal'); if(m) m.classList.add('hidden'); }
-
- window.openPbsDatastore = function(nodeId, storeName) { const m = document.getElementById('pbsDatastoreModal'); if(m) m.classList.remove('hidden'); }
+ // --- WEITERE PBS FUNKTIONEN ---
+ window.fetchPbsStats = async function() { if(document.getElementById('tab-pbs') && 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(!container) return; if(res.data.length === 0) { container.innerHTML = 'Keine PBS Server gefunden.
'; return; } container.innerHTML = ''; res.data.forEach(ds => { const total = ds.total || 0; const used = ds.used || 0; const percent = total > 0 ? ((used / total) * 100).toFixed(1) : 0; const colorClass = percent > 85 ? 'bg-red-500' : (percent > 70 ? 'bg-orange-500' : 'bg-pbs'); container.innerHTML += `${ds.store}
PBS Host: ${ds.host}
Auslastung${percent}%
Used: ${formatBytes(used)}Total: ${formatBytes(total)}
`; }); } } catch(e) {} }
+ window.openPbsDatastore = async function(nodeId, storeName) { if (!nodeId) return alert('Node ID fehlt.'); const m = document.getElementById('pbsDatastoreModal'); if(!m) return; m.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() { const m = document.getElementById('pbsDatastoreModal'); if(m) m.classList.add('hidden'); }
+ async function loadPbsBackups(nodeId, storeName) { const container = document.getElementById('pbsBackupsContainer'); if(!container) return; container.innerHTML = 'Durchsuche Namespaces nach Backups...
'; try { const res = await (await fetch(`api.php?action=pbs_get_datastore_content&node_id=${nodeId}&store=${storeName}`)).json(); if(res.success && res.data) { if(res.data.length === 0) { container.innerHTML = 'Keine Backups vorhanden.
'; return; } container.innerHTML = ''; res.data.sort((a,b) => b['backup-time'] - a['backup-time']).forEach(b => { const timeStr = formatDate(b['backup-time']); const sizeStr = formatBytes(b.size); const nsLabel = b.ns ? `[${b.ns}]` : `[Root]`; container.innerHTML += `${b['backup-type']} / ${b['backup-id']} ${nsLabel}
${timeStr} | ${sizeStr}
`; }); } } catch(e) { container.innerHTML = 'Fehler beim Laden.'; } }
+ window.deletePbsSnapshot = async function(btype, bid, btime, ns) { if(!confirm(`Backup ${btype}/${bid} wirklich 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'); if(!mixCont) return; mixCont.innerHTML = 'Lade System-Jobs...
'; const table = document.getElementById('pbsJobsTable'); table.innerHTML = '| Lade Historie... |
'; try { const res = await (await fetch(`api.php?action=pbs_get_jobs&node_id=${nodeId}`)).json(); if(res.success && res.data) { mixCont.innerHTML = ''; table.innerHTML = ''; let mixCount = 0; res.data.sort((a,b) => b.starttime - a.starttime).forEach(job => { const statusColor = job.status === 'OK' ? 'text-green-500' : (job.status ? 'text-red-500' : 'text-blue-400'); const timeStr = formatDate(job.starttime); const runtime = job.endtime ? (Math.round(job.endtime - job.starttime) + 's') : 'Running...'; const isSystemJob = job.worker_type === 'verify' || job.worker_type === 'garbage_collection' || job.worker_type === 'prune'; if (isSystemJob && mixCount < 6) { mixCont.innerHTML += `${job.worker_type}
${timeStr}
${job.status || 'Active'}
`; mixCount++; } table.innerHTML += `| ${timeStr} | ${job.worker_type} | ${job.status || 'Active'} | ${runtime} | |
`; }); if(mixCount === 0) mixCont.innerHTML = 'Keine System-Jobs gefunden.
'; if(res.data.length === 0) table.innerHTML = '| Keine Historie gefunden. |
'; } } catch(e) {} }
+ async function loadPbsSyncJobs(nodeId) { const container = document.getElementById('pbsSyncContainer'); if(!container) return; container.innerHTML = 'Lade Sync-Jobs...
'; try { const res = await (await fetch(`api.php?action=pbs_get_sync_jobs&node_id=${nodeId}`)).json(); if(res.success && res.data) { if(res.data.length === 0) { container.innerHTML = 'Keine Sync-Jobs eingerichtet.
'; return; } container.innerHTML = ''; res.data.forEach(sync => { container.innerHTML += `${sync.id}
Quelle: ${sync.remote || 'Lokal'} -> ${sync['remote-store']}
Ziel: ${sync.store}
Zeitplan: ${sync.schedule || 'Manuell'}
`; }); } } catch(e) { container.innerHTML = 'Fehler.'; } }
+
+ // --- PMG FUNKTIONEN ---
+ window.fetchPmgStats = async function() { if(document.getElementById('tab-pmg') && 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(!container) return; if(res.data.length === 0) { container.innerHTML = 'Keine Mail Gateways angebunden.
'; return; } container.innerHTML = ''; res.data.forEach(pmg => { const cpuPercent = ((pmg.cpu || 0) * 100).toFixed(1); const ramPercent = pmg.memory && pmg.memory.total ? ((pmg.memory.used / pmg.memory.total) * 100).toFixed(1) : 0; const diskPercent = pmg.rootfs && pmg.rootfs.total ? ((pmg.rootfs.used / pmg.rootfs.total) * 100).toFixed(1) : 0; container.innerHTML += `CPU${cpuPercent}%
RAM${ramPercent}%
System Disk${diskPercent}%
`; }); } } catch(e) {} }
+ window.openPmgManager = function(nodeId, hostName, internalName) { const m = document.getElementById('pmgManagerModal'); if(!m) return; m.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; window.switchPmgTab('spam'); }
+ window.closePmgManager = function() { const m = document.getElementById('pmgManagerModal'); if(m) m.classList.add('hidden'); }
+ window.switchPmgTab = function(tabName) { ['spam', 'tracking', 'queues', 'settings'].forEach(t => { const btn = document.getElementById('btn-pmg-' + t); const content = document.getElementById('pmg-tab-' + t); if(btn && content) { if (t === tabName) { btn.classList.add('border-pmg', 'text-white'); btn.classList.remove('border-transparent', 'text-gray-400'); content.classList.remove('hidden'); } else { btn.classList.remove('border-pmg', 'text-white'); btn.classList.add('border-transparent', 'text-gray-400'); content.classList.add('hidden'); } } }); const nodeId = document.getElementById('pmgNodeId').value; if (!nodeId) return; if(tabName === 'spam') window.loadPmgSpamQueue(nodeId); if(tabName === 'tracking') window.loadPmgTracking(); if(tabName === 'queues') window.loadPmgQueues(); if(tabName === 'settings') { window.loadPmgConfig(nodeId); window.loadPmgSsl(nodeId); window.loadPmgDomains(nodeId); window.loadPmgNetworks(nodeId); } }
+ window.loadPmgSpamQueue = async function(nodeId) { const table = document.getElementById('pmgSpamTable'); if(!table) return; table.innerHTML = '| Lade Spam Quarantäne... |
'; try { const res = await (await fetch(`api.php?action=pmg_get_spam_queue&node_id=${nodeId}`)).json(); if(res.success && res.data) { table.innerHTML = ''; if(res.data.length === 0) { table.innerHTML = '| 🎉 Keine Mails in der Quarantäne. |
'; return; } res.data.sort((a,b) => b.time - a.time).forEach(mail => { const timeStr = formatDate(mail.time); const subject = mail.subject || 'Kein Betreff'; const sender = mail.sender || 'Unbekannt'; const receiver = mail.receiver || 'Unbekannt'; const score = mail.spamlevel || 0; table.innerHTML += `| ${timeStr} | ${sender} | ${receiver} | ${subject} | ${score} | |
`; }); } else { table.innerHTML = '| Fehler beim Laden. |
'; } } catch(e) { table.innerHTML = '| Netzwerkfehler. |
'; } }
+ window.pmgSpamAction = async function(actionType, mailId) { if(actionType === 'delete' && !confirm('Mail unwiderruflich löschen?')) return; if(actionType === 'deliver' && !confirm('Soll diese Mail 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) window.loadPmgSpamQueue(document.getElementById('pmgNodeId').value); else alert('Aktion fehlgeschlagen.'); } catch(e) { alert('Netzwerkfehler'); } }
+ window.loadPmgTracking = async function() { const nodeId = document.getElementById('pmgNodeId').value; const internalName = document.getElementById('pmgInternalName').value; const filter = document.getElementById('pmgTrackingFilter').value.trim(); const table = document.getElementById('pmgTrackingTable'); if(!table) return; table.innerHTML = '| Lese Syslog (Letzte 24h)... |
'; try { const res = await (await fetch(`api.php?action=pmg_get_tracking&node_id=${nodeId}&internal_name=${internalName}&filter=${encodeURIComponent(filter)}`)).json(); if(res.success && res.data) { table.innerHTML = ''; if(res.data.length === 0) { table.innerHTML = `| Keine Logs für diesen Filter gefunden. |
`; return; } res.data.forEach(log => { const timestamp = log.time || log.timestamp || 0; const timeStr = timestamp ? formatDate(timestamp) : 'Unbekannt'; const sender = log.from || log.sender || '-'; const receiver = log.to || log.receiver || '-'; let statusTxt = 'Unbekannt'; let statusColor = 'text-gray-400'; if (log.dstatus) { if (log.dstatus === 'A') { statusTxt = 'Delivered / Accept'; statusColor = 'text-green-500'; } else if (log.dstatus === 'N') { statusTxt = 'Rejected / Blocked'; statusColor = 'text-red-500'; } else if (log.dstatus === 'Q') { statusTxt = 'Quarantined'; statusColor = 'text-orange-500'; } else if (log.dstatus === 'B') { statusTxt = 'Bounced'; statusColor = 'text-red-500'; } else { statusTxt = log.dstatus; statusColor = 'text-blue-400'; } } else if (log.relay) { statusTxt = 'Relayed (' + log.relay.split('[')[0] + ')'; statusColor = 'text-blue-400'; } else if (log.msgid) { statusTxt = 'In Processing (' + log.msgid + ')'; statusColor = 'text-gray-400'; } else if (log.client) { statusTxt = 'Connection from ' + log.client.split('[')[0]; statusColor = 'text-gray-500'; } else if (log.id && log.id.length > 5) { statusTxt = 'Queue ID: ' + log.id; statusColor = 'text-gray-400'; } else { statusTxt = JSON.stringify(log).substring(0, 30); } table.innerHTML += `| ${timeStr} | ${sender} | ${receiver} | ${statusTxt} |
`; }); } else { table.innerHTML = '| Syslog konnte nicht gelesen werden. |
'; } } catch(e) { table.innerHTML = '| Netzwerkfehler. |
'; } }
+ window.loadPmgQueues = async function() { const nodeId = document.getElementById('pmgNodeId').value; const internalName = document.getElementById('pmgInternalName').value; const table = document.getElementById('pmgQueueTable'); const summary = document.getElementById('pmgQueuesSummary'); if(!table || !summary) return; table.innerHTML = '| Lese Warteschlangen aus Postfix... |
'; summary.innerHTML = 'Lade...
'; try { const res = await (await fetch(`api.php?action=pmg_get_queues&node_id=${nodeId}&internal_name=${internalName}`)).json(); if(res.success && res.data) { let activeCount = 0, deferCount = 0, holdCount = 0; res.data.forEach(q => { const queueName = q.queue_name || q.queue || ''; if(queueName === 'active') activeCount++; else if(queueName === 'deferred') deferCount++; else holdCount++; }); summary.innerHTML = `Active Queue
${activeCount}
Deferred (Greylisted/Wartend)
${deferCount}
`; table.innerHTML = ''; if(res.data.length === 0) { table.innerHTML = `| Alle Postfix-Queues sind komplett leer. |
`; return; } res.data.forEach(q => { const queueName = q.queue_name || q.queue || 'unknown'; const timestamp = q.arrival_time || q.time || 0; const timeStr = timestamp ? formatDate(timestamp) : 'Unbekannt'; const sender = q.sender || '-'; let receiver = q.receiver || q.recipients || '-'; if (Array.isArray(receiver)) receiver = receiver[0]; const reason = q.reason || q.error || '-'; const qColor = queueName === 'deferred' ? 'text-orange-400' : (queueName === 'active' ? 'text-blue-400' : 'text-gray-400'); table.innerHTML += `| ${queueName} | ${timeStr} | ${sender} | ${receiver} | ${reason} |
`; }); } else { table.innerHTML = '| Queues konnten nicht gelesen werden. |
'; } } catch(e) { table.innerHTML = '| Netzwerkfehler. |
'; } }
+ window.loadPmgConfig = async function(nodeId) { try { const res = await (await fetch(`api.php?action=pmg_get_config&node_id=${nodeId}`)).json(); if(res.success && res.data) { document.getElementById('pmgCfgRelay').value = res.data.relay || ''; document.getElementById('pmgCfgExtPort').value = res.data.ext_port || 25; document.getElementById('pmgCfgIntPort').value = res.data.int_port || 26; document.getElementById('pmgCfgTls').value = res.data.tls || 0; document.getElementById('pmgCfgTlsLog').value = res.data.tlslog || 0; } } catch(e) {} }
+ window.savePmgConfig = async function() { const btn = event.target; const oTxt = btn.innerText; btn.innerText = 'Speichere...'; const fd = new FormData(); fd.append('node_id', document.getElementById('pmgNodeId').value); fd.append('relay', document.getElementById('pmgCfgRelay').value); fd.append('ext_port', document.getElementById('pmgCfgExtPort').value); fd.append('int_port', document.getElementById('pmgCfgIntPort').value); fd.append('tls', document.getElementById('pmgCfgTls').value); fd.append('tlslog', document.getElementById('pmgCfgTlsLog').value); try { const res = await (await fetch('api.php?action=pmg_set_config', {method: 'POST', body: fd})).json(); if(res.success) { alert('Proxy Einstellungen gespeichert!'); } else { alert('Fehler beim Speichern.'); } } catch(e) { alert('Netzwerkfehler.'); } finally { btn.innerText = oTxt; } }
+ window.loadPmgDomains = async function(nodeId) { const container = document.getElementById('pmgDomainsContainer'); if(!container) return; container.innerHTML = 'Lade...
'; try { const res = await (await fetch(`api.php?action=pmg_get_domains&node_id=${nodeId}`)).json(); if(res.success && res.data) { if(res.data.length === 0) { container.innerHTML = 'Keine Domains.
'; return; } container.innerHTML = ''; res.data.forEach(d => { container.innerHTML += `${d.domain}
`; }); } else { container.innerHTML = 'Fehler.
'; } } catch(e) {} }
+ window.addPmgDomain = async function() { const domain = document.getElementById('pmgNewDomain').value.trim(); if(!domain) return; const fd = new FormData(); fd.append('node_id', document.getElementById('pmgNodeId').value); fd.append('domain', domain); const res = await (await fetch('api.php?action=pmg_add_domain', {method: 'POST', body: fd})).json(); if(res.success) { document.getElementById('pmgNewDomain').value = ''; window.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) window.loadPmgDomains(document.getElementById('pmgNodeId').value); else alert('Fehler.'); }
+ window.loadPmgNetworks = async function(nodeId) { const container = document.getElementById('pmgNetworksContainer'); if(!container) return; container.innerHTML = 'Lade...
'; try { const res = await (await fetch(`api.php?action=pmg_get_networks&node_id=${nodeId}`)).json(); if(res.success && res.data) { if(res.data.length === 0) { container.innerHTML = 'Keine Netzwerke.
'; return; } container.innerHTML = ''; res.data.forEach(n => { container.innerHTML += `${n.cidr}
`; }); } else { container.innerHTML = 'Fehler.
'; } } catch(e) {} }
+ window.addPmgNetwork = async function() { const cidr = document.getElementById('pmgNewNetwork').value.trim(); if(!cidr) return; const fd = new FormData(); fd.append('node_id', document.getElementById('pmgNodeId').value); fd.append('cidr', cidr); const res = await (await fetch('api.php?action=pmg_add_network', {method: 'POST', body: fd})).json(); if(res.success) { document.getElementById('pmgNewNetwork').value = ''; window.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) window.loadPmgNetworks(document.getElementById('pmgNodeId').value); else alert('Fehler.'); }
+ window.loadPmgSsl = async function(nodeId) { const internalName = document.getElementById('pmgInternalName').value; const container = document.getElementById('pmgSslContainer'); if(!container) return; container.innerHTML = 'Lade Zertifikate...
'; try { const res = await (await fetch(`api.php?action=pmg_get_ssl&node_id=${nodeId}&internal_name=${internalName}`)).json(); if(res.success && res.data) { container.innerHTML = ''; res.data.forEach(cert => { const validUntil = cert.notafter ? formatDate(cert.notafter) : 'Unbekannt'; const isExpired = cert.notafter && cert.notafter < (Date.now() / 1000); const statusDot = isExpired ? '' : ''; container.innerHTML += `${statusDot}
${cert.filename || cert.subject || 'Zertifikat'}
Aussteller: ${cert.issuer || '-'}
Subject: ${cert.subject || '-'}
Ablaufdatum: ${validUntil}
`; }); } else { container.innerHTML = 'Fehler beim Laden.
'; } } catch(e) { container.innerHTML = 'Netzwerkfehler.
'; } }
+ window.uploadPmgSsl = async function() { const cert = document.getElementById('pmgSslCert').value.trim(); const key = document.getElementById('pmgSslKey').value.trim(); if(!cert || !key) return alert('Bitte Key und Zertifikat 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 neustart...'); document.getElementById('pmgSslCert').value = ''; document.getElementById('pmgSslKey').value = ''; window.loadPmgSsl(document.getElementById('pmgNodeId').value); } else { alert('Fehler beim Upload.'); } } catch(e) { alert('Netzwerkfehler.'); } finally { btn.innerText = oTxt; } }
+
+ // --- SONSTIGE MODALS ---
+ window.openUserManager = function() { const m = document.getElementById('userManagerModal'); if(m) { m.classList.remove('hidden'); if(typeof window.loadUsersIntoTable === 'function') window.loadUsersIntoTable(); } }
+ window.closeUserManager = function() { const m = document.getElementById('userManagerModal'); if(m) m.classList.add('hidden'); }
+ window.loadUsersIntoTable = async function() { const tbody = document.getElementById('userTableBody'); if(!tbody) return; try { const res = await (await fetch('api.php?action=get_users')).json(); if(res.success) { tbody.innerHTML = ''; res.data.forEach(user => { const isMe = user.username === window.APP.username; const deleteBtn = isMe ? `Das bist du` : ``; let roleBadge = user.role === 'admin' ? 'Admin' : (user.role === 'vm_manager' ? 'VM Manager' : 'Viewer'); tbody.innerHTML += `| ${user.username} | ${roleBadge} | ${deleteBtn} |
`; }); } } catch (err) {} }
+ window.handleRoleChange = async function() { const role = document.getElementById('newUserRole').value; const vmBlock = document.getElementById('vmPermissionsBlock'); if (role === 'vm_manager') { vmBlock.classList.remove('hidden'); try { const res = await (await fetch('api.php?action=get_top_vms')).json(); if (res.success && res.data) { const list = document.getElementById('vmCheckboxList'); list.innerHTML = ''; res.data.forEach(vm => { list.innerHTML += ``; }); } } catch(e) {} } else vmBlock.classList.add('hidden'); }
+ const addNewUserForm = document.getElementById('addNewUserForm');
+ if(addNewUserForm) { addNewUserForm.addEventListener('submit', async function(e) { e.preventDefault(); const btn = this.querySelector('button[type="submit"]'); const oTxt = btn.innerText; btn.innerText = 'Speichere...'; const fd = new FormData(); fd.append('username', document.getElementById('newUsername').value); fd.append('password', document.getElementById('newUserPass').value); const role = document.getElementById('newUserRole').value; fd.append('role', role); let permissions = {}; if (role === 'vm_manager') { let allowedVms = []; document.querySelectorAll('input[name="allowed_vms[]"]:checked').forEach(cb => allowedVms.push(cb.value)); permissions.allowed_vms = allowedVms; } fd.append('permissions', JSON.stringify(permissions)); const res = await (await fetch('api.php?action=create_user', { method: 'POST', body: fd })).json(); if(res.success) { addNewUserForm.reset(); window.handleRoleChange(); window.loadUsersIntoTable(); } else alert('Fehler.'); btn.innerText = oTxt; }); }
+ window.deleteUser = async function(id, name) { if(!confirm(`Benutzer '${name}' löschen?`)) return; const fd = new FormData(); fd.append('id', id); await fetch('api.php?action=delete_user', { method: 'POST', body: fd }); window.loadUsersIntoTable(); }
- // VM List Modal
window.openVmManager = async function() {
const m = document.getElementById('vmManagerModal'); if(!m) return; m.classList.remove('hidden');
const tbody = document.getElementById('vmTableBody'); if(!tbody) return; tbody.innerHTML = '| Lade VMs... |
';
@@ -452,9 +524,13 @@ if (window.APP.isLoggedIn && window.APP.nodeCount > 0) {
}
window.closeVmManager = function() { const m = document.getElementById('vmManagerModal'); if(m) m.classList.add('hidden'); }
- // Server & API Modal
window.openNodeManager = async function() {
const m = document.getElementById('nodeManagerModal'); if(!m) return; m.classList.remove('hidden');
+ window.loadNodesIntoTable();
+ }
+ window.closeNodeManager = function() { const m = document.getElementById('nodeManagerModal'); if(m) m.classList.add('hidden'); }
+ window.toggleAddNodeForm = function() { const f = document.getElementById('addNodeFormContainer'); if(f) f.classList.toggle('hidden'); }
+ window.loadNodesIntoTable = async function() {
try {
const res = await (await fetch('api.php?action=get_nodes')).json();
if(res.success) {
@@ -462,11 +538,36 @@ if (window.APP.isLoggedIn && window.APP.nodeCount > 0) {
if(res.data.length === 0) { tbody.innerHTML = '| Keine Server. |
'; return; }
res.data.forEach(node => {
let typeBadge = node.type === 'pbs' ? 'PBS Backup' : (node.type === 'pmg' ? 'PMG Mail' : 'PVE Node');
- tbody.innerHTML += `| ${node.name} | ${typeBadge} | ${node.ip_address} | |
`;
+ tbody.innerHTML += `| ${node.name} | ${typeBadge} | ${node.ip_address} | |
`;
});
}
- } catch (err) {}
+ } catch (err) {}
}
- window.closeNodeManager = function() { const m = document.getElementById('nodeManagerModal'); if(m) m.classList.add('hidden'); }
- window.toggleAddNodeForm = function() { const f = document.getElementById('addNodeFormContainer'); if(f) f.classList.toggle('hidden'); }
+ window.deleteNode = async function(id, name) { if(!confirm(`Server '${name}' löschen?`)) return; const fd = new FormData(); fd.append('id', id); await fetch('api.php?action=delete_node', { method: 'POST', body: fd }); window.loadNodesIntoTable(); loadSidebarNodes(); }
+
+ const addNewNodeForm = document.getElementById('addNewNodeForm');
+ if(addNewNodeForm) { addNewNodeForm.addEventListener('submit', async function(e) { e.preventDefault(); const btn = this.querySelector('button[type="submit"]'); const oTxt = btn.innerText; btn.innerText = 'Verbinde...'; const fd = new FormData(); fd.append('name', document.getElementById('newNodeName').value); fd.append('ip', document.getElementById('newNodeIp').value); fd.append('user', document.getElementById('newNodeUser').value); fd.append('pass', document.getElementById('newNodePass').value); fd.append('type', document.getElementById('newNodeType').value); try { const res = await (await fetch('api.php?action=add_node', { method: 'POST', body: fd })).json(); if(res.success) { addNewNodeForm.reset(); window.toggleAddNodeForm(); window.loadNodesIntoTable(); loadSidebarNodes(); alert('Erfolgreich angebunden!'); } else alert(res.error); } catch(err) {} btn.innerText = oTxt; }); }
+
+ window.openNodeTopology = async function() { const m = document.getElementById('nodeTopologyModal'); if(!m) return; m.classList.remove('hidden'); const container = document.getElementById('nodeTopologyContainer'); if(!container) return; container.innerHTML = 'Lade Cluster-Daten...
'; try { const [nodesRes, vmsRes] = await Promise.all([ fetch('api.php?action=get_nodes').then(r => r.json()), fetch('api.php?action=get_all_vms').then(r => r.json()) ]); if(nodesRes.success && vmsRes.success) { container.innerHTML = ''; const pveNodes = nodesRes.data.filter(n => n.type === 'pve'); if(pveNodes.length === 0) return; pveNodes.forEach(node => { const nodeVms = vmsRes.data.filter(v => v.node_id == node.id); let vmsHtml = ''; if(nodeVms.length > 0) { nodeVms.sort((a, b) => { if(a.status === 'running' && b.status !== 'running') return -1; if(a.status !== 'running' && b.status === 'running') return 1; return a.name.localeCompare(b.name); }); nodeVms.forEach(vm => { const icon = vm.type === 'lxc' ? '📦' : '🖥️'; const isRunning = vm.status === 'running'; const statusColor = isRunning ? 'border-green-500/30 bg-green-500/10 text-green-400' : 'border-darkborder bg-darkcard text-gray-400'; const dot = isRunning ? '' : ''; vmsHtml += ``; }); } container.innerHTML += `${node.name}
${node.ip_address}
${vmsHtml}
`; }); } } catch(e) {} }
+ window.closeNodeTopology = function() { const m = document.getElementById('nodeTopologyModal'); if(m) m.classList.add('hidden'); }
+
+ window.openAuditLog = async function() { const m = document.getElementById('auditLogModal'); if(m) m.classList.remove('hidden'); const tbody = document.getElementById('auditLogTableBody'); if(!tbody) return; tbody.innerHTML = '| Lade Protokoll... |
'; try { const res = await (await fetch('api.php?action=get_audit_logs')).json(); if (res.success) { tbody.innerHTML = ''; if(res.data.length === 0) { tbody.innerHTML = '| Noch keine Einträge. |
'; return; } res.data.forEach(log => { const d = new Date(log.timestamp + 'Z'); const timeStr = d.toLocaleDateString('de-DE') + ' ' + d.toLocaleTimeString('de-DE'); const isSystem = log.username === 'System'; tbody.innerHTML += `| ${timeStr} | ${log.username} | ${log.action} | ${log.target || '-'} |
`; }); } } catch(e) { tbody.innerHTML = '| Fehler beim Laden. |
'; } }
+ window.closeAuditLog = function() { const m = document.getElementById('auditLogModal'); if(m) m.classList.add('hidden'); }
+
+ window.openTaskLog = async function(upid, nodeId, host) { const m = document.getElementById('taskLogModal'); if(!m) return; m.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() { const m = document.getElementById('taskLogModal'); if(m) m.classList.add('hidden'); }
+
+ window.openCronManager = async function() { const m = document.getElementById('cronManagerModal'); if(!m) return; m.classList.remove('hidden'); const select = document.getElementById('cronNodeId'); if(select) { select.innerHTML = ''; try { const res = await (await fetch('api.php?action=get_nodes')).json(); if (res.success) { select.innerHTML = ''; res.data.filter(n => n.type === 'pve').forEach(node => { select.innerHTML += ``; }); } } catch(e) {} } window.loadCronJobs(); }
+ window.closeCronManager = function() { const m = document.getElementById('cronManagerModal'); if(m) m.classList.add('hidden'); }
+ window.checkCronAction = function() { const action = document.getElementById('cronAction').value; const vmBlock = document.getElementById('cronVmBlock'); if(vmBlock) { if (action.includes('_vm')) vmBlock.classList.remove('hidden'); else vmBlock.classList.add('hidden'); } }
+ window.loadCronJobs = async function() { const tbody = document.getElementById('cronTableBody'); if(!tbody) return; tbody.innerHTML = '| Lade Jobs... |
'; try { const res = await (await fetch('api.php?action=get_cron_jobs')).json(); if (res.success) { tbody.innerHTML = ''; if(res.data.length === 0) { tbody.innerHTML = '| Keine geplanten Jobs. |
'; return; } res.data.forEach(job => { const lastRun = formatDate(job.last_run); const isActive = parseInt(job.is_active) === 1; const statusDot = isActive ? ' Aktiv' : ' Pausiert'; const targetStr = job.action_type.includes('_vm') ? `VM ${job.target_vmid}` : 'Gesamter Host'; let actionStr = job.action_type; if(actionStr === 'reboot_node') actionStr = 'Host Reboot'; if(actionStr === 'start_vm') actionStr = 'VM Start'; if(actionStr === 'stop_vm') actionStr = 'VM Stop'; if(actionStr === 'reboot_vm') actionStr = 'VM Reboot'; tbody.innerHTML += `${statusDot} | ${job.name} ${actionStr} | ${job.node_name} Ziel: ${targetStr} | ${job.cron_schedule} | ${lastRun} | |
`; }); } } catch(e) {} }
+ window.toggleCronDate = function() { const d = document.getElementById('cronDays').value; const i = document.getElementById('cronSpecificDate'); if(i) { if (d === 'specific') { i.classList.remove('hidden'); i.required = true; } else { i.classList.add('hidden'); i.required = false; } } }
+ const addCronForm = document.getElementById('addCronJobForm');
+ if(addCronForm) { addCronForm.addEventListener('submit', async function(e) { e.preventDefault(); const btn = this.querySelector('button[type="submit"]'); const oTxt = btn.innerText; btn.innerText = 'Speichere...'; const 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('-'); 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'; window.toggleCronDate(); window.checkCronAction(); window.loadCronJobs(); } else alert('Fehler.'); } catch(e) {} finally { btn.innerText = oTxt; } }); }
+ window.toggleCronJob = async function(id, newState) { const fd = new FormData(); fd.append('id', id); fd.append('is_active', newState); await fetch('api.php?action=toggle_cron_job', {method: 'POST', body: fd}); window.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}); window.loadCronJobs(); }
+
+ window.openUpdateManager = async function() { const m = document.getElementById('updateManagerModal'); if(m) m.classList.remove('hidden'); const content = document.getElementById('updateManagerContent'); if(!content) return; content.innerHTML = 'Prüfe Updates...
'; 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 = '🎉 Alle Systeme sind auf dem neuesten Stand!
'; return; } res.data.forEach(node => { let pkgsHtml = ''; node.packages.forEach(p => { pkgsHtml += `${p.Title}${p.OldVersion} ➔ ${p.Version}
`; }); content.innerHTML += ` ${node.display_name} ${node.packages.length} Updates
${pkgsHtml}
`; }); } } catch(e) {} }
+ window.closeUpdateManager = function() { const m = document.getElementById('updateManagerModal'); if(m) m.classList.add('hidden'); if(typeof window.fetchUpdates === 'function') window.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) { window.openTaskLog(res.upid, nodeId, host); } else alert('Fehler: ' + res.error); } catch(e) {} }
}
\ No newline at end of file