diff --git a/src/app.js b/src/app.js index 463aef5..87002b0 100644 --- a/src/app.js +++ b/src/app.js @@ -23,9 +23,8 @@ if (window.APP.isLoggedIn && window.APP.nodeCount > 0) { } } - const pwdModal = document.getElementById('passwordModal'); - window.openPasswordModal = function() { if(pwdModal) { pwdModal.classList.remove('hidden'); document.getElementById('changePasswordForm').reset(); } } - window.closePasswordModal = function() { if(pwdModal) pwdModal.classList.add('hidden'); } + 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) { @@ -65,7 +64,8 @@ if (window.APP.isLoggedIn && window.APP.nodeCount > 0) { } }); document.querySelectorAll('[id^="nav-node-"]').forEach(el => el.classList.remove('text-white', 'font-bold')); - if(tab === 'pbs') fetchPbsStats(); if(tab === 'pmg') fetchPmgStats(); + if(tab === 'pbs') { if(typeof fetchPbsStats === 'function') fetchPbsStats(); } + if(tab === 'pmg') { if(typeof fetchPmgStats === 'function') fetchPmgStats(); } } window.openNodeView = async function(nodeId, host) { @@ -74,41 +74,54 @@ if (window.APP.isLoggedIn && window.APP.nodeCount > 0) { const activeNav = document.getElementById(`nav-node-${host}`); if(activeNav) activeNav.classList.add('text-white', 'font-bold'); - document.getElementById('nodeViewTitle').innerText = 'Host: ' + host; + const titleEl = document.getElementById('nodeViewTitle'); + if(titleEl) titleEl.innerText = 'Host: ' + host; const headerBtn = document.getElementById('nodeViewHeaderBtn'); const backupBtn = document.getElementById('nodeViewBackupBtn'); - const newHeaderBtn = headerBtn.cloneNode(true); headerBtn.parentNode.replaceChild(newHeaderBtn, headerBtn); - newHeaderBtn.addEventListener('click', () => openLiveGraph('node', 0, host, nodeId, host)); - const newBackupBtn = backupBtn.cloneNode(true); backupBtn.parentNode.replaceChild(newBackupBtn, backupBtn); - newBackupBtn.addEventListener('click', () => openNodeBackups(nodeId, host)); + + if(headerBtn) { + const newHeaderBtn = headerBtn.cloneNode(true); headerBtn.parentNode.replaceChild(newHeaderBtn, headerBtn); + newHeaderBtn.addEventListener('click', () => openLiveGraph('node', 0, host, nodeId, host)); + } + if(backupBtn) { + const newBackupBtn = backupBtn.cloneNode(true); backupBtn.parentNode.replaceChild(newBackupBtn, backupBtn); + newBackupBtn.addEventListener('click', () => openNodeBackups(nodeId, host)); + } window.currentSelectedNodeId = nodeId; window.currentSelectedHost = host; - const storContainer = document.getElementById('nodeStoragesContainer'); storContainer.innerHTML = '
Lade Storages...
'; - const vmsBody = document.getElementById('nodeVmsTableBody'); vmsBody.innerHTML = 'Lade VMs...'; + 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) { - 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(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}
`; + }); + } - 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}`; - }); + 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) {} } @@ -126,9 +139,15 @@ if (window.APP.isLoggedIn && window.APP.nodeCount > 0) { const res = await (await fetch('api.php?action=get_stats')).json(); if(res.success && res.data) { const d = res.data; - document.getElementById('stat-cpu-text').innerText = `${d.cpu_percent}% (${d.cpu_cores} Cores)`; document.getElementById('stat-cpu-bar').style.width = `${d.cpu_percent}%`; - let ramPercent = (d.ram_used / d.ram_total) * 100 || 0; document.getElementById('stat-ram-text').innerText = `${formatBytes(d.ram_used)} / ${formatBytes(d.ram_total)}`; document.getElementById('stat-ram-bar').style.width = `${ramPercent}%`; - let diskPercent = (d.disk_used / d.disk_total) * 100 || 0; document.getElementById('stat-disk-text').innerText = `${formatBytes(d.disk_used)} / ${formatBytes(d.disk_total)}`; document.getElementById('stat-disk-bar').style.width = `${diskPercent}%`; + if(document.getElementById('stat-cpu-text')) document.getElementById('stat-cpu-text').innerText = `${d.cpu_percent}% (${d.cpu_cores} Cores)`; + if(document.getElementById('stat-cpu-bar')) document.getElementById('stat-cpu-bar').style.width = `${d.cpu_percent}%`; + let ramPercent = (d.ram_used / d.ram_total) * 100 || 0; + if(document.getElementById('stat-ram-text')) document.getElementById('stat-ram-text').innerText = `${formatBytes(d.ram_used)} / ${formatBytes(d.ram_total)}`; + if(document.getElementById('stat-ram-bar')) document.getElementById('stat-ram-bar').style.width = `${ramPercent}%`; + let diskPercent = (d.disk_used / d.disk_total) * 100 || 0; + if(document.getElementById('stat-disk-text')) document.getElementById('stat-disk-text').innerText = `${formatBytes(d.disk_used)} / ${formatBytes(d.disk_total)}`; + if(document.getElementById('stat-disk-bar')) document.getElementById('stat-disk-bar').style.width = `${diskPercent}%`; + if (d.cluster_stats) { const elNodesOn = document.getElementById('stat-nodes-online'); 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'; } @@ -166,81 +185,76 @@ if (window.APP.isLoggedIn && window.APP.nodeCount > 0) { async function fetchRecentJobs() { if(!document.getElementById('recent-jobs-container')) return; try { const res = await (await fetch('api.php?action=get_recent_jobs')).json(); if(res.success && res.data) { const container = document.getElementById('recent-jobs-container'); container.innerHTML = ''; if(res.data.length === 0) { container.innerHTML = '

Keine aktuellen Jobs.

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

${jobTypeStr}

Host: ${job.node_name}

${statusText}

${dateStr} - ${timeStr}

`; }); } } catch (err) {} } 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) {} } - window.openUpdateManager = async function() { document.getElementById('updateManagerModal').classList.remove('hidden'); const content = document.getElementById('updateManagerContent'); 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() { document.getElementById('updateManagerModal').classList.add('hidden'); fetchUpdates(); } + 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'); 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) {} } // --- PBS FUNKTIONEN --- - async function fetchPbsStats() { 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(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. Bitte Seite neu laden (F5).'); document.getElementById('pbsDatastoreModal').classList.remove('hidden'); document.getElementById('pbsModalTitle').innerText = 'Datastore: ' + storeName; document.getElementById('pbsNodeId').value = nodeId; document.getElementById('pbsStoreName').value = storeName; loadPbsBackups(nodeId, storeName); loadPbsJobs(nodeId, storeName); loadPbsSyncJobs(nodeId); } - window.closePbsDatastore = function() { document.getElementById('pbsDatastoreModal').classList.add('hidden'); } - async function loadPbsBackups(nodeId, storeName) { const container = document.getElementById('pbsBackupsContainer'); container.innerHTML = '

Durchsuche Namespaces nach Backups...

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

Keine Backups vorhanden.

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

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

${timeStr} | ${sizeStr}

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

Lade System-Jobs...

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

${job.worker_type}

${timeStr}

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

Keine System-Jobs gefunden.

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

Lade Sync-Jobs...

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

Keine Sync-Jobs eingerichtet.

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

${sync.id}

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

Ziel: ${sync.store}

Zeitplan: ${sync.schedule || 'Manuell'}
`; }); } } catch(e) { container.innerHTML = 'Fehler.'; } } + window.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 --- - async function fetchPmgStats() { 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(res.data.length === 0) { container.innerHTML = '
Keine Mail Gateways angebunden.
'; return; } container.innerHTML = ''; res.data.forEach(pmg => { const cpuPercent = ((pmg.cpu || 0) * 100).toFixed(1); const ramPercent = pmg.memory && pmg.memory.total ? ((pmg.memory.used / pmg.memory.total) * 100).toFixed(1) : 0; const diskPercent = pmg.rootfs && pmg.rootfs.total ? ((pmg.rootfs.used / pmg.rootfs.total) * 100).toFixed(1) : 0; container.innerHTML += `

${pmg.host}

Online

CPU${cpuPercent}%
RAM${ramPercent}%
System Disk${diskPercent}%
`; }); } } catch(e) {} } - window.openPmgManager = function(nodeId, hostName, internalName) { document.getElementById('pmgManagerModal').classList.remove('hidden'); document.getElementById('pmgModalTitle').innerText = 'Mail Gateway: ' + hostName; document.getElementById('pmgNodeId').value = nodeId; document.getElementById('pmgHostName').value = hostName; document.getElementById('pmgInternalName').value = internalName; switchPmgTab('spam'); } - window.closePmgManager = function() { document.getElementById('pmgManagerModal').classList.add('hidden'); } + window.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 += `

${pmg.host}

Online

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; 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') loadPmgSpamQueue(nodeId); if(tabName === 'tracking') loadPmgTracking(); if(tabName === 'queues') loadPmgQueues(); if(tabName === 'settings') { loadPmgConfig(nodeId); loadPmgSsl(nodeId); loadPmgDomains(nodeId); loadPmgNetworks(nodeId); } } - window.loadPmgSpamQueue = async function(nodeId) { const table = document.getElementById('pmgSpamTable'); table.innerHTML = 'Lade Spam Quarantäne...'; try { const res = await (await fetch(`api.php?action=pmg_get_spam_queue&node_id=${nodeId}`)).json(); if(res.success && res.data) { table.innerHTML = ''; if(res.data.length === 0) { table.innerHTML = '🎉 Keine Mails in der Quarantäne.'; return; } res.data.sort((a,b) => b.time - a.time).forEach(mail => { const timeStr = formatDate(mail.time); const subject = mail.subject || 'Kein Betreff'; const sender = mail.sender || 'Unbekannt'; const receiver = mail.receiver || 'Unbekannt'; const score = mail.spamlevel || 0; table.innerHTML += `${timeStr}${sender}${receiver}${subject}${score}`; }); } else { table.innerHTML = 'Fehler beim Laden.'; } } catch(e) { table.innerHTML = 'Netzwerkfehler.'; } } + window.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) loadPmgSpamQueue(document.getElementById('pmgNodeId').value); else alert('Aktion fehlgeschlagen.'); } catch(e) { alert('Netzwerkfehler'); } } - window.loadPmgTracking = async function() { const nodeId = document.getElementById('pmgNodeId').value; const internalName = document.getElementById('pmgInternalName').value; const filter = document.getElementById('pmgTrackingFilter').value.trim(); const table = document.getElementById('pmgTrackingTable'); table.innerHTML = 'Lese Syslog (Letzte 24h)...'; try { const res = await (await fetch(`api.php?action=pmg_get_tracking&node_id=${nodeId}&internal_name=${internalName}&filter=${encodeURIComponent(filter)}`)).json(); if(res.success && res.data) { table.innerHTML = ''; if(res.data.length === 0) { table.innerHTML = `Keine Logs für diesen Filter gefunden.`; return; } res.data.forEach(log => { const timestamp = log.time || log.timestamp || 0; const timeStr = timestamp ? formatDate(timestamp) : 'Unbekannt'; const sender = log.from || log.sender || '-'; const receiver = log.to || log.receiver || '-'; let statusTxt = 'Unbekannt'; let statusColor = 'text-gray-400'; if (log.dstatus) { if (log.dstatus === 'A') { statusTxt = 'Delivered / Accept'; statusColor = 'text-green-500'; } else if (log.dstatus === 'N') { statusTxt = 'Rejected / Blocked'; statusColor = 'text-red-500'; } else if (log.dstatus === 'Q') { statusTxt = 'Quarantined'; statusColor = 'text-orange-500'; } else if (log.dstatus === 'B') { statusTxt = 'Bounced'; statusColor = 'text-red-500'; } else { statusTxt = log.dstatus; statusColor = 'text-blue-400'; } } else if (log.relay) { statusTxt = 'Relayed (' + log.relay.split('[')[0] + ')'; statusColor = 'text-blue-400'; } else if (log.msgid) { statusTxt = 'In Processing (' + log.msgid + ')'; statusColor = 'text-gray-400'; } else if (log.client) { statusTxt = 'Connection from ' + log.client.split('[')[0]; statusColor = 'text-gray-500'; } else if (log.id && log.id.length > 5) { statusTxt = 'Queue ID: ' + log.id; statusColor = 'text-gray-400'; } else { statusTxt = JSON.stringify(log).substring(0, 30); } table.innerHTML += `${timeStr}${sender}${receiver}${statusTxt}`; }); } else { table.innerHTML = 'Syslog konnte nicht gelesen werden.'; } } catch(e) { table.innerHTML = 'Netzwerkfehler.'; } } - window.loadPmgQueues = async function() { const nodeId = document.getElementById('pmgNodeId').value; const internalName = document.getElementById('pmgInternalName').value; const table = document.getElementById('pmgQueueTable'); const summary = document.getElementById('pmgQueuesSummary'); table.innerHTML = 'Lese Warteschlangen aus Postfix...'; summary.innerHTML = '
Lade...
'; try { const res = await (await fetch(`api.php?action=pmg_get_queues&node_id=${nodeId}&internal_name=${internalName}`)).json(); if(res.success && res.data) { let activeCount = 0, deferCount = 0, holdCount = 0; res.data.forEach(q => { const queueName = q.queue_name || q.queue || ''; if(queueName === 'active') activeCount++; else if(queueName === 'deferred') deferCount++; else holdCount++; }); summary.innerHTML = `

Active Queue

${activeCount}

Deferred (Greylisted/Wartend)

${deferCount}

Other / Hold

${holdCount}

`; table.innerHTML = ''; if(res.data.length === 0) { table.innerHTML = `Alle Postfix-Queues sind komplett leer.`; return; } res.data.forEach(q => { const queueName = q.queue_name || q.queue || 'unknown'; const timestamp = q.arrival_time || q.time || 0; const timeStr = timestamp ? formatDate(timestamp) : 'Unbekannt'; const sender = q.sender || '-'; let receiver = q.receiver || q.recipients || '-'; if (Array.isArray(receiver)) receiver = receiver[0]; const reason = q.reason || q.error || '-'; const qColor = queueName === 'deferred' ? 'text-orange-400' : (queueName === 'active' ? 'text-blue-400' : 'text-gray-400'); table.innerHTML += `${queueName}${timeStr}${sender}${receiver}${reason}`; }); } else { table.innerHTML = 'Queues konnten nicht gelesen werden.'; } } catch(e) { table.innerHTML = 'Netzwerkfehler.'; } } + window.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}

Other / Hold

${holdCount}

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

Lade...

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

Keine Domains.

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

Fehler.

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

Lade...

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

Keine Netzwerke.

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

Fehler.

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

Lade Zertifikate...

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

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

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

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

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

Fehler beim Laden.

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

Netzwerkfehler.

'; } } - window.uploadPmgSsl = async function() { const cert = document.getElementById('pmgSslCert').value.trim(); const key = document.getElementById('pmgSslKey').value.trim(); if(!cert || !key) return alert('Bitte Key und Zertifikat (PEM) einfügen!'); const btn = event.target; const oTxt = btn.innerText; btn.innerText = 'Lade hoch...'; const fd = new FormData(); fd.append('node_id', document.getElementById('pmgNodeId').value); fd.append('internal_name', document.getElementById('pmgInternalName').value); fd.append('certificate', cert); fd.append('private_key', key); try { const res = await (await fetch('api.php?action=pmg_upload_ssl', {method: 'POST', body: fd})).json(); if(res.success) { alert('Zertifikat hochgeladen! Dienste werden neu gestartet.'); document.getElementById('pmgSslCert').value = ''; document.getElementById('pmgSslKey').value = ''; loadPmgSsl(document.getElementById('pmgNodeId').value); } else { alert('Fehler beim Upload. (Format prüfen)'); } } catch(e) { alert('Netzwerkfehler.'); } finally { btn.innerText = oTxt; } } + 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 = ''; loadPmgSsl(document.getElementById('pmgNodeId').value); } else { alert('Fehler beim Upload.'); } } catch(e) { alert('Netzwerkfehler.'); } finally { btn.innerText = oTxt; } } - // --- Sonstige Modals (User, Topology, TaskLog, etc) --- - const userModal = document.getElementById('userManagerModal'); - window.openUserManager = function() { userModal.classList.remove('hidden'); loadUsersIntoTable(); } - window.closeUserManager = function() { userModal.classList.add('hidden'); } - async function loadUsersIntoTable() { try { const res = await (await fetch('api.php?action=get_users')).json(); if(res.success) { const tbody = document.getElementById('userTableBody'); tbody.innerHTML = ''; res.data.forEach(user => { const isMe = user.username === window.APP.username; const deleteBtn = isMe ? `Das bist du` : ``; let roleBadge = user.role === 'admin' ? 'Admin' : (user.role === 'vm_manager' ? 'VM Manager' : 'Viewer'); tbody.innerHTML += `${user.username}${roleBadge}${deleteBtn}`; }); } } catch (err) {} } + // --- SONSTIGE MODALS & LÖSCHUNGEN --- + window.openUserManager = function() { const m = document.getElementById('userManagerModal'); if(m) { m.classList.remove('hidden'); loadUsersIntoTable(); } } + window.closeUserManager = function() { const m = document.getElementById('userManagerModal'); if(m) m.classList.add('hidden'); } + async function loadUsersIntoTable() { 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(); handleRoleChange(); loadUsersIntoTable(); } else alert('Fehler.'); btn.innerText = oTxt; }); } window.deleteUser = async function(id, name) { if(!confirm(`Benutzer '${name}' löschen?`)) return; const fd = new FormData(); fd.append('id', id); await fetch('api.php?action=delete_user', { method: 'POST', body: fd }); loadUsersIntoTable(); } - const vmModal = document.getElementById('vmManagerModal'); - window.openVmManager = function() { vmModal.classList.remove('hidden'); loadVmsIntoTable(); } - window.closeVmManager = function() { vmModal.classList.add('hidden'); } - async function loadVmsIntoTable() { const tbody = document.getElementById('vmTableBody'); tbody.innerHTML = 'Lade VMs...'; try { const res = await (await fetch('api.php?action=get_all_vms')).json(); if(res.success) { tbody.innerHTML = ''; if(res.data.length === 0) { tbody.innerHTML = 'Keine VMs.'; return; } res.data.forEach(vm => { const isRunning = vm.status === 'running'; const typeStr = vm.type === 'lxc' ? '📦 LXC' : '🖥️ QEMU'; const statusBadge = isRunning ? 'Online' : 'Offline'; let actionButtons = isRunning ? `` : ``; actionButtons += ``; tbody.innerHTML += `${vm.vmid} | ${vm.name}${typeStr} - ${vm.host}${vm.maxcpu||1} C / ${formatBytes(vm.maxmem||0)}${statusBadge}${actionButtons}`; }); } } catch (err) {} } + window.openVmManager = function() { const m = document.getElementById('vmManagerModal'); if(m) { m.classList.remove('hidden'); loadVmsIntoTable(); } } + window.closeVmManager = function() { const m = document.getElementById('vmManagerModal'); if(m) m.classList.add('hidden'); } + async function loadVmsIntoTable() { const tbody = document.getElementById('vmTableBody'); if(!tbody) return; tbody.innerHTML = 'Lade VMs...'; try { const res = await (await fetch('api.php?action=get_all_vms')).json(); if(res.success) { tbody.innerHTML = ''; if(res.data.length === 0) { tbody.innerHTML = 'Keine VMs.'; return; } res.data.forEach(vm => { const isRunning = vm.status === 'running'; const typeStr = vm.type === 'lxc' ? '📦 LXC' : '🖥️ QEMU'; const statusBadge = isRunning ? 'Online' : 'Offline'; let actionButtons = isRunning ? `` : ``; actionButtons += ``; tbody.innerHTML += `${vm.vmid} | ${vm.name}${typeStr} - ${vm.host}${vm.maxcpu||1} C / ${formatBytes(vm.maxmem||0)}${statusBadge}${actionButtons}`; }); } } catch (err) {} } - const topologyModal = document.getElementById('nodeTopologyModal'); - window.openNodeTopology = async function() { topologyModal.classList.remove('hidden'); const container = document.getElementById('nodeTopologyContainer'); container.innerHTML = '
Lade Cluster-Daten...
'; try { const [nodesRes, vmsRes] = await Promise.all([ fetch('api.php?action=get_nodes').then(r => r.json()), fetch('api.php?action=get_all_vms').then(r => r.json()) ]); if(nodesRes.success && vmsRes.success) { container.innerHTML = ''; const pveNodes = nodesRes.data.filter(n => n.type === 'pve'); if(pveNodes.length === 0) return; pveNodes.forEach(node => { const nodeVms = vmsRes.data.filter(v => v.node_id == node.id); let vmsHtml = ''; if(nodeVms.length > 0) { nodeVms.sort((a, b) => { if(a.status === 'running' && b.status !== 'running') return -1; if(a.status !== 'running' && b.status === 'running') return 1; return a.name.localeCompare(b.name); }); nodeVms.forEach(vm => { const icon = vm.type === 'lxc' ? '📦' : '🖥️'; const isRunning = vm.status === 'running'; const statusColor = isRunning ? 'border-green-500/30 bg-green-500/10 text-green-400' : 'border-darkborder bg-darkcard text-gray-400'; const dot = isRunning ? '' : ''; vmsHtml += `
${dot}${icon} ${vm.name}
ID: ${vm.vmid}${vm.maxcpu || 1}C / ${formatBytes(vm.maxmem || 0)}
`; }); } container.innerHTML += `

${node.name}

${node.ip_address}
${vmsHtml}
`; }); } } catch(e) {} } - window.closeNodeTopology = function() { topologyModal.classList.add('hidden'); } + window.openNodeTopology = async function() { const m = document.getElementById('nodeTopologyModal'); if(m) 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 += `
${dot}${icon} ${vm.name}
ID: ${vm.vmid}${vm.maxcpu || 1}C / ${formatBytes(vm.maxmem || 0)}
`; }); } container.innerHTML += `

${node.name}

${node.ip_address}
${vmsHtml}
`; }); } } catch(e) {} } + window.closeNodeTopology = function() { const m = document.getElementById('nodeTopologyModal'); if(m) m.classList.add('hidden'); } - 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.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'); } - const cronModal = document.getElementById('cronManagerModal'); - window.openCronManager = async function() { cronModal.classList.remove('hidden'); const select = document.getElementById('cronNodeId'); select.innerHTML = ''; try { const res = await (await fetch('api.php?action=get_nodes')).json(); if (res.success) { select.innerHTML = ''; res.data.filter(n => n.type === 'pve').forEach(node => { select.innerHTML += ``; }); } } catch(e) {} loadCronJobs(); } - window.closeCronManager = function() { cronModal.classList.add('hidden'); } - window.checkCronAction = function() { const action = document.getElementById('cronAction').value; const vmBlock = document.getElementById('cronVmBlock'); if (action.includes('_vm')) vmBlock.classList.remove('hidden'); else vmBlock.classList.add('hidden'); } - async function loadCronJobs() { const tbody = document.getElementById('cronTableBody'); tbody.innerHTML = 'Lade Jobs...'; try { const res = await (await fetch('api.php?action=get_cron_jobs')).json(); if (res.success) { tbody.innerHTML = ''; if(res.data.length === 0) { tbody.innerHTML = 'Keine geplanten Jobs.'; return; } res.data.forEach(job => { const lastRun = formatDate(job.last_run); const isActive = parseInt(job.is_active) === 1; const statusDot = isActive ? ' Aktiv' : ' Pausiert'; const targetStr = job.action_type.includes('_vm') ? `VM ${job.target_vmid}` : 'Gesamter Host'; let actionStr = job.action_type; if(actionStr === 'reboot_node') actionStr = 'Host Reboot'; if(actionStr === 'start_vm') actionStr = 'VM Start'; if(actionStr === 'stop_vm') actionStr = 'VM Stop'; if(actionStr === 'reboot_vm') actionStr = 'VM Reboot'; tbody.innerHTML += `
${statusDot}

${job.name}

${actionStr}

${job.node_name}

Ziel: ${targetStr}

${job.cron_schedule}${lastRun}`; }); } } catch(e) {} } - 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; } } + window.openCronManager = async function() { const m = document.getElementById('cronManagerModal'); if(m) 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) {} } 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'); } } + async function loadCronJobs() { 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'; toggleCronDate(); checkCronAction(); loadCronJobs(); } else alert('Fehler.'); } catch(e) {} finally { btn.innerText = oTxt; } }); } window.toggleCronJob = async function(id, newState) { const fd = new FormData(); fd.append('id', id); fd.append('is_active', newState); await fetch('api.php?action=toggle_cron_job', {method: 'POST', body: fd}); loadCronJobs(); } window.deleteCronJob = async function(id) { if(!confirm('Diesen geplanten Job wirklich löschen?')) return; const fd = new FormData(); fd.append('id', id); await fetch('api.php?action=delete_cron_job', {method: 'POST', body: fd}); loadCronJobs(); } - const auditModal = document.getElementById('auditLogModal'); - window.openAuditLog = async function() { auditModal.classList.remove('hidden'); const tbody = document.getElementById('auditLogTableBody'); tbody.innerHTML = 'Lade Protokoll...'; try { const res = await (await fetch('api.php?action=get_audit_logs')).json(); if (res.success) { tbody.innerHTML = ''; if(res.data.length === 0) { tbody.innerHTML = 'Noch keine Einträge.'; return; } res.data.forEach(log => { const d = new Date(log.timestamp + 'Z'); const timeStr = d.toLocaleDateString('de-DE') + ' ' + d.toLocaleTimeString('de-DE'); const isSystem = log.username === 'System'; tbody.innerHTML += `${timeStr}${log.username}${log.action}${log.target || '-'}`; }); } } catch(e) { tbody.innerHTML = 'Fehler beim Laden.'; } } - window.closeAuditLog = function() { auditModal.classList.add('hidden'); } + 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'); } // Initiale Intervall-Starts setInterval(fetchGlobalStats, 10000); setInterval(fetchTopVms, 10000); setInterval(fetchRecentJobs, 15000); setInterval(fetchUpdates, 60000); - setInterval(fetchPbsStats, 10000); - setInterval(fetchPmgStats, 10000); + setInterval(() => { if(typeof fetchPbsStats === 'function') fetchPbsStats(); }, 10000); + setInterval(() => { if(typeof fetchPmgStats === 'function') fetchPmgStats(); }, 10000); } \ No newline at end of file diff --git a/src/modals.php b/src/modals.php index 2bac8b1..d7badfb 100644 --- a/src/modals.php +++ b/src/modals.php @@ -1,91 +1,30 @@ - + - - + - - + - - + - \ No newline at end of file + + + + + + + + + + + + + + + + + \ No newline at end of file