Release v1.2: Manageing Node Page.

This commit is contained in:
root committed 2026-09-10 08:31:38 +02:00
1 parent 317cba0cbb
commit a39a04360d
2 files changed
+90 -137

No files matched your search

+68 -54
View File
@@ -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,23 +74,33 @@ 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');
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 = '<div class="text-gray-500 animate-pulse">Lade Storages...</div>';
const vmsBody = document.getElementById('nodeVmsTableBody'); vmsBody.innerHTML = '<tr><td colspan="5" class="text-center text-gray-500 py-4 animate-pulse">Lade VMs...</td></tr>';
const storContainer = document.getElementById('nodeStoragesContainer');
if(storContainer) storContainer.innerHTML = '<div class="text-gray-500 animate-pulse">Lade Storages...</div>';
const vmsBody = document.getElementById('nodeVmsTableBody');
if(vmsBody) vmsBody.innerHTML = '<tr><td colspan="5" class="text-center text-gray-500 py-4 animate-pulse">Lade VMs...</td></tr>';
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;
@@ -99,7 +109,9 @@ if (window.APP.isLoggedIn && window.APP.nodeCount > 0) {
<div class="w-full bg-darkcard rounded-full h-1.5 mb-1"><div class="bg-proxmox h-1.5 rounded-full" style="width: ${pct}%"></div></div>
<div class="text-xs text-gray-500">${st.content}</div></div>`;
});
}
if(vmsBody) {
vmsBody.innerHTML = '';
if(res.vms.length === 0) vmsBody.innerHTML = '<tr><td colspan="5" class="text-center text-gray-500 py-4">Keine VMs auf diesem Host.</td></tr>';
res.vms.forEach(vm => {
@@ -110,6 +122,7 @@ if (window.APP.isLoggedIn && window.APP.nodeCount > 0) {
vmsBody.innerHTML += `<tr class="border-b border-darkborder/50 hover:bg-darkbg transition-colors"><td class="py-2 px-3 text-white font-mono">${vm.vmid}</td><td class="py-2 px-3 text-white font-bold truncate max-w-[200px]">${vm.name}</td><td class="py-2 px-3 text-gray-400 whitespace-nowrap">${typeStr}</td><td class="py-2 px-3 whitespace-nowrap">${statusBadge}</td><td class="py-2 px-3 text-right whitespace-nowrap">${actionButtons}</td></tr>`;
});
}
}
} 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 = '<p class="text-gray-400 text-sm">Keine aktuellen Jobs.</p>'; return; } res.data.forEach(job => { const jobTypeStr = job.type || job.worker_type || 'unknown'; let statusColor = 'text-gray-400', statusIcon = '⏳', statusText = job.status || 'running...'; if(statusText.toLowerCase() === 'ok') { statusColor = 'text-green-500'; statusIcon = '✅'; } else if(statusText !== 'running...') { statusColor = 'text-red-500'; statusIcon = '❌'; } else { statusColor = 'text-blue-400'; statusIcon = '🔄'; } const date = new Date(job.starttime * 1000); const timeStr = date.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' }), dateStr = date.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' }); const isBackup = jobTypeStr.includes('sync') || jobTypeStr.includes('prune') || jobTypeStr.includes('garbage_collection') || jobTypeStr.includes('vzdump') || jobTypeStr.includes('verify'); const jobTypeColor = isBackup ? 'text-purple-400' : 'text-white'; container.innerHTML += `<div class="bg-darkbg border border-darkborder rounded-lg p-3 flex justify-between items-center transition-colors hover:bg-darkborder/50"><div class="flex items-center gap-3"><div class="text-lg">${statusIcon}</div><div class="max-w-[120px]"><p class="${jobTypeColor} font-medium text-sm capitalize truncate" title="${jobTypeStr}">${jobTypeStr}</p><p class="text-xs text-gray-500 truncate" title="${job.node_name}">Host: <span class="text-proxmox">${job.node_name}</span></p></div></div><div class="text-right"><p class="${statusColor} font-bold text-sm uppercase">${statusText}</p><p class="text-xs text-gray-500">${dateStr} - ${timeStr}</p></div></div>`; }); } } catch (err) {} }
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 = '<div class="text-center py-10 text-gray-500 animate-pulse">Prüfe Updates...</div>'; try { const res = await (await fetch('api.php?action=get_update_details')).json(); if (res.success) { content.innerHTML = ''; if(res.data.length === 0) { content.innerHTML = '<div class="text-center py-10 text-green-500 font-bold">🎉 Alle Systeme sind auf dem neuesten Stand!</div>'; return; } res.data.forEach(node => { let pkgsHtml = ''; node.packages.forEach(p => { pkgsHtml += `<div class="flex justify-between text-xs py-1 border-b border-darkborder/50"><span class="text-gray-300">${p.Title}</span><span class="text-gray-500">${p.OldVersion} ➔ <span class="text-proxmox">${p.Version}</span></span></div>`; }); content.innerHTML += `<div class="bg-darkcard border border-darkborder rounded-xl p-5 mb-4 shadow-lg"><div class="flex justify-between items-center mb-4 border-b border-darkborder pb-2"><h3 class="text-white font-bold flex items-center gap-2"><svg class="w-5 h-5 text-proxmox" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"></path></svg> ${node.display_name} <span class="text-xs bg-red-500/20 text-red-400 px-2 py-0.5 rounded ml-2">${node.packages.length} Updates</span></h3><button onclick="triggerAptUpgrade(${node.node_id}, '${node.host}')" class="bg-proxmox hover:bg-orange-600 text-white font-bold py-1.5 px-4 rounded text-sm transition-colors">Installieren & Upgraden</button></div><div class="max-h-60 overflow-y-auto pr-2">${pkgsHtml}</div></div>`; }); } } catch(e) {} }
window.closeUpdateManager = function() { document.getElementById('updateManagerModal').classList.add('hidden'); fetchUpdates(); }
window.openUpdateManager = async function() { const m = document.getElementById('updateManagerModal'); if(m) m.classList.remove('hidden'); const content = document.getElementById('updateManagerContent'); if(!content) return; content.innerHTML = '<div class="text-center py-10 text-gray-500 animate-pulse">Prüfe Updates...</div>'; try { const res = await (await fetch('api.php?action=get_update_details')).json(); if (res.success) { content.innerHTML = ''; if(res.data.length === 0) { content.innerHTML = '<div class="text-center py-10 text-green-500 font-bold">🎉 Alle Systeme sind auf dem neuesten Stand!</div>'; return; } res.data.forEach(node => { let pkgsHtml = ''; node.packages.forEach(p => { pkgsHtml += `<div class="flex justify-between text-xs py-1 border-b border-darkborder/50"><span class="text-gray-300">${p.Title}</span><span class="text-gray-500">${p.OldVersion} ➔ <span class="text-proxmox">${p.Version}</span></span></div>`; }); content.innerHTML += `<div class="bg-darkcard border border-darkborder rounded-xl p-5 mb-4 shadow-lg"><div class="flex justify-between items-center mb-4 border-b border-darkborder pb-2"><h3 class="text-white font-bold flex items-center gap-2"><svg class="w-5 h-5 text-proxmox" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"></path></svg> ${node.display_name} <span class="text-xs bg-red-500/20 text-red-400 px-2 py-0.5 rounded ml-2">${node.packages.length} Updates</span></h3><button onclick="triggerAptUpgrade(${node.node_id}, '${node.host}')" class="bg-proxmox hover:bg-orange-600 text-white font-bold py-1.5 px-4 rounded text-sm transition-colors">Installieren & Upgraden</button></div><div class="max-h-60 overflow-y-auto pr-2">${pkgsHtml}</div></div>`; }); } } catch(e) {} }
window.closeUpdateManager = function() { 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 = '<div class="col-span-full text-center text-gray-500 p-10 bg-darkcard rounded-lg border border-darkborder">Keine PBS Server gefunden.</div>'; return; } container.innerHTML = ''; res.data.forEach(ds => { const total = ds.total || 0; const used = ds.used || 0; const percent = total > 0 ? ((used / total) * 100).toFixed(1) : 0; const colorClass = percent > 85 ? 'bg-red-500' : (percent > 70 ? 'bg-orange-500' : 'bg-pbs'); container.innerHTML += `<div class="bg-darkcard border border-darkborder rounded-xl p-5 shadow-lg hover:border-pbs cursor-pointer transition-colors" onclick="openPbsDatastore(${ds.node_id}, '${ds.store}')"><div class="flex justify-between items-start mb-4"><div><h3 class="text-lg font-bold text-white">${ds.store}</h3><p class="text-xs text-gray-400">PBS Host: ${ds.host}</p></div><div class="p-2 bg-purple-500/10 rounded-lg"><svg class="w-6 h-6 text-pbs" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4"></path></svg></div></div><div class="mb-2 flex justify-between text-sm"><span class="text-gray-400">Auslastung</span><span class="text-white font-bold">${percent}%</span></div><div class="w-full bg-darkbg rounded-full h-2 mb-3"><div class="${colorClass} h-2 rounded-full transition-all duration-1000" style="width: ${percent}%"></div></div><div class="flex justify-between text-xs text-gray-500"><span>Used: ${formatBytes(used)}</span><span>Total: ${formatBytes(total)}</span></div></div>`; }); } } catch(e) {} }
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 = '<p class="text-gray-500 animate-pulse">Durchsuche Namespaces nach Backups...</p>'; try { const res = await (await fetch(`api.php?action=pbs_get_datastore_content&node_id=${nodeId}&store=${storeName}`)).json(); if(res.success && res.data) { if(res.data.length === 0) { container.innerHTML = '<p class="text-gray-500 text-sm">Keine Backups vorhanden.</p>'; return; } container.innerHTML = ''; res.data.sort((a,b) => b['backup-time'] - a['backup-time']).forEach(b => { const timeStr = formatDate(b['backup-time']); const sizeStr = formatBytes(b.size); const nsLabel = b.ns ? `<span class="text-pbs font-normal ml-2">[${b.ns}]</span>` : `<span class="text-gray-500 font-normal ml-2">[Root]</span>`; container.innerHTML += `<div class="flex justify-between items-center bg-darkbg p-2 border border-darkborder rounded mb-2 hover:border-pbs transition-colors"><div><p class="text-white text-xs font-bold">${b['backup-type']} / ${b['backup-id']} ${nsLabel}</p><p class="text-xs text-gray-500">${timeStr} | ${sizeStr}</p></div><button onclick="deletePbsSnapshot('${b['backup-type']}', '${b['backup-id']}', ${b['backup-time']}, '${b.ns || ''}')" class="text-red-500 hover:text-white text-xs font-bold px-3 py-1 bg-red-500/10 hover:bg-red-500 rounded transition-colors">Löschen</button></div>`; }); } } catch(e) { container.innerHTML = 'Fehler beim Laden.'; } }
window.deletePbsSnapshot = async function(btype, bid, btime, ns) { if(!confirm(`Backup ${btype}/${bid} wirklich unwiderruflich löschen?`)) return; const fd = new FormData(); fd.append('node_id', document.getElementById('pbsNodeId').value); fd.append('store', document.getElementById('pbsStoreName').value); fd.append('btype', btype); fd.append('bid', bid); fd.append('btime', btime); fd.append('ns', ns); const res = await (await fetch('api.php?action=pbs_delete_snapshot', {method: 'POST', body: fd})).json(); if(res.success) { loadPbsBackups(document.getElementById('pbsNodeId').value, document.getElementById('pbsStoreName').value); } else alert('Fehler beim Löschen.'); }
async function loadPbsJobs(nodeId, storeName) { const mixCont = document.getElementById('pbsVerifyGcContainer'); mixCont.innerHTML = '<p class="text-gray-500 animate-pulse">Lade System-Jobs...</p>'; const table = document.getElementById('pbsJobsTable'); table.innerHTML = '<tr><td colspan="5" class="text-center py-4 text-gray-500">Lade Historie...</td></tr>'; try { const res = await (await fetch(`api.php?action=pbs_get_jobs&node_id=${nodeId}`)).json(); if(res.success && res.data) { mixCont.innerHTML = ''; table.innerHTML = ''; let mixCount = 0; res.data.sort((a,b) => b.starttime - a.starttime).forEach(job => { const statusColor = job.status === 'OK' ? 'text-green-500' : (job.status ? 'text-red-500' : 'text-blue-400'); const timeStr = formatDate(job.starttime); const runtime = job.endtime ? (Math.round(job.endtime - job.starttime) + 's') : 'Running...'; const isSystemJob = job.worker_type === 'verify' || job.worker_type === 'garbage_collection' || job.worker_type === 'prune'; if (isSystemJob && mixCount < 6) { mixCont.innerHTML += `<div class="flex justify-between items-center bg-darkbg p-2 border border-darkborder rounded mb-1"><div><p class="text-white text-xs font-bold uppercase">${job.worker_type}</p><p class="text-xs text-gray-500">${timeStr}</p></div><div class="flex items-center gap-2"><span class="${statusColor} font-bold text-xs uppercase mr-2">${job.status || 'Active'}</span><button onclick="openTaskLog('${job.upid}', ${nodeId}, 'localhost')" class="text-gray-400 hover:text-white text-xs bg-darkcard px-2 py-1 rounded border border-darkborder transition-colors">📄 Log</button></div></div>`; mixCount++; } table.innerHTML += `<tr class="hover:bg-darkbg transition-colors border-b border-darkborder/50"><td class="px-4 py-2">${timeStr}</td><td class="px-4 py-2 font-medium text-white">${job.worker_type}</td><td class="px-4 py-2 ${statusColor} font-bold uppercase">${job.status || 'Active'}</td><td class="px-4 py-2">${runtime}</td><td class="px-4 py-2 text-right"><button onclick="openTaskLog('${job.upid}', ${nodeId}, 'localhost')" class="text-pbs hover:text-white font-bold text-xs transition-colors">Ansehen</button></td></tr>`; }); if(mixCount === 0) mixCont.innerHTML = '<p class="text-gray-500 text-xs italic">Keine System-Jobs gefunden.</p>'; if(res.data.length === 0) table.innerHTML = '<tr><td colspan="5" class="text-center py-4 text-gray-500">Keine Historie gefunden.</td></tr>'; } } catch(e) {} }
async function loadPbsSyncJobs(nodeId) { const container = document.getElementById('pbsSyncContainer'); container.innerHTML = '<p class="text-gray-500 animate-pulse">Lade Sync-Jobs...</p>'; try { const res = await (await fetch(`api.php?action=pbs_get_sync_jobs&node_id=${nodeId}`)).json(); if(res.success && res.data) { if(res.data.length === 0) { container.innerHTML = '<p class="text-gray-500 text-sm">Keine Sync-Jobs eingerichtet.</p>'; return; } container.innerHTML = ''; res.data.forEach(sync => { container.innerHTML += `<div class="bg-darkbg p-3 border border-darkborder rounded mb-2 hover:border-blue-500 transition-colors"><h4 class="text-white text-sm font-bold truncate">${sync.id}</h4><p class="text-xs text-gray-400 mt-1">Quelle: <span class="text-blue-400">${sync.remote || 'Lokal'} -> ${sync['remote-store']}</span></p><p class="text-xs text-gray-400">Ziel: <span class="text-emerald-400">${sync.store}</span></p><div class="mt-2 text-xs text-gray-500 bg-darkcard p-1.5 rounded inline-block">Zeitplan: ${sync.schedule || 'Manuell'}</div></div>`; }); } } catch(e) { container.innerHTML = 'Fehler.'; } }
window.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 = '<div class="col-span-full text-center text-gray-500 p-10 bg-darkcard rounded-lg border border-darkborder">Keine PBS Server gefunden.</div>'; return; } container.innerHTML = ''; res.data.forEach(ds => { const total = ds.total || 0; const used = ds.used || 0; const percent = total > 0 ? ((used / total) * 100).toFixed(1) : 0; const colorClass = percent > 85 ? 'bg-red-500' : (percent > 70 ? 'bg-orange-500' : 'bg-pbs'); container.innerHTML += `<div class="bg-darkcard border border-darkborder rounded-xl p-5 shadow-lg hover:border-pbs cursor-pointer transition-colors" onclick="openPbsDatastore(${ds.node_id}, '${ds.store}')"><div class="flex justify-between items-start mb-4"><div><h3 class="text-lg font-bold text-white">${ds.store}</h3><p class="text-xs text-gray-400">PBS Host: ${ds.host}</p></div><div class="p-2 bg-purple-500/10 rounded-lg"><svg class="w-6 h-6 text-pbs" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4"></path></svg></div></div><div class="mb-2 flex justify-between text-sm"><span class="text-gray-400">Auslastung</span><span class="text-white font-bold">${percent}%</span></div><div class="w-full bg-darkbg rounded-full h-2 mb-3"><div class="${colorClass} h-2 rounded-full transition-all duration-1000" style="width: ${percent}%"></div></div><div class="flex justify-between text-xs text-gray-500"><span>Used: ${formatBytes(used)}</span><span>Total: ${formatBytes(total)}</span></div></div>`; }); } } catch(e) {} }
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 = '<p class="text-gray-500 animate-pulse">Durchsuche Namespaces nach Backups...</p>'; try { const res = await (await fetch(`api.php?action=pbs_get_datastore_content&node_id=${nodeId}&store=${storeName}`)).json(); if(res.success && res.data) { if(res.data.length === 0) { container.innerHTML = '<p class="text-gray-500 text-sm">Keine Backups vorhanden.</p>'; return; } container.innerHTML = ''; res.data.sort((a,b) => b['backup-time'] - a['backup-time']).forEach(b => { const timeStr = formatDate(b['backup-time']); const sizeStr = formatBytes(b.size); const nsLabel = b.ns ? `<span class="text-pbs font-normal ml-2">[${b.ns}]</span>` : `<span class="text-gray-500 font-normal ml-2">[Root]</span>`; container.innerHTML += `<div class="flex justify-between items-center bg-darkbg p-2 border border-darkborder rounded mb-2 hover:border-pbs transition-colors"><div><p class="text-white text-xs font-bold">${b['backup-type']} / ${b['backup-id']} ${nsLabel}</p><p class="text-xs text-gray-500">${timeStr} | ${sizeStr}</p></div><button onclick="deletePbsSnapshot('${b['backup-type']}', '${b['backup-id']}', ${b['backup-time']}, '${b.ns || ''}')" class="text-red-500 hover:text-white text-xs font-bold px-3 py-1 bg-red-500/10 hover:bg-red-500 rounded transition-colors">Löschen</button></div>`; }); } } catch(e) { container.innerHTML = 'Fehler beim Laden.'; } }
window.deletePbsSnapshot = async function(btype, bid, btime, ns) { if(!confirm(`Backup ${btype}/${bid} wirklich 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 = '<p class="text-gray-500 animate-pulse">Lade System-Jobs...</p>'; const table = document.getElementById('pbsJobsTable'); table.innerHTML = '<tr><td colspan="5" class="text-center py-4 text-gray-500">Lade Historie...</td></tr>'; try { const res = await (await fetch(`api.php?action=pbs_get_jobs&node_id=${nodeId}`)).json(); if(res.success && res.data) { mixCont.innerHTML = ''; table.innerHTML = ''; let mixCount = 0; res.data.sort((a,b) => b.starttime - a.starttime).forEach(job => { const statusColor = job.status === 'OK' ? 'text-green-500' : (job.status ? 'text-red-500' : 'text-blue-400'); const timeStr = formatDate(job.starttime); const runtime = job.endtime ? (Math.round(job.endtime - job.starttime) + 's') : 'Running...'; const isSystemJob = job.worker_type === 'verify' || job.worker_type === 'garbage_collection' || job.worker_type === 'prune'; if (isSystemJob && mixCount < 6) { mixCont.innerHTML += `<div class="flex justify-between items-center bg-darkbg p-2 border border-darkborder rounded mb-1"><div><p class="text-white text-xs font-bold uppercase">${job.worker_type}</p><p class="text-xs text-gray-500">${timeStr}</p></div><div class="flex items-center gap-2"><span class="${statusColor} font-bold text-xs uppercase mr-2">${job.status || 'Active'}</span><button onclick="openTaskLog('${job.upid}', ${nodeId}, 'localhost')" class="text-gray-400 hover:text-white text-xs bg-darkcard px-2 py-1 rounded border border-darkborder transition-colors">📄 Log</button></div></div>`; mixCount++; } table.innerHTML += `<tr class="hover:bg-darkbg transition-colors border-b border-darkborder/50"><td class="px-4 py-2">${timeStr}</td><td class="px-4 py-2 font-medium text-white">${job.worker_type}</td><td class="px-4 py-2 ${statusColor} font-bold uppercase">${job.status || 'Active'}</td><td class="px-4 py-2">${runtime}</td><td class="px-4 py-2 text-right"><button onclick="openTaskLog('${job.upid}', ${nodeId}, 'localhost')" class="text-pbs hover:text-white font-bold text-xs transition-colors">Ansehen</button></td></tr>`; }); if(mixCount === 0) mixCont.innerHTML = '<p class="text-gray-500 text-xs italic">Keine System-Jobs gefunden.</p>'; if(res.data.length === 0) table.innerHTML = '<tr><td colspan="5" class="text-center py-4 text-gray-500">Keine Historie gefunden.</td></tr>'; } } catch(e) {} }
async function loadPbsSyncJobs(nodeId) { const container = document.getElementById('pbsSyncContainer'); if(!container) return; container.innerHTML = '<p class="text-gray-500 animate-pulse">Lade Sync-Jobs...</p>'; try { const res = await (await fetch(`api.php?action=pbs_get_sync_jobs&node_id=${nodeId}`)).json(); if(res.success && res.data) { if(res.data.length === 0) { container.innerHTML = '<p class="text-gray-500 text-sm">Keine Sync-Jobs eingerichtet.</p>'; return; } container.innerHTML = ''; res.data.forEach(sync => { container.innerHTML += `<div class="bg-darkbg p-3 border border-darkborder rounded mb-2 hover:border-blue-500 transition-colors"><h4 class="text-white text-sm font-bold truncate">${sync.id}</h4><p class="text-xs text-gray-400 mt-1">Quelle: <span class="text-blue-400">${sync.remote || 'Lokal'} -> ${sync['remote-store']}</span></p><p class="text-xs text-gray-400">Ziel: <span class="text-emerald-400">${sync.store}</span></p><div class="mt-2 text-xs text-gray-500 bg-darkcard p-1.5 rounded inline-block">Zeitplan: ${sync.schedule || 'Manuell'}</div></div>`; }); } } catch(e) { container.innerHTML = 'Fehler.'; } }
// --- 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 = '<div class="col-span-full text-center text-gray-500 p-10 bg-darkcard rounded-lg border border-darkborder">Keine Mail Gateways angebunden.</div>'; return; } container.innerHTML = ''; res.data.forEach(pmg => { const cpuPercent = ((pmg.cpu || 0) * 100).toFixed(1); const ramPercent = pmg.memory && pmg.memory.total ? ((pmg.memory.used / pmg.memory.total) * 100).toFixed(1) : 0; const diskPercent = pmg.rootfs && pmg.rootfs.total ? ((pmg.rootfs.used / pmg.rootfs.total) * 100).toFixed(1) : 0; container.innerHTML += `<div class="bg-darkcard border border-darkborder rounded-xl p-5 shadow-lg hover:border-pmg cursor-pointer transition-colors" onclick="openPmgManager(${pmg.node_id}, '${pmg.host}', '${pmg.internal_name}')"><div class="flex justify-between items-start mb-4"><div><h3 class="text-lg font-bold text-white">${pmg.host}</h3><p class="text-xs text-green-400">Online</p></div><div class="p-2 bg-blue-500/10 rounded-lg"><svg class="w-6 h-6 text-pmg" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path></svg></div></div><div class="mb-1 flex justify-between text-xs"><span class="text-gray-400">CPU</span><span class="text-white font-bold">${cpuPercent}%</span></div><div class="w-full bg-darkbg rounded-full h-1.5 mb-3"><div class="bg-blue-500 h-1.5 rounded-full" style="width: ${cpuPercent}%"></div></div><div class="mb-1 flex justify-between text-xs"><span class="text-gray-400">RAM</span><span class="text-white font-bold">${ramPercent}%</span></div><div class="w-full bg-darkbg rounded-full h-1.5 mb-3"><div class="bg-proxmox h-1.5 rounded-full" style="width: ${ramPercent}%"></div></div><div class="mb-1 flex justify-between text-xs"><span class="text-gray-400">System Disk</span><span class="text-white font-bold">${diskPercent}%</span></div><div class="w-full bg-darkbg rounded-full h-1.5"><div class="bg-emerald-500 h-1.5 rounded-full" style="width: ${diskPercent}%"></div></div></div>`; }); } } catch(e) {} }
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 = '<div class="col-span-full text-center text-gray-500 p-10 bg-darkcard rounded-lg border border-darkborder">Keine Mail Gateways angebunden.</div>'; return; } container.innerHTML = ''; res.data.forEach(pmg => { const cpuPercent = ((pmg.cpu || 0) * 100).toFixed(1); const ramPercent = pmg.memory && pmg.memory.total ? ((pmg.memory.used / pmg.memory.total) * 100).toFixed(1) : 0; const diskPercent = pmg.rootfs && pmg.rootfs.total ? ((pmg.rootfs.used / pmg.rootfs.total) * 100).toFixed(1) : 0; container.innerHTML += `<div class="bg-darkcard border border-darkborder rounded-xl p-5 shadow-lg hover:border-pmg cursor-pointer transition-colors" onclick="openPmgManager(${pmg.node_id}, '${pmg.host}', '${pmg.internal_name}')"><div class="flex justify-between items-start mb-4"><div><h3 class="text-lg font-bold text-white">${pmg.host}</h3><p class="text-xs text-green-400">Online</p></div><div class="p-2 bg-blue-500/10 rounded-lg"><svg class="w-6 h-6 text-pmg" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path></svg></div></div><div class="mb-1 flex justify-between text-xs"><span class="text-gray-400">CPU</span><span class="text-white font-bold">${cpuPercent}%</span></div><div class="w-full bg-darkbg rounded-full h-1.5 mb-3"><div class="bg-blue-500 h-1.5 rounded-full" style="width: ${cpuPercent}%"></div></div><div class="mb-1 flex justify-between text-xs"><span class="text-gray-400">RAM</span><span class="text-white font-bold">${ramPercent}%</span></div><div class="w-full bg-darkbg rounded-full h-1.5 mb-3"><div class="bg-proxmox h-1.5 rounded-full" style="width: ${ramPercent}%"></div></div><div class="mb-1 flex justify-between text-xs"><span class="text-gray-400">System Disk</span><span class="text-white font-bold">${diskPercent}%</span></div><div class="w-full bg-darkbg rounded-full h-1.5"><div class="bg-emerald-500 h-1.5 rounded-full" style="width: ${diskPercent}%"></div></div></div>`; }); } } catch(e) {} }
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 = '<tr><td colspan="6" class="text-center py-6 text-gray-500 animate-pulse">Lade Spam Quarantäne...</td></tr>'; try { const res = await (await fetch(`api.php?action=pmg_get_spam_queue&node_id=${nodeId}`)).json(); if(res.success && res.data) { table.innerHTML = ''; if(res.data.length === 0) { table.innerHTML = '<tr><td colspan="6" class="text-center py-6 text-gray-500">🎉 Keine Mails in der Quarantäne.</td></tr>'; return; } res.data.sort((a,b) => b.time - a.time).forEach(mail => { const timeStr = formatDate(mail.time); const subject = mail.subject || 'Kein Betreff'; const sender = mail.sender || 'Unbekannt'; const receiver = mail.receiver || 'Unbekannt'; const score = mail.spamlevel || 0; table.innerHTML += `<tr class="hover:bg-darkcard/50 transition-colors border-b border-darkborder/50"><td class="px-4 py-3 whitespace-nowrap">${timeStr}</td><td class="px-4 py-3 text-white truncate max-w-[200px]" title="${sender}">${sender}</td><td class="px-4 py-3 text-gray-300 truncate max-w-[200px]" title="${receiver}">${receiver}</td><td class="px-4 py-3 truncate max-w-[250px]" title="${subject}">${subject}</td><td class="px-4 py-3 text-red-400 font-bold text-center">${score}</td><td class="px-4 py-3 text-right whitespace-nowrap"><button onclick="pmgSpamAction('deliver', '${mail.id}')" class="text-green-500 hover:text-white px-2 py-1.5 bg-green-500/10 hover:bg-green-500 rounded text-xs font-bold transition-colors mr-2">Deliver</button><button onclick="pmgSpamAction('delete', '${mail.id}')" class="text-red-500 hover:text-white px-2 py-1.5 bg-red-500/10 hover:bg-red-500 rounded text-xs font-bold transition-colors">Delete</button></td></tr>`; }); } else { table.innerHTML = '<tr><td colspan="6" class="text-center py-6 text-red-500">Fehler beim Laden.</td></tr>'; } } catch(e) { table.innerHTML = '<tr><td colspan="6" class="text-center py-6 text-red-500">Netzwerkfehler.</td></tr>'; } }
window.loadPmgSpamQueue = async function(nodeId) { const table = document.getElementById('pmgSpamTable'); if(!table) return; table.innerHTML = '<tr><td colspan="6" class="text-center py-6 text-gray-500 animate-pulse">Lade Spam Quarantäne...</td></tr>'; try { const res = await (await fetch(`api.php?action=pmg_get_spam_queue&node_id=${nodeId}`)).json(); if(res.success && res.data) { table.innerHTML = ''; if(res.data.length === 0) { table.innerHTML = '<tr><td colspan="6" class="text-center py-6 text-gray-500">🎉 Keine Mails in der Quarantäne.</td></tr>'; return; } res.data.sort((a,b) => b.time - a.time).forEach(mail => { const timeStr = formatDate(mail.time); const subject = mail.subject || 'Kein Betreff'; const sender = mail.sender || 'Unbekannt'; const receiver = mail.receiver || 'Unbekannt'; const score = mail.spamlevel || 0; table.innerHTML += `<tr class="hover:bg-darkcard/50 transition-colors border-b border-darkborder/50"><td class="px-4 py-3 whitespace-nowrap">${timeStr}</td><td class="px-4 py-3 text-white truncate max-w-[200px]" title="${sender}">${sender}</td><td class="px-4 py-3 text-gray-300 truncate max-w-[200px]" title="${receiver}">${receiver}</td><td class="px-4 py-3 truncate max-w-[250px]" title="${subject}">${subject}</td><td class="px-4 py-3 text-red-400 font-bold text-center">${score}</td><td class="px-4 py-3 text-right whitespace-nowrap"><button onclick="pmgSpamAction('deliver', '${mail.id}')" class="text-green-500 hover:text-white px-2 py-1.5 bg-green-500/10 hover:bg-green-500 rounded text-xs font-bold transition-colors mr-2">Deliver</button><button onclick="pmgSpamAction('delete', '${mail.id}')" class="text-red-500 hover:text-white px-2 py-1.5 bg-red-500/10 hover:bg-red-500 rounded text-xs font-bold transition-colors">Delete</button></td></tr>`; }); } else { table.innerHTML = '<tr><td colspan="6" class="text-center py-6 text-red-500">Fehler beim Laden.</td></tr>'; } } catch(e) { table.innerHTML = '<tr><td colspan="6" class="text-center py-6 text-red-500">Netzwerkfehler.</td></tr>'; } }
window.pmgSpamAction = async function(actionType, mailId) { if(actionType === 'delete' && !confirm('Mail unwiderruflich löschen?')) return; if(actionType === 'deliver' && !confirm('Soll diese Mail 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 = '<tr><td colspan="4" class="text-center py-6 text-gray-500 animate-pulse">Lese Syslog (Letzte 24h)...</td></tr>'; try { const res = await (await fetch(`api.php?action=pmg_get_tracking&node_id=${nodeId}&internal_name=${internalName}&filter=${encodeURIComponent(filter)}`)).json(); if(res.success && res.data) { table.innerHTML = ''; if(res.data.length === 0) { table.innerHTML = `<tr><td colspan="4" class="text-center py-6 text-gray-500">Keine Logs für diesen Filter gefunden.</td></tr>`; return; } res.data.forEach(log => { const timestamp = log.time || log.timestamp || 0; const timeStr = timestamp ? formatDate(timestamp) : 'Unbekannt'; const sender = log.from || log.sender || '-'; const receiver = log.to || log.receiver || '-'; let statusTxt = 'Unbekannt'; let statusColor = 'text-gray-400'; if (log.dstatus) { if (log.dstatus === 'A') { statusTxt = 'Delivered / Accept'; statusColor = 'text-green-500'; } else if (log.dstatus === 'N') { statusTxt = 'Rejected / Blocked'; statusColor = 'text-red-500'; } else if (log.dstatus === 'Q') { statusTxt = 'Quarantined'; statusColor = 'text-orange-500'; } else if (log.dstatus === 'B') { statusTxt = 'Bounced'; statusColor = 'text-red-500'; } else { statusTxt = log.dstatus; statusColor = 'text-blue-400'; } } else if (log.relay) { statusTxt = 'Relayed (' + log.relay.split('[')[0] + ')'; statusColor = 'text-blue-400'; } else if (log.msgid) { statusTxt = 'In Processing (' + log.msgid + ')'; statusColor = 'text-gray-400'; } else if (log.client) { statusTxt = 'Connection from ' + log.client.split('[')[0]; statusColor = 'text-gray-500'; } else if (log.id && log.id.length > 5) { statusTxt = 'Queue ID: ' + log.id; statusColor = 'text-gray-400'; } else { statusTxt = JSON.stringify(log).substring(0, 30); } table.innerHTML += `<tr class="hover:bg-darkcard/50 transition-colors border-b border-darkborder/50"><td class="px-4 py-3 whitespace-nowrap text-gray-400">${timeStr}</td><td class="px-4 py-3 text-white truncate max-w-[200px]">${sender}</td><td class="px-4 py-3 text-gray-300 truncate max-w-[200px]">${receiver}</td><td class="px-4 py-3 font-bold uppercase ${statusColor} truncate max-w-[300px]" title="${statusTxt}">${statusTxt}</td></tr>`; }); } else { table.innerHTML = '<tr><td colspan="4" class="text-center py-6 text-red-500">Syslog konnte nicht gelesen werden.</td></tr>'; } } catch(e) { table.innerHTML = '<tr><td colspan="4" class="text-center py-6 text-red-500">Netzwerkfehler.</td></tr>'; } }
window.loadPmgQueues = async function() { const nodeId = document.getElementById('pmgNodeId').value; const internalName = document.getElementById('pmgInternalName').value; const table = document.getElementById('pmgQueueTable'); const summary = document.getElementById('pmgQueuesSummary'); table.innerHTML = '<tr><td colspan="5" class="text-center py-6 text-gray-500 animate-pulse">Lese Warteschlangen aus Postfix...</td></tr>'; summary.innerHTML = '<div class="col-span-3 text-center text-gray-500 animate-pulse">Lade...</div>'; try { const res = await (await fetch(`api.php?action=pmg_get_queues&node_id=${nodeId}&internal_name=${internalName}`)).json(); if(res.success && res.data) { let activeCount = 0, deferCount = 0, holdCount = 0; res.data.forEach(q => { const queueName = q.queue_name || q.queue || ''; if(queueName === 'active') activeCount++; else if(queueName === 'deferred') deferCount++; else holdCount++; }); summary.innerHTML = `<div class="bg-darkcard border ${activeCount > 0 ? 'border-blue-500/50 shadow-[0_0_15px_rgba(59,130,246,0.1)]' : 'border-darkborder'} rounded-xl p-5 text-center transition-all"><p class="text-gray-400 text-sm font-bold uppercase mb-1">Active Queue</p><h4 class="text-3xl font-bold ${activeCount > 0 ? 'text-blue-500' : 'text-white'}">${activeCount}</h4></div><div class="bg-darkcard border ${deferCount > 0 ? 'border-orange-500/50 shadow-[0_0_15px_rgba(249,115,22,0.1)]' : 'border-darkborder'} rounded-xl p-5 text-center transition-all"><p class="text-gray-400 text-sm font-bold uppercase mb-1">Deferred (Greylisted/Wartend)</p><h4 class="text-3xl font-bold ${deferCount > 0 ? 'text-orange-500' : 'text-white'}">${deferCount}</h4></div><div class="bg-darkcard border ${holdCount > 0 ? 'border-red-500/50 shadow-[0_0_15px_rgba(239,68,68,0.1)]' : 'border-darkborder'} rounded-xl p-5 text-center"><p class="text-gray-400 text-sm font-bold uppercase mb-1">Other / Hold</p><h4 class="text-3xl font-bold ${holdCount > 0 ? 'text-red-500' : 'text-white'}">${holdCount}</h4></div>`; table.innerHTML = ''; if(res.data.length === 0) { table.innerHTML = `<tr><td colspan="5" class="text-center py-6 text-gray-500">Alle Postfix-Queues sind komplett leer.</td></tr>`; return; } res.data.forEach(q => { const queueName = q.queue_name || q.queue || 'unknown'; const timestamp = q.arrival_time || q.time || 0; const timeStr = timestamp ? formatDate(timestamp) : 'Unbekannt'; const sender = q.sender || '-'; let receiver = q.receiver || q.recipients || '-'; if (Array.isArray(receiver)) receiver = receiver[0]; const reason = q.reason || q.error || '-'; const qColor = queueName === 'deferred' ? 'text-orange-400' : (queueName === 'active' ? 'text-blue-400' : 'text-gray-400'); table.innerHTML += `<tr class="hover:bg-darkcard/50 transition-colors border-b border-darkborder/50"><td class="px-4 py-3 font-bold uppercase ${qColor}">${queueName}</td><td class="px-4 py-3 whitespace-nowrap text-gray-400">${timeStr}</td><td class="px-4 py-3 text-white truncate max-w-[200px]" title="${sender}">${sender}</td><td class="px-4 py-3 text-gray-300 truncate max-w-[200px]" title="${receiver}">${receiver}</td><td class="px-4 py-3 text-xs text-gray-500 truncate max-w-[300px]" title="${reason}">${reason}</td></tr>`; }); } else { table.innerHTML = '<tr><td colspan="5" class="text-center py-6 text-red-500">Queues konnten nicht gelesen werden.</td></tr>'; } } catch(e) { table.innerHTML = '<tr><td colspan="5" class="text-center py-6 text-red-500">Netzwerkfehler.</td></tr>'; } }
window.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 = '<tr><td colspan="4" class="text-center py-6 text-gray-500 animate-pulse">Lese Syslog (Letzte 24h)...</td></tr>'; try { const res = await (await fetch(`api.php?action=pmg_get_tracking&node_id=${nodeId}&internal_name=${internalName}&filter=${encodeURIComponent(filter)}`)).json(); if(res.success && res.data) { table.innerHTML = ''; if(res.data.length === 0) { table.innerHTML = `<tr><td colspan="4" class="text-center py-6 text-gray-500">Keine Logs für diesen Filter gefunden.</td></tr>`; return; } res.data.forEach(log => { const timestamp = log.time || log.timestamp || 0; const timeStr = timestamp ? formatDate(timestamp) : 'Unbekannt'; const sender = log.from || log.sender || '-'; const receiver = log.to || log.receiver || '-'; let statusTxt = 'Unbekannt'; let statusColor = 'text-gray-400'; if (log.dstatus) { if (log.dstatus === 'A') { statusTxt = 'Delivered / Accept'; statusColor = 'text-green-500'; } else if (log.dstatus === 'N') { statusTxt = 'Rejected / Blocked'; statusColor = 'text-red-500'; } else if (log.dstatus === 'Q') { statusTxt = 'Quarantined'; statusColor = 'text-orange-500'; } else if (log.dstatus === 'B') { statusTxt = 'Bounced'; statusColor = 'text-red-500'; } else { statusTxt = log.dstatus; statusColor = 'text-blue-400'; } } else if (log.relay) { statusTxt = 'Relayed (' + log.relay.split('[')[0] + ')'; statusColor = 'text-blue-400'; } else if (log.msgid) { statusTxt = 'In Processing (' + log.msgid + ')'; statusColor = 'text-gray-400'; } else if (log.client) { statusTxt = 'Connection from ' + log.client.split('[')[0]; statusColor = 'text-gray-500'; } else if (log.id && log.id.length > 5) { statusTxt = 'Queue ID: ' + log.id; statusColor = 'text-gray-400'; } else { statusTxt = JSON.stringify(log).substring(0, 30); } table.innerHTML += `<tr class="hover:bg-darkcard/50 transition-colors border-b border-darkborder/50"><td class="px-4 py-3 whitespace-nowrap text-gray-400">${timeStr}</td><td class="px-4 py-3 text-white truncate max-w-[200px]">${sender}</td><td class="px-4 py-3 text-gray-300 truncate max-w-[200px]">${receiver}</td><td class="px-4 py-3 font-bold uppercase ${statusColor} truncate max-w-[300px]" title="${statusTxt}">${statusTxt}</td></tr>`; }); } else { table.innerHTML = '<tr><td colspan="4" class="text-center py-6 text-red-500">Syslog konnte nicht gelesen werden.</td></tr>'; } } catch(e) { table.innerHTML = '<tr><td colspan="4" class="text-center py-6 text-red-500">Netzwerkfehler.</td></tr>'; } }
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 = '<tr><td colspan="5" class="text-center py-6 text-gray-500 animate-pulse">Lese Warteschlangen aus Postfix...</td></tr>'; summary.innerHTML = '<div class="col-span-3 text-center text-gray-500 animate-pulse">Lade...</div>'; try { const res = await (await fetch(`api.php?action=pmg_get_queues&node_id=${nodeId}&internal_name=${internalName}`)).json(); if(res.success && res.data) { let activeCount = 0, deferCount = 0, holdCount = 0; res.data.forEach(q => { const queueName = q.queue_name || q.queue || ''; if(queueName === 'active') activeCount++; else if(queueName === 'deferred') deferCount++; else holdCount++; }); summary.innerHTML = `<div class="bg-darkcard border ${activeCount > 0 ? 'border-blue-500/50 shadow-[0_0_15px_rgba(59,130,246,0.1)]' : 'border-darkborder'} rounded-xl p-5 text-center transition-all"><p class="text-gray-400 text-sm font-bold uppercase mb-1">Active Queue</p><h4 class="text-3xl font-bold ${activeCount > 0 ? 'text-blue-500' : 'text-white'}">${activeCount}</h4></div><div class="bg-darkcard border ${deferCount > 0 ? 'border-orange-500/50 shadow-[0_0_15px_rgba(249,115,22,0.1)]' : 'border-darkborder'} rounded-xl p-5 text-center transition-all"><p class="text-gray-400 text-sm font-bold uppercase mb-1">Deferred (Greylisted/Wartend)</p><h4 class="text-3xl font-bold ${deferCount > 0 ? 'text-orange-500' : 'text-white'}">${deferCount}</h4></div><div class="bg-darkcard border ${holdCount > 0 ? 'border-red-500/50 shadow-[0_0_15px_rgba(239,68,68,0.1)]' : 'border-darkborder'} rounded-xl p-5 text-center"><p class="text-gray-400 text-sm font-bold uppercase mb-1">Other / Hold</p><h4 class="text-3xl font-bold ${holdCount > 0 ? 'text-red-500' : 'text-white'}">${holdCount}</h4></div>`; table.innerHTML = ''; if(res.data.length === 0) { table.innerHTML = `<tr><td colspan="5" class="text-center py-6 text-gray-500">Alle Postfix-Queues sind komplett leer.</td></tr>`; return; } res.data.forEach(q => { const queueName = q.queue_name || q.queue || 'unknown'; const timestamp = q.arrival_time || q.time || 0; const timeStr = timestamp ? formatDate(timestamp) : 'Unbekannt'; const sender = q.sender || '-'; let receiver = q.receiver || q.recipients || '-'; if (Array.isArray(receiver)) receiver = receiver[0]; const reason = q.reason || q.error || '-'; const qColor = queueName === 'deferred' ? 'text-orange-400' : (queueName === 'active' ? 'text-blue-400' : 'text-gray-400'); table.innerHTML += `<tr class="hover:bg-darkcard/50 transition-colors border-b border-darkborder/50"><td class="px-4 py-3 font-bold uppercase ${qColor}">${queueName}</td><td class="px-4 py-3 whitespace-nowrap text-gray-400">${timeStr}</td><td class="px-4 py-3 text-white truncate max-w-[200px]" title="${sender}">${sender}</td><td class="px-4 py-3 text-gray-300 truncate max-w-[200px]" title="${receiver}">${receiver}</td><td class="px-4 py-3 text-xs text-gray-500 truncate max-w-[300px]" title="${reason}">${reason}</td></tr>`; }); } else { table.innerHTML = '<tr><td colspan="5" class="text-center py-6 text-red-500">Queues konnten nicht gelesen werden.</td></tr>'; } } catch(e) { table.innerHTML = '<tr><td colspan="5" class="text-center py-6 text-red-500">Netzwerkfehler.</td></tr>'; } }
window.loadPmgConfig = async function(nodeId) { try { const res = await (await fetch(`api.php?action=pmg_get_config&node_id=${nodeId}`)).json(); if(res.success && res.data) { document.getElementById('pmgCfgRelay').value = res.data.relay || ''; document.getElementById('pmgCfgExtPort').value = res.data.ext_port || 25; document.getElementById('pmgCfgIntPort').value = res.data.int_port || 26; document.getElementById('pmgCfgTls').value = res.data.tls || 0; document.getElementById('pmgCfgTlsLog').value = res.data.tlslog || 0; } } catch(e) {} }
window.savePmgConfig = async function() { const btn = event.target; const oTxt = btn.innerText; btn.innerText = 'Speichere...'; const fd = new FormData(); fd.append('node_id', document.getElementById('pmgNodeId').value); fd.append('relay', document.getElementById('pmgCfgRelay').value); fd.append('ext_port', document.getElementById('pmgCfgExtPort').value); fd.append('int_port', document.getElementById('pmgCfgIntPort').value); fd.append('tls', document.getElementById('pmgCfgTls').value); fd.append('tlslog', document.getElementById('pmgCfgTlsLog').value); try { const res = await (await fetch('api.php?action=pmg_set_config', {method: 'POST', body: fd})).json(); if(res.success) { alert('Proxy Einstellungen gespeichert!'); } else { alert('Fehler beim Speichern.'); } } catch(e) { alert('Netzwerkfehler.'); } finally { btn.innerText = oTxt; } }
window.loadPmgDomains = async function(nodeId) { const container = document.getElementById('pmgDomainsContainer'); container.innerHTML = '<p class="text-gray-500 text-sm animate-pulse">Lade...</p>'; try { const res = await (await fetch(`api.php?action=pmg_get_domains&node_id=${nodeId}`)).json(); if(res.success && res.data) { if(res.data.length === 0) { container.innerHTML = '<p class="text-gray-500 text-sm">Keine Domains.</p>'; return; } container.innerHTML = ''; res.data.forEach(d => { container.innerHTML += `<div class="flex justify-between items-center bg-darkbg p-2 border border-darkborder rounded mb-1"><span class="text-white text-sm font-bold">${d.domain}</span><button onclick="deletePmgDomain('${d.domain}')" class="text-red-500 hover:text-red-400 text-xs font-bold">Löschen</button></div>`; }); } else { container.innerHTML = '<p class="text-red-500 text-sm">Fehler.</p>'; } } catch(e) {} }
window.loadPmgDomains = async function(nodeId) { const container = document.getElementById('pmgDomainsContainer'); if(!container) return; container.innerHTML = '<p class="text-gray-500 text-sm animate-pulse">Lade...</p>'; try { const res = await (await fetch(`api.php?action=pmg_get_domains&node_id=${nodeId}`)).json(); if(res.success && res.data) { if(res.data.length === 0) { container.innerHTML = '<p class="text-gray-500 text-sm">Keine Domains.</p>'; return; } container.innerHTML = ''; res.data.forEach(d => { container.innerHTML += `<div class="flex justify-between items-center bg-darkbg p-2 border border-darkborder rounded mb-1"><span class="text-white text-sm font-bold">${d.domain}</span><button onclick="deletePmgDomain('${d.domain}')" class="text-red-500 hover:text-red-400 text-xs font-bold">Löschen</button></div>`; }); } else { container.innerHTML = '<p class="text-red-500 text-sm">Fehler.</p>'; } } catch(e) {} }
window.addPmgDomain = async function() { const domain = document.getElementById('pmgNewDomain').value.trim(); if(!domain) return; const fd = new FormData(); fd.append('node_id', document.getElementById('pmgNodeId').value); fd.append('domain', domain); const res = await (await fetch('api.php?action=pmg_add_domain', {method: 'POST', body: fd})).json(); if(res.success) { document.getElementById('pmgNewDomain').value = ''; loadPmgDomains(document.getElementById('pmgNodeId').value); } else alert('Fehler.'); }
window.deletePmgDomain = async function(domain) { if(!confirm(`Domain ${domain} löschen?`)) return; const fd = new FormData(); fd.append('node_id', document.getElementById('pmgNodeId').value); fd.append('domain', domain); const res = await (await fetch('api.php?action=pmg_delete_domain', {method: 'POST', body: fd})).json(); if(res.success) loadPmgDomains(document.getElementById('pmgNodeId').value); else alert('Fehler.'); }
window.loadPmgNetworks = async function(nodeId) { const container = document.getElementById('pmgNetworksContainer'); container.innerHTML = '<p class="text-gray-500 text-sm animate-pulse">Lade...</p>'; try { const res = await (await fetch(`api.php?action=pmg_get_networks&node_id=${nodeId}`)).json(); if(res.success && res.data) { if(res.data.length === 0) { container.innerHTML = '<p class="text-gray-500 text-sm">Keine Netzwerke.</p>'; return; } container.innerHTML = ''; res.data.forEach(n => { container.innerHTML += `<div class="flex justify-between items-center bg-darkbg p-2 border border-darkborder rounded mb-1"><span class="text-white text-sm font-bold">${n.cidr}</span><button onclick="deletePmgNetwork('${n.cidr}')" class="text-red-500 hover:text-red-400 text-xs font-bold">Löschen</button></div>`; }); } else { container.innerHTML = '<p class="text-red-500 text-sm">Fehler.</p>'; } } catch(e) {} }
window.loadPmgNetworks = async function(nodeId) { const container = document.getElementById('pmgNetworksContainer'); if(!container) return; container.innerHTML = '<p class="text-gray-500 text-sm animate-pulse">Lade...</p>'; try { const res = await (await fetch(`api.php?action=pmg_get_networks&node_id=${nodeId}`)).json(); if(res.success && res.data) { if(res.data.length === 0) { container.innerHTML = '<p class="text-gray-500 text-sm">Keine Netzwerke.</p>'; return; } container.innerHTML = ''; res.data.forEach(n => { container.innerHTML += `<div class="flex justify-between items-center bg-darkbg p-2 border border-darkborder rounded mb-1"><span class="text-white text-sm font-bold">${n.cidr}</span><button onclick="deletePmgNetwork('${n.cidr}')" class="text-red-500 hover:text-red-400 text-xs font-bold">Löschen</button></div>`; }); } else { container.innerHTML = '<p class="text-red-500 text-sm">Fehler.</p>'; } } catch(e) {} }
window.addPmgNetwork = async function() { const cidr = document.getElementById('pmgNewNetwork').value.trim(); if(!cidr) return; const fd = new FormData(); fd.append('node_id', document.getElementById('pmgNodeId').value); fd.append('cidr', cidr); const res = await (await fetch('api.php?action=pmg_add_network', {method: 'POST', body: fd})).json(); if(res.success) { document.getElementById('pmgNewNetwork').value = ''; loadPmgNetworks(document.getElementById('pmgNodeId').value); } else alert('Fehler.'); }
window.deletePmgNetwork = async function(cidr) { if(!confirm(`Netzwerk ${cidr} löschen?`)) return; const fd = new FormData(); fd.append('node_id', document.getElementById('pmgNodeId').value); fd.append('cidr', cidr); const res = await (await fetch('api.php?action=pmg_delete_network', {method: 'POST', body: fd})).json(); if(res.success) loadPmgNetworks(document.getElementById('pmgNodeId').value); else alert('Fehler.'); }
window.loadPmgSsl = async function(nodeId) { const internalName = document.getElementById('pmgInternalName').value; const container = document.getElementById('pmgSslContainer'); container.innerHTML = '<p class="text-gray-500 animate-pulse text-sm">Lade Zertifikate...</p>'; try { const res = await (await fetch(`api.php?action=pmg_get_ssl&node_id=${nodeId}&internal_name=${internalName}`)).json(); if(res.success && res.data) { container.innerHTML = ''; res.data.forEach(cert => { const validUntil = cert.notafter ? formatDate(cert.notafter) : 'Unbekannt'; const isExpired = cert.notafter && cert.notafter < (Date.now() / 1000); const statusDot = isExpired ? '<span class="w-2 h-2 rounded-full bg-red-500 shrink-0"></span>' : '<span class="w-2 h-2 rounded-full bg-green-500 shrink-0"></span>'; container.innerHTML += `<div class="bg-darkbg border border-darkborder rounded p-3 text-sm flex gap-3"><div class="mt-1">${statusDot}</div><div class="flex-1 overflow-hidden"><h4 class="text-white font-bold mb-1 break-all">${cert.filename || cert.subject || 'Zertifikat'}</h4><p class="text-gray-400 text-xs mb-1"><span class="font-bold text-gray-500">Aussteller:</span> ${cert.issuer || '-'}</p><p class="text-gray-400 text-xs truncate" title="${cert.subject}"><span class="font-bold text-gray-500">Subject:</span> ${cert.subject || '-'}</p><div class="mt-2 text-xs font-bold ${isExpired ? 'text-red-400' : 'text-emerald-400'} bg-darkcard inline-block px-2 py-1 rounded border border-darkborder">Ablaufdatum: ${validUntil}</div></div></div>`; }); } else { container.innerHTML = '<p class="text-red-500 text-sm">Fehler beim Laden.</p>'; } } catch(e) { container.innerHTML = '<p class="text-red-500 text-sm">Netzwerkfehler.</p>'; } }
window.uploadPmgSsl = async function() { const cert = document.getElementById('pmgSslCert').value.trim(); const key = document.getElementById('pmgSslKey').value.trim(); if(!cert || !key) return alert('Bitte Key und Zertifikat (PEM) einfügen!'); const btn = event.target; const oTxt = btn.innerText; btn.innerText = 'Lade hoch...'; const fd = new FormData(); fd.append('node_id', document.getElementById('pmgNodeId').value); fd.append('internal_name', document.getElementById('pmgInternalName').value); fd.append('certificate', cert); fd.append('private_key', key); try { const res = await (await fetch('api.php?action=pmg_upload_ssl', {method: 'POST', body: fd})).json(); if(res.success) { alert('Zertifikat hochgeladen! Dienste werden neu gestartet.'); document.getElementById('pmgSslCert').value = ''; document.getElementById('pmgSslKey').value = ''; loadPmgSsl(document.getElementById('pmgNodeId').value); } else { alert('Fehler beim Upload. (Format prüfen)'); } } catch(e) { alert('Netzwerkfehler.'); } finally { btn.innerText = oTxt; } }
window.loadPmgSsl = async function(nodeId) { const internalName = document.getElementById('pmgInternalName').value; const container = document.getElementById('pmgSslContainer'); if(!container) return; container.innerHTML = '<p class="text-gray-500 animate-pulse text-sm">Lade Zertifikate...</p>'; try { const res = await (await fetch(`api.php?action=pmg_get_ssl&node_id=${nodeId}&internal_name=${internalName}`)).json(); if(res.success && res.data) { container.innerHTML = ''; res.data.forEach(cert => { const validUntil = cert.notafter ? formatDate(cert.notafter) : 'Unbekannt'; const isExpired = cert.notafter && cert.notafter < (Date.now() / 1000); const statusDot = isExpired ? '<span class="w-2 h-2 rounded-full bg-red-500 shrink-0"></span>' : '<span class="w-2 h-2 rounded-full bg-green-500 shrink-0"></span>'; container.innerHTML += `<div class="bg-darkbg border border-darkborder rounded p-3 text-sm flex gap-3"><div class="mt-1">${statusDot}</div><div class="flex-1 overflow-hidden"><h4 class="text-white font-bold mb-1 break-all">${cert.filename || cert.subject || 'Zertifikat'}</h4><p class="text-gray-400 text-xs mb-1"><span class="font-bold text-gray-500">Aussteller:</span> ${cert.issuer || '-'}</p><p class="text-gray-400 text-xs truncate" title="${cert.subject}"><span class="font-bold text-gray-500">Subject:</span> ${cert.subject || '-'}</p><div class="mt-2 text-xs font-bold ${isExpired ? 'text-red-400' : 'text-emerald-400'} bg-darkcard inline-block px-2 py-1 rounded border border-darkborder">Ablaufdatum: ${validUntil}</div></div></div>`; }); } else { container.innerHTML = '<p class="text-red-500 text-sm">Fehler beim Laden.</p>'; } } catch(e) { container.innerHTML = '<p class="text-red-500 text-sm">Netzwerkfehler.</p>'; } }
window.uploadPmgSsl = async function() { const cert = document.getElementById('pmgSslCert').value.trim(); const key = document.getElementById('pmgSslKey').value.trim(); if(!cert || !key) return alert('Bitte Key und Zertifikat 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 ? `<span class="text-xs text-gray-500">Das bist du</span>` : `<button onclick="deleteUser(${user.id}, '${user.username}')" class="text-red-500 hover:text-red-400 text-sm font-medium">Löschen</button>`; let roleBadge = user.role === 'admin' ? '<span class="bg-red-500/20 text-red-400 px-2 py-0.5 rounded text-xs">Admin</span>' : (user.role === 'vm_manager' ? '<span class="bg-blue-500/20 text-blue-400 px-2 py-0.5 rounded text-xs">VM Manager</span>' : '<span class="bg-gray-500/20 text-gray-400 px-2 py-0.5 rounded text-xs">Viewer</span>'); tbody.innerHTML += `<tr class="hover:bg-darkcard/50 transition-colors"><td class="px-4 py-3 font-medium text-white">${user.username}</td><td class="px-4 py-3">${roleBadge}</td><td class="px-4 py-3 text-right">${deleteBtn}</td></tr>`; }); } } 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 ? `<span class="text-xs text-gray-500">Das bist du</span>` : `<button onclick="deleteUser(${user.id}, '${user.username}')" class="text-red-500 hover:text-red-400 text-sm font-medium">Löschen</button>`; let roleBadge = user.role === 'admin' ? '<span class="bg-red-500/20 text-red-400 px-2 py-0.5 rounded text-xs">Admin</span>' : (user.role === 'vm_manager' ? '<span class="bg-blue-500/20 text-blue-400 px-2 py-0.5 rounded text-xs">VM Manager</span>' : '<span class="bg-gray-500/20 text-gray-400 px-2 py-0.5 rounded text-xs">Viewer</span>'); tbody.innerHTML += `<tr class="hover:bg-darkcard/50 transition-colors"><td class="px-4 py-3 font-medium text-white">${user.username}</td><td class="px-4 py-3">${roleBadge}</td><td class="px-4 py-3 text-right">${deleteBtn}</td></tr>`; }); } } 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 += `<label class="flex items-center gap-2 hover:bg-darkbg p-1 rounded cursor-pointer"><input type="checkbox" name="allowed_vms[]" value="${vm.vmid}" class="accent-proxmox"><span>${vm.name} <span class="text-xs text-gray-500">(${vm.host})</span></span></label>`; }); } } 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 = '<tr><td colspan="5" class="px-4 py-4 text-center animate-pulse text-gray-500">Lade VMs...</td></tr>'; 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 = '<tr><td colspan="5" class="px-4 py-4 text-center text-gray-500">Keine VMs.</td></tr>'; return; } res.data.forEach(vm => { const isRunning = vm.status === 'running'; const typeStr = vm.type === 'lxc' ? '📦 LXC' : '🖥️ QEMU'; const statusBadge = isRunning ? '<span class="text-green-500 font-bold">Online</span>' : '<span class="text-red-500 font-bold">Offline</span>'; let actionButtons = isRunning ? `<button onclick="sendVmCommand('${vm.vmid}', '${vm.host}', '${vm.type}', 'stop', ${vm.node_id})" class="text-red-500 px-2 text-lg">⏹️</button>` : `<button onclick="sendVmCommand('${vm.vmid}', '${vm.host}', '${vm.type}', 'start', ${vm.node_id})" class="text-green-500 px-2 text-lg">▶️</button>`; actionButtons += `<button onclick="openVmSettings('${vm.vmid}', '${vm.host}', '${vm.type}', ${vm.node_id}, '${vm.name}')" class="text-gray-400 hover:text-white px-2 ml-2 border-l border-darkborder text-lg">⚙️</button>`; tbody.innerHTML += `<tr class="border-b border-darkborder/50 hover:bg-darkbg"><td class="py-2 px-3 text-white">${vm.vmid} | ${vm.name}</td><td class="py-2 px-3 text-gray-400">${typeStr} - ${vm.host}</td><td class="py-2 px-3 text-gray-400">${vm.maxcpu||1} C / ${formatBytes(vm.maxmem||0)}</td><td class="py-2 px-3">${statusBadge}</td><td class="py-2 px-3 text-right">${actionButtons}</td></tr>`; }); } } 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 = '<tr><td colspan="5" class="px-4 py-4 text-center animate-pulse text-gray-500">Lade VMs...</td></tr>'; 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 = '<tr><td colspan="5" class="px-4 py-4 text-center text-gray-500">Keine VMs.</td></tr>'; return; } res.data.forEach(vm => { const isRunning = vm.status === 'running'; const typeStr = vm.type === 'lxc' ? '📦 LXC' : '🖥️ QEMU'; const statusBadge = isRunning ? '<span class="text-green-500 font-bold">Online</span>' : '<span class="text-red-500 font-bold">Offline</span>'; let actionButtons = isRunning ? `<button onclick="sendVmCommand('${vm.vmid}', '${vm.host}', '${vm.type}', 'stop', ${vm.node_id})" class="text-red-500 px-2 text-lg">⏹️</button>` : `<button onclick="sendVmCommand('${vm.vmid}', '${vm.host}', '${vm.type}', 'start', ${vm.node_id})" class="text-green-500 px-2 text-lg">▶️</button>`; actionButtons += `<button onclick="openVmSettings('${vm.vmid}', '${vm.host}', '${vm.type}', ${vm.node_id}, '${vm.name}')" class="text-gray-400 hover:text-white px-2 ml-2 border-l border-darkborder text-lg">⚙️</button>`; tbody.innerHTML += `<tr class="border-b border-darkborder/50 hover:bg-darkbg"><td class="py-2 px-3 text-white">${vm.vmid} | ${vm.name}</td><td class="py-2 px-3 text-gray-400">${typeStr} - ${vm.host}</td><td class="py-2 px-3 text-gray-400">${vm.maxcpu||1} C / ${formatBytes(vm.maxmem||0)}</td><td class="py-2 px-3">${statusBadge}</td><td class="py-2 px-3 text-right">${actionButtons}</td></tr>`; }); } } catch (err) {} }
const topologyModal = document.getElementById('nodeTopologyModal');
window.openNodeTopology = async function() { topologyModal.classList.remove('hidden'); const container = document.getElementById('nodeTopologyContainer'); container.innerHTML = '<div class="text-center text-gray-500 py-10 animate-pulse">Lade Cluster-Daten...</div>'; try { const [nodesRes, vmsRes] = await Promise.all([ fetch('api.php?action=get_nodes').then(r => r.json()), fetch('api.php?action=get_all_vms').then(r => r.json()) ]); if(nodesRes.success && vmsRes.success) { container.innerHTML = ''; const pveNodes = nodesRes.data.filter(n => n.type === 'pve'); if(pveNodes.length === 0) return; pveNodes.forEach(node => { const nodeVms = vmsRes.data.filter(v => v.node_id == node.id); let vmsHtml = ''; if(nodeVms.length > 0) { nodeVms.sort((a, b) => { if(a.status === 'running' && b.status !== 'running') return -1; if(a.status !== 'running' && b.status === 'running') return 1; return a.name.localeCompare(b.name); }); nodeVms.forEach(vm => { const icon = vm.type === 'lxc' ? '📦' : '🖥️'; const isRunning = vm.status === 'running'; const statusColor = isRunning ? 'border-green-500/30 bg-green-500/10 text-green-400' : 'border-darkborder bg-darkcard text-gray-400'; const dot = isRunning ? '<span class="w-2 h-2 rounded-full bg-green-500 animate-pulse shrink-0"></span>' : '<span class="w-2 h-2 rounded-full bg-gray-600 shrink-0"></span>'; vmsHtml += `<div class="flex flex-col p-3 rounded-lg border ${statusColor} transition-transform hover:scale-[1.02]"><div class="flex items-center gap-2 mb-1">${dot}<span class="font-bold text-sm truncate" title="${vm.name}">${icon} ${vm.name}</span></div><div class="flex justify-between text-xs opacity-75"><span>ID: ${vm.vmid}</span><span>${vm.maxcpu || 1}C / ${formatBytes(vm.maxmem || 0)}</span></div></div>`; }); } container.innerHTML += `<div class="bg-darkcard border border-darkborder rounded-xl p-5 shadow-lg"><div class="flex justify-between items-center mb-4 border-b border-darkborder pb-3"><h3 class="text-lg font-bold text-white flex items-center gap-2"><svg class="w-5 h-5 text-proxmox" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"></path></svg>${node.name}</h3><div class="flex gap-3"><button onclick="openLiveGraph('node', 0, '${node.name}', ${node.id}, '${node.name}')" class="text-blue-400 hover:text-white transition-colors text-sm" title="Node Performance">📈 Live Graph</button><span class="text-xs font-bold text-gray-400 bg-darkbg px-3 py-1 rounded-full border border-darkborder">${node.ip_address}</span></div></div><div class="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4">${vmsHtml}</div></div>`; }); } } catch(e) {} }
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 = '<div class="text-center text-gray-500 py-10 animate-pulse">Lade Cluster-Daten...</div>'; try { const [nodesRes, vmsRes] = await Promise.all([ fetch('api.php?action=get_nodes').then(r => r.json()), fetch('api.php?action=get_all_vms').then(r => r.json()) ]); if(nodesRes.success && vmsRes.success) { container.innerHTML = ''; const pveNodes = nodesRes.data.filter(n => n.type === 'pve'); if(pveNodes.length === 0) return; pveNodes.forEach(node => { const nodeVms = vmsRes.data.filter(v => v.node_id == node.id); let vmsHtml = ''; if(nodeVms.length > 0) { nodeVms.sort((a, b) => { if(a.status === 'running' && b.status !== 'running') return -1; if(a.status !== 'running' && b.status === 'running') return 1; return a.name.localeCompare(b.name); }); nodeVms.forEach(vm => { const icon = vm.type === 'lxc' ? '📦' : '🖥️'; const isRunning = vm.status === 'running'; const statusColor = isRunning ? 'border-green-500/30 bg-green-500/10 text-green-400' : 'border-darkborder bg-darkcard text-gray-400'; const dot = isRunning ? '<span class="w-2 h-2 rounded-full bg-green-500 animate-pulse shrink-0"></span>' : '<span class="w-2 h-2 rounded-full bg-gray-600 shrink-0"></span>'; vmsHtml += `<div class="flex flex-col p-3 rounded-lg border ${statusColor} transition-transform hover:scale-[1.02]"><div class="flex items-center gap-2 mb-1">${dot}<span class="font-bold text-sm truncate" title="${vm.name}">${icon} ${vm.name}</span></div><div class="flex justify-between text-xs opacity-75"><span>ID: ${vm.vmid}</span><span>${vm.maxcpu || 1}C / ${formatBytes(vm.maxmem || 0)}</span></div></div>`; }); } container.innerHTML += `<div class="bg-darkcard border border-darkborder rounded-xl p-5 shadow-lg"><div class="flex justify-between items-center mb-4 border-b border-darkborder pb-3"><h3 class="text-lg font-bold text-white flex items-center gap-2"><svg class="w-5 h-5 text-proxmox" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"></path></svg>${node.name}</h3><div class="flex gap-3"><button onclick="openLiveGraph('node', 0, '${node.name}', ${node.id}, '${node.name}')" class="text-blue-400 hover:text-white transition-colors text-sm" title="Node Performance">📈 Live Graph</button><span class="text-xs font-bold text-gray-400 bg-darkbg px-3 py-1 rounded-full border border-darkborder">${node.ip_address}</span></div></div><div class="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4">${vmsHtml}</div></div>`; }); } } catch(e) {} }
window.closeNodeTopology = function() { 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 = '<option value="">Lade Hosts...</option>'; try { const res = await (await fetch('api.php?action=get_nodes')).json(); if (res.success) { select.innerHTML = '<option value="">-- PVE Node auswählen --</option>'; res.data.filter(n => n.type === 'pve').forEach(node => { select.innerHTML += `<option value="${node.id}">${node.name} (${node.ip_address})</option>`; }); } } catch(e) {} loadCronJobs(); }
window.closeCronManager = function() { cronModal.classList.add('hidden'); }
window.checkCronAction = function() { const action = document.getElementById('cronAction').value; const vmBlock = document.getElementById('cronVmBlock'); if (action.includes('_vm')) vmBlock.classList.remove('hidden'); else vmBlock.classList.add('hidden'); }
async function loadCronJobs() { const tbody = document.getElementById('cronTableBody'); tbody.innerHTML = '<tr><td colspan="6" class="text-center text-gray-500 py-4 animate-pulse">Lade Jobs...</td></tr>'; try { const res = await (await fetch('api.php?action=get_cron_jobs')).json(); if (res.success) { tbody.innerHTML = ''; if(res.data.length === 0) { tbody.innerHTML = '<tr><td colspan="6" class="text-center text-gray-500 py-4">Keine geplanten Jobs.</td></tr>'; return; } res.data.forEach(job => { const lastRun = formatDate(job.last_run); const isActive = parseInt(job.is_active) === 1; const statusDot = isActive ? '<span class="w-2 h-2 rounded-full bg-green-500 shadow-[0_0_5px_#22c55e]"></span> Aktiv' : '<span class="w-2 h-2 rounded-full bg-gray-600"></span> Pausiert'; const targetStr = job.action_type.includes('_vm') ? `VM ${job.target_vmid}` : 'Gesamter Host'; let actionStr = job.action_type; if(actionStr === 'reboot_node') actionStr = 'Host Reboot'; if(actionStr === 'start_vm') actionStr = 'VM Start'; if(actionStr === 'stop_vm') actionStr = 'VM Stop'; if(actionStr === 'reboot_vm') actionStr = 'VM Reboot'; tbody.innerHTML += `<tr class="hover:bg-darkcard/50 transition-colors border-b border-darkborder/50"><td class="px-4 py-3 whitespace-nowrap cursor-pointer" onclick="toggleCronJob(${job.id}, ${isActive ? 0 : 1})"><div class="flex items-center gap-2 text-xs font-bold text-gray-300 hover:text-white">${statusDot}</div></td><td class="px-4 py-3"><p class="text-white font-bold">${job.name}</p><p class="text-xs text-proxmox font-bold">${actionStr}</p></td><td class="px-4 py-3"><p class="text-gray-300">${job.node_name}</p><p class="text-xs text-gray-500">Ziel: ${targetStr}</p></td><td class="px-4 py-3 font-mono text-blue-400 font-bold tracking-widest">${job.cron_schedule}</td><td class="px-4 py-3 text-gray-400 whitespace-nowrap">${lastRun}</td><td class="px-4 py-3 text-right"><button onclick="deleteCronJob(${job.id})" class="text-red-500 hover:text-white px-2 py-1 bg-red-500/10 hover:bg-red-500 rounded text-xs font-bold transition-colors">Löschen</button></td></tr>`; }); } } catch(e) {} }
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 = '<option value="">Lade Hosts...</option>'; try { const res = await (await fetch('api.php?action=get_nodes')).json(); if (res.success) { select.innerHTML = '<option value="">-- PVE Node auswählen --</option>'; res.data.filter(n => n.type === 'pve').forEach(node => { select.innerHTML += `<option value="${node.id}">${node.name} (${node.ip_address})</option>`; }); } } catch(e) {} } loadCronJobs(); }
window.closeCronManager = function() { 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 = '<tr><td colspan="6" class="text-center text-gray-500 py-4 animate-pulse">Lade Jobs...</td></tr>'; try { const res = await (await fetch('api.php?action=get_cron_jobs')).json(); if (res.success) { tbody.innerHTML = ''; if(res.data.length === 0) { tbody.innerHTML = '<tr><td colspan="6" class="text-center text-gray-500 py-4">Keine geplanten Jobs.</td></tr>'; return; } res.data.forEach(job => { const lastRun = formatDate(job.last_run); const isActive = parseInt(job.is_active) === 1; const statusDot = isActive ? '<span class="w-2 h-2 rounded-full bg-green-500 shadow-[0_0_5px_#22c55e]"></span> Aktiv' : '<span class="w-2 h-2 rounded-full bg-gray-600"></span> Pausiert'; const targetStr = job.action_type.includes('_vm') ? `VM ${job.target_vmid}` : 'Gesamter Host'; let actionStr = job.action_type; if(actionStr === 'reboot_node') actionStr = 'Host Reboot'; if(actionStr === 'start_vm') actionStr = 'VM Start'; if(actionStr === 'stop_vm') actionStr = 'VM Stop'; if(actionStr === 'reboot_vm') actionStr = 'VM Reboot'; tbody.innerHTML += `<tr class="hover:bg-darkcard/50 transition-colors border-b border-darkborder/50"><td class="px-4 py-3 whitespace-nowrap cursor-pointer" onclick="toggleCronJob(${job.id}, ${isActive ? 0 : 1})"><div class="flex items-center gap-2 text-xs font-bold text-gray-300 hover:text-white">${statusDot}</div></td><td class="px-4 py-3"><p class="text-white font-bold">${job.name}</p><p class="text-xs text-proxmox font-bold">${actionStr}</p></td><td class="px-4 py-3"><p class="text-gray-300">${job.node_name}</p><p class="text-xs text-gray-500">Ziel: ${targetStr}</p></td><td class="px-4 py-3 font-mono text-blue-400 font-bold tracking-widest">${job.cron_schedule}</td><td class="px-4 py-3 text-gray-400 whitespace-nowrap">${lastRun}</td><td class="px-4 py-3 text-right"><button onclick="deleteCronJob(${job.id})" class="text-red-500 hover:text-white px-2 py-1 bg-red-500/10 hover:bg-red-500 rounded text-xs font-bold transition-colors">Löschen</button></td></tr>`; }); } } catch(e) {} }
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 = '<tr><td colspan="4" class="text-center text-gray-500 py-4 animate-pulse">Lade Protokoll...</td></tr>'; try { const res = await (await fetch('api.php?action=get_audit_logs')).json(); if (res.success) { tbody.innerHTML = ''; if(res.data.length === 0) { tbody.innerHTML = '<tr><td colspan="4" class="text-center text-gray-500 py-4">Noch keine Einträge.</td></tr>'; return; } res.data.forEach(log => { const d = new Date(log.timestamp + 'Z'); const timeStr = d.toLocaleDateString('de-DE') + ' ' + d.toLocaleTimeString('de-DE'); const isSystem = log.username === 'System'; tbody.innerHTML += `<tr class="hover:bg-darkcard/50 transition-colors border-b border-darkborder/50"><td class="px-4 py-3 text-gray-400 whitespace-nowrap text-xs">${timeStr}</td><td class="px-4 py-3"><span class="${isSystem ? 'text-gray-500' : 'text-blue-400 font-bold'}">${log.username}</span></td><td class="px-4 py-3 font-bold text-white">${log.action}</td><td class="px-4 py-3 text-gray-300 text-xs">${log.target || '-'}</td></tr>`; }); } } catch(e) { tbody.innerHTML = '<tr><td colspan="4" class="text-center text-red-500 py-4">Fehler beim Laden.</td></tr>'; } }
window.closeAuditLog = function() { auditModal.classList.add('hidden'); }
window.openAuditLog = async function() { const m = document.getElementById('auditLogModal'); if(m) m.classList.remove('hidden'); const tbody = document.getElementById('auditLogTableBody'); if(!tbody) return; tbody.innerHTML = '<tr><td colspan="4" class="text-center text-gray-500 py-4 animate-pulse">Lade Protokoll...</td></tr>'; try { const res = await (await fetch('api.php?action=get_audit_logs')).json(); if (res.success) { tbody.innerHTML = ''; if(res.data.length === 0) { tbody.innerHTML = '<tr><td colspan="4" class="text-center text-gray-500 py-4">Noch keine Einträge.</td></tr>'; return; } res.data.forEach(log => { const d = new Date(log.timestamp + 'Z'); const timeStr = d.toLocaleDateString('de-DE') + ' ' + d.toLocaleTimeString('de-DE'); const isSystem = log.username === 'System'; tbody.innerHTML += `<tr class="hover:bg-darkcard/50 transition-colors border-b border-darkborder/50"><td class="px-4 py-3 text-gray-400 whitespace-nowrap text-xs">${timeStr}</td><td class="px-4 py-3"><span class="${isSystem ? 'text-gray-500' : 'text-blue-400 font-bold'}">${log.username}</span></td><td class="px-4 py-3 font-bold text-white">${log.action}</td><td class="px-4 py-3 text-gray-300 text-xs">${log.target || '-'}</td></tr>`; }); } } catch(e) { tbody.innerHTML = '<tr><td colspan="4" class="text-center text-red-500 py-4">Fehler beim Laden.</td></tr>'; } }
window.closeAuditLog = function() { 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);
}
+22 -83
View File
@@ -1,91 +1,30 @@
<!-- /home/docker/pve_dashboard/src/modals.php -->
<!-- [Bestehende Modals für NodeManager, UserManager, CronManager bleiben erhalten (aus Platzgründen gekürzt, kopiere deine bestehenden Modals hier rein, falls nötig)] -->
<div id="nodeManagerModal" class="fixed inset-0 bg-black/80 hidden z-50 flex items-center justify-center backdrop-blur-sm"><div class="bg-darkcard border border-darkborder rounded-xl shadow-2xl w-full max-w-4xl overflow-hidden flex flex-col max-h-[85vh]"><div class="p-5 border-b border-darkborder flex justify-between items-center bg-darkbg"><h2 class="text-xl font-bold text-white flex items-center gap-2">Node Verwaltung</h2><button onclick="closeNodeManager()" class="text-gray-400 hover:text-white transition-colors">✕</button></div><div class="p-6 overflow-y-auto flex-1"><div class="mb-8"><div class="flex justify-between items-center mb-4"><h3 class="text-white font-bold">Verbundene Systeme</h3><button onclick="toggleAddNodeForm()" class="bg-proxmox/20 text-proxmox hover:bg-proxmox hover:text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-colors">+ Server hinzufügen</button></div><div class="bg-darkbg rounded-lg border border-darkborder overflow-hidden"><table class="w-full text-left text-sm text-gray-400"><thead class="bg-darkcard border-b border-darkborder text-xs uppercase"><tr><th class="px-4 py-3">Name</th><th class="px-4 py-3">Typ</th><th class="px-4 py-3">IP-Adresse</th><th class="px-4 py-3 text-right">Aktion</th></tr></thead><tbody id="nodeTableBody" class="divide-y divide-darkborder"></tbody></table></div></div><div id="addNodeFormContainer" class="hidden bg-darkbg p-5 rounded-lg border border-darkborder"><h3 class="text-white font-bold mb-4 border-b border-darkborder pb-2">Neues System anbinden</h3><form id="addNewNodeForm" class="grid grid-cols-2 gap-4"><div><label class="block text-xs font-medium text-gray-500 mb-1">System Typ</label><select id="newNodeType" class="w-full bg-darkcard border border-darkborder rounded p-2 text-white"><option value="pve">Proxmox VE (PVE)</option><option value="pbs">Backup Server (PBS)</option><option value="pmg">Mail Gateway (PMG)</option></select></div><div><label class="block text-xs font-medium text-gray-500 mb-1">Anzeigename</label><input type="text" id="newNodeName" class="w-full bg-darkcard border border-darkborder rounded p-2 text-white" required></div><div><label class="block text-xs font-medium text-gray-500 mb-1">IP-Adresse</label><input type="text" id="newNodeIp" class="w-full bg-darkcard border border-darkborder rounded p-2 text-white" required></div><div><label class="block text-xs font-medium text-gray-500 mb-1">Benutzername</label><input type="text" id="newNodeUser" value="root@pam" class="w-full bg-darkcard border border-darkborder rounded p-2 text-white" required></div><div><label class="block text-xs font-medium text-gray-500 mb-1">Passwort</label><input type="password" id="newNodePass" class="w-full bg-darkcard border border-darkborder rounded p-2 text-white" required></div><div class="col-span-2 mt-2"><button type="submit" class="w-full bg-proxmox hover:bg-orange-600 text-white font-bold py-2.5 rounded">Verbinden</button></div></form></div></div></div></div>
<!-- ANGEPASST: VM SETTINGS MODAL (Erweitert um ISO & OnBoot) -->
<div id="vmSettingsModal" class="fixed inset-0 bg-black/80 hidden z-[70] flex items-center justify-center backdrop-blur-sm">
<div class="bg-darkcard border border-darkborder rounded-xl shadow-2xl w-full max-w-4xl overflow-hidden flex flex-col max-h-[90vh]">
<div class="p-5 border-b border-darkborder flex justify-between items-center bg-darkbg">
<h2 class="text-xl font-bold text-white flex items-center gap-2"><span id="settingsModalTitle">Einstellungen</span></h2>
<button onclick="closeVmSettings()" class="text-gray-400 hover:text-white transition-colors text-2xl">✕</button>
</div>
<div class="p-6 overflow-y-auto space-y-6 flex-1">
<input type="hidden" id="setVmid"><input type="hidden" id="setHost"><input type="hidden" id="setType"><input type="hidden" id="setNodeId"><input type="hidden" id="setRawNet0"><input type="hidden" id="setPrimaryDisk">
<div id="userManagerModal" class="fixed inset-0 bg-black/80 hidden z-50 flex items-center justify-center backdrop-blur-sm"><div class="bg-darkcard border border-darkborder rounded-xl shadow-2xl w-full max-w-4xl overflow-hidden flex flex-col max-h-[90vh]"><div class="p-5 border-b border-darkborder flex justify-between items-center bg-darkbg"><h2 class="text-xl font-bold text-white flex items-center gap-2">Benutzer & Rollen</h2><button onclick="closeUserManager()" class="text-gray-400 hover:text-white transition-colors">✕</button></div><div class="p-6 overflow-y-auto flex-1 flex flex-col lg:flex-row gap-8"><div class="flex-1"><h3 class="text-white font-bold mb-4">Aktive Accounts</h3><div class="bg-darkbg rounded-lg border border-darkborder overflow-hidden"><table class="w-full text-left text-sm text-gray-400"><thead class="bg-darkcard border-b border-darkborder text-xs uppercase"><tr><th class="px-4 py-3">Benutzer</th><th class="px-4 py-3">Rolle</th><th class="px-4 py-3 text-right">Aktion</th></tr></thead><tbody id="userTableBody" class="divide-y divide-darkborder"></tbody></table></div></div><div class="flex-1 bg-darkbg p-5 rounded-lg border border-darkborder"><h3 class="text-white font-bold mb-4 border-b border-darkborder pb-2">Neuen Benutzer anlegen</h3><form id="addNewUserForm" class="space-y-4"><div class="grid grid-cols-2 gap-4"><div><label class="block text-xs font-medium text-gray-500 mb-1">Benutzername</label><input type="text" id="newUsername" class="w-full bg-darkcard border border-darkborder rounded p-2 text-white" required></div><div><label class="block text-xs font-medium text-gray-500 mb-1">Passwort</label><input type="password" id="newUserPass" class="w-full bg-darkcard border border-darkborder rounded p-2 text-white" required></div></div><div><label class="block text-xs font-medium text-gray-500 mb-1">Rolle</label><select id="newUserRole" onchange="handleRoleChange()" class="w-full bg-darkcard border border-darkborder rounded p-2 text-white"><option value="viewer">Viewer</option><option value="vm_manager">VM Manager</option><option value="admin">Admin</option></select></div><div id="vmPermissionsBlock" class="hidden border border-darkborder rounded p-3 bg-darkcard/50"><p class="text-xs font-bold text-gray-400 mb-2">VMs erlauben:</p><div id="vmCheckboxList" class="max-h-40 overflow-y-auto space-y-1 text-sm text-gray-300"></div></div><button type="submit" class="w-full bg-proxmox hover:bg-orange-600 text-white font-bold py-2.5 rounded mt-4">Anlegen</button></form></div></div></div></div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- Spalte 1 -->
<div class="space-y-6">
<div class="bg-darkbg p-5 rounded-lg border border-darkborder">
<h3 class="text-white font-bold mb-4">💻 Ressourcen & Boot</h3>
<div class="grid grid-cols-2 gap-4 mb-4">
<div><label class="block text-xs font-medium text-gray-500 mb-1">RAM (MB)</label><input type="number" id="setMemory" class="w-full bg-darkcard border border-darkborder rounded p-2 text-white text-sm"></div>
<div><label class="block text-xs font-medium text-gray-500 mb-1">CPU Cores</label><input type="number" id="setCores" class="w-full bg-darkcard border border-darkborder rounded p-2 text-white text-sm"></div>
</div>
<div class="mb-4">
<label class="block text-xs font-medium text-gray-500 mb-1">CD/DVD Laufwerk (ISO Mounten)</label>
<select id="setIso" class="w-full bg-darkcard border border-darkborder rounded p-2 text-white text-sm">
<option value="">Lade ISOs...</option>
</select>
</div>
<div class="mb-4">
<label class="flex items-center gap-2 text-white text-sm cursor-pointer">
<input type="checkbox" id="setOnboot" class="accent-proxmox w-4 h-4"> Start at boot (Auto-Start mit PVE)
</label>
</div>
<button onclick="saveHardwareSettings()" class="w-full bg-proxmox hover:bg-orange-600 text-white font-bold py-2 px-4 rounded text-sm transition-colors">Alle VM-Settings Speichern</button>
</div>
<div id="vmManagerModal" class="fixed inset-0 bg-black/80 hidden z-[60] flex items-center justify-center backdrop-blur-sm"><div class="bg-darkcard border border-darkborder rounded-xl shadow-2xl w-full max-w-5xl overflow-hidden flex flex-col max-h-[90vh]"><div class="p-5 border-b border-darkborder flex justify-between items-center bg-darkbg"><div class="flex items-center gap-4"><h2 class="text-xl font-bold text-white flex items-center gap-2"><svg class="w-6 h-6 text-proxmox" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01"></path></svg> Virtuelle Maschinen & LXC</h2><button onclick="openCreateVm()" class="bg-proxmox/20 text-proxmox hover:bg-proxmox hover:text-white px-3 py-1 rounded text-xs font-bold border border-proxmox/50">+ Neue VM</button></div><button onclick="closeVmManager()" class="text-gray-400 hover:text-white transition-colors">✕</button></div><div class="p-6 overflow-y-auto flex-1"><div class="bg-darkbg rounded-lg border border-darkborder overflow-hidden"><table class="w-full text-left text-sm text-gray-400"><thead class="bg-darkcard border-b border-darkborder text-xs uppercase"><tr><th class="px-4 py-3">ID / Name</th><th class="px-4 py-3">Host / Typ</th><th class="px-4 py-3">Ressourcen</th><th class="px-4 py-3 text-center">Status</th><th class="px-4 py-3 text-right">Aktionen</th></tr></thead><tbody id="vmTableBody" class="divide-y divide-darkborder"></tbody></table></div></div></div></div>
<div class="bg-darkbg p-5 rounded-lg border border-darkborder">
<h3 class="text-white font-bold mb-4">🌐 Netzwerkverwaltung</h3>
<div class="flex items-center gap-3 mb-6 border-b border-darkborder pb-4">
<span id="netStatusBadge" class="px-2 py-1 rounded text-xs font-bold text-gray-400 bg-gray-500/20">Lade...</span>
<button id="btnToggleNet" onclick="toggleNetwork()" class="bg-darkcard border border-darkborder hover:border-proxmox text-white font-bold py-1.5 px-4 rounded text-sm transition-colors">Lade...</button>
</div>
<div>
<h4 class="text-gray-400 font-bold mb-2 text-xs uppercase">Zusätzliche NIC</h4>
<div class="flex items-end gap-3"><div class="flex-1"><label class="block text-xs font-medium text-gray-500 mb-1">Bridge (z.B. vmbr0)</label><input type="text" id="addNicBridge" value="vmbr0" class="w-full bg-darkcard border border-darkborder rounded p-2 text-white text-sm"></div><button onclick="addNic()" class="bg-proxmox hover:bg-orange-600 text-white font-bold py-2 px-4 rounded text-sm">+ Add</button></div>
</div>
</div>
</div>
<div id="createVmModal" class="fixed inset-0 bg-black/80 hidden z-[80] flex items-center justify-center backdrop-blur-sm"><div class="bg-darkcard border border-darkborder rounded-xl shadow-2xl w-full max-w-md overflow-hidden flex flex-col"><div class="p-5 border-b border-darkborder flex justify-between items-center bg-darkbg"><h2 class="text-xl font-bold text-white">✨ Neue VM erstellen</h2><button onclick="closeCreateVm()" class="text-gray-400 hover:text-white transition-colors">✕</button></div><div class="p-6"><form id="createVmForm" class="space-y-4"><div><label class="block text-xs font-medium text-gray-500 mb-1">Ziel-Host (PVE Node)</label><select id="createVmHost" class="w-full bg-darkbg border border-darkborder rounded p-2 text-white text-sm focus:border-proxmox"><option value="">Lade Hosts...</option></select></div><div><label class="block text-xs font-medium text-gray-500 mb-1">Name der VM</label><input type="text" id="createVmName" placeholder="z.B. webserver-01" class="w-full bg-darkbg border border-darkborder rounded p-2 text-white text-sm" required></div><div class="grid grid-cols-2 gap-4"><div><label class="block text-xs font-medium text-gray-500 mb-1">RAM (MB)</label><input type="number" id="createVmRam" value="2048" step="512" min="512" class="w-full bg-darkbg border border-darkborder rounded p-2 text-white text-sm" required></div><div><label class="block text-xs font-medium text-gray-500 mb-1">CPU Cores</label><input type="number" id="createVmCores" value="2" min="1" class="w-full bg-darkbg border border-darkborder rounded p-2 text-white text-sm" required></div></div><button type="submit" class="w-full bg-proxmox hover:bg-orange-600 text-white font-bold py-3 rounded mt-4 transition-colors">Freie ID suchen & Erstellen</button></form></div></div></div>
<!-- Spalte 2 -->
<div class="space-y-6">
<div class="bg-darkbg p-5 rounded-lg border border-darkborder border-l-4 border-l-emerald-500">
<h3 class="text-white font-bold mb-4">📸 Snapshots</h3>
<div class="flex items-end gap-3 mb-4"><div class="flex-1"><input type="text" id="newSnapName" placeholder="Name" class="w-full bg-darkcard border border-darkborder rounded p-2 text-white text-sm"></div><button onclick="createSnapshot()" class="bg-emerald-600 hover:bg-emerald-500 text-white font-bold py-2 px-4 rounded text-sm transition-colors">+ Create</button></div>
<div class="space-y-2 mt-4 max-h-40 overflow-y-auto" id="snapshotListContainer"></div>
</div>
<div class="bg-darkbg p-5 rounded-lg border border-darkborder border-l-4 border-l-pbs">
<h3 class="text-white font-bold mb-4">🛡️ Backup (vzdump)</h3>
<div class="flex items-end gap-3 mb-4 border-b border-darkborder pb-4"><div class="flex-1"><select id="backupTargetStorage" class="w-full bg-darkcard border border-darkborder rounded p-2 text-white text-sm"></select></div><button onclick="createBackup()" class="bg-pbs hover:bg-purple-500 text-white font-bold py-2 px-4 rounded text-sm transition-colors">Backup</button></div>
<div class="space-y-2 max-h-40 overflow-y-auto" id="backupListContainer"></div>
</div>
</div>
</div>
</div>
</div>
</div>
<div id="vmSettingsModal" class="fixed inset-0 bg-black/80 hidden z-[70] flex items-center justify-center backdrop-blur-sm"><div class="bg-darkcard border border-darkborder rounded-xl shadow-2xl w-full max-w-4xl overflow-hidden flex flex-col max-h-[90vh]"><div class="p-5 border-b border-darkborder flex justify-between items-center bg-darkbg"><h2 class="text-xl font-bold text-white flex items-center gap-2"><span id="settingsModalTitle">Einstellungen</span></h2><button onclick="closeVmSettings()" class="text-gray-400 hover:text-white transition-colors text-2xl">✕</button></div><div class="p-6 overflow-y-auto space-y-6 flex-1"><input type="hidden" id="setVmid"><input type="hidden" id="setHost"><input type="hidden" id="setType"><input type="hidden" id="setNodeId"><input type="hidden" id="setRawNet0"><input type="hidden" id="setPrimaryDisk"><div class="grid grid-cols-1 lg:grid-cols-2 gap-6"><div class="space-y-6"><div class="bg-darkbg p-5 rounded-lg border border-darkborder"><h3 class="text-white font-bold mb-4">💻 Ressourcen & Boot</h3><div class="grid grid-cols-2 gap-4 mb-4"><div><label class="block text-xs font-medium text-gray-500 mb-1">RAM (MB)</label><input type="number" id="setMemory" class="w-full bg-darkcard border border-darkborder rounded p-2 text-white text-sm"></div><div><label class="block text-xs font-medium text-gray-500 mb-1">CPU Cores</label><input type="number" id="setCores" class="w-full bg-darkcard border border-darkborder rounded p-2 text-white text-sm"></div></div><div class="mb-4"><label class="block text-xs font-medium text-gray-500 mb-1">CD/DVD Laufwerk (ISO Mounten)</label><select id="setIso" class="w-full bg-darkcard border border-darkborder rounded p-2 text-white text-sm"><option value="">Lade ISOs...</option></select></div><div class="mb-4"><label class="flex items-center gap-2 text-white text-sm cursor-pointer"><input type="checkbox" id="setOnboot" class="accent-proxmox w-4 h-4"> Start at boot (Auto-Start mit PVE)</label></div><button onclick="saveHardwareSettings()" class="w-full bg-proxmox hover:bg-orange-600 text-white font-bold py-2 px-4 rounded text-sm transition-colors">Alle VM-Settings Speichern</button></div><div class="bg-darkbg p-5 rounded-lg border border-darkborder"><h3 class="text-white font-bold mb-4">🌐 Netzwerkverwaltung</h3><div class="flex items-center gap-3 mb-6 border-b border-darkborder pb-4"><span id="netStatusBadge" class="px-2 py-1 rounded text-xs font-bold text-gray-400 bg-gray-500/20">Lade...</span><button id="btnToggleNet" onclick="toggleNetwork()" class="bg-darkcard border border-darkborder hover:border-proxmox text-white font-bold py-1.5 px-4 rounded text-sm transition-colors">Lade...</button></div><div><h4 class="text-gray-400 font-bold mb-2 text-xs uppercase">Zusätzliche NIC</h4><div class="flex items-end gap-3"><div class="flex-1"><label class="block text-xs font-medium text-gray-500 mb-1">Bridge Name (z.B. vmbr0)</label><input type="text" id="addNicBridge" value="vmbr0" class="w-full bg-darkcard border border-darkborder rounded p-2 text-white text-sm"></div><button onclick="addNic()" class="bg-proxmox hover:bg-orange-600 text-white font-bold py-2 px-4 rounded text-sm">+ Hinzufügen</button></div></div></div><div class="bg-darkbg p-5 rounded-lg border border-darkborder border-l-4 border-l-blue-500"><h3 class="text-white font-bold mb-4">💾 Festplatte erweitern</h3><div class="flex items-end gap-4"><div class="flex-1"><label class="block text-xs font-medium text-gray-500 mb-1">Um wie viel GB? (Disk: <span id="diskLabelName" class="text-proxmox">Lade...</span>)</label><input type="number" id="addDiskGb" min="1" placeholder="z.B. 10" class="w-full bg-darkcard border border-darkborder rounded p-2 text-white text-sm"></div><button onclick="expandDisk()" class="bg-blue-600 hover:bg-blue-500 text-white font-bold py-2 px-6 rounded text-sm transition-colors">Erweitern</button></div></div></div><div class="space-y-6"><div class="bg-darkbg p-5 rounded-lg border border-darkborder border-l-4 border-l-emerald-500"><h3 class="text-white font-bold mb-4">📸 Sicherung & Snapshots</h3><div class="flex items-end gap-3 mb-4"><div class="flex-1"><label class="block text-xs font-medium text-gray-500 mb-1">Neuer Snapshot Name</label><input type="text" id="newSnapName" placeholder="z.B. pre-update" class="w-full bg-darkcard border border-darkborder rounded p-2 text-white text-sm"></div><button onclick="createSnapshot()" class="bg-emerald-600 hover:bg-emerald-500 text-white font-bold py-2 px-4 rounded text-sm transition-colors">+ Snapshot</button></div><div class="space-y-2 mt-4 max-h-40 overflow-y-auto" id="snapshotListContainer"><div class="text-gray-500 text-sm italic">Lade Snapshots...</div></div></div><div class="bg-darkbg p-5 rounded-lg border border-darkborder border-l-4 border-l-pbs"><h3 class="text-white font-bold mb-4">🛡️ Backup Archive (vzdump)</h3><div class="flex items-end gap-3 mb-4 border-b border-darkborder pb-4"><div class="flex-1"><label clasLine truncated
<!-- NEU: NODE BACKUP JOBS MODAL -->
<div id="nodeBackupsModal" class="fixed inset-0 bg-black/80 hidden z-[75] flex items-center justify-center backdrop-blur-sm">
<div class="bg-darkcard border border-darkborder rounded-xl shadow-2xl w-full max-w-4xl overflow-hidden flex flex-col max-h-[80vh]">
<div class="p-5 border-b border-darkborder flex justify-between items-center bg-darkbg">
<h2 class="text-xl font-bold text-white" id="nodeBackupsTitle">Backup Jobs</h2>
<button onclick="closeNodeBackups()" class="text-gray-400 hover:text-white transition-colors text-2xl">✕</button>
</div>
<div class="p-6 overflow-y-auto flex-1 bg-darkbg">
<div class="bg-darkcard border border-darkborder rounded-xl shadow-lg">
<table class="w-full text-left text-sm text-gray-400">
<thead class="bg-darkbg text-xs uppercase border-b border-darkborder">
<tr><th class="py-3 px-4">ID</th><th class="py-3 px-4">Schedule</th><th class="py-3 px-4">Storage</th><th class="py-3 px-4">VMs</th><th class="py-3 px-4 text-right">Info</th></tr>
</thead>
<tbody id="nodeBackupsTableBody" class="divide-y divide-darkborder/50"></tbody>
</table>
</div>
</div>
</div>
</div>
<div id="nodeBackupsModal" class="fixed inset-0 bg-black/80 hidden z-[75] flex items-center justify-center backdrop-blur-sm"><div class="bg-darkcard border border-darkborder rounded-xl shadow-2xl w-full max-w-4xl overflow-hidden flex flex-col max-h-[80vh]"><div class="p-5 border-b border-darkborder flex justify-between items-center bg-darkbg"><h2 class="text-xl font-bold text-white" id="nodeBackupsTitle">Backup Jobs</h2><button onclick="closeNodeBackups()" class="text-gray-400 hover:text-white transition-colors text-2xl">✕</button></div><div class="p-6 overflow-y-auto flex-1 bg-darkbg"><div class="bg-darkcard border border-darkborder rounded-xl shadow-lg"><table class="w-full text-left text-sm text-gray-400"><thead class="bg-darkbg text-xs uppercase border-b border-darkborder"><tr><th class="py-3 px-4">ID</th><th class="py-3 px-4">Schedule</th><th class="py-3 px-4">Storage</th><th class="py-3 px-4">VMs</th><th class="py-3 px-4 text-right">Info</th></tr></thead><tbody id="nodeBackupsTableBody" class="divide-y divide-darkborder/50"></tbody></table></div></div></div></div>
<!-- [Die restlichen Modal-Container (liveGraphModal, taskLogModal, cronManagerModal etc.) bleiben wie sie waren.] -->
<div id="createVmModal" class="fixed inset-0 bg-black/80 hidden z-[80] flex items-center justify-center backdrop-blur-sm"><div class="bg-darkcard border border-darkborder rounded-xl shadow-2xl w-full max-w-md overflow-hidden flex flex-col"><div class="p-5 border-b border-darkborder flex justify-between items-center bg-darkbg"><h2 class="text-xl font-bold text-white">✨ Neue VM erstellen</h2><button onclick="closeCreateVm()" class="text-gray-400 hover:text-white transition-colors">✕</button></div><div class="p-6"><form id="createVmForm" class="space-y-4"><div><label class="block text-xs font-medium text-gray-500 mb-1">Ziel-Host</label><select id="createVmHost" class="w-full bg-darkbg border border-darkborder rounded p-2 text-white text-sm" required></select></div><div><label class="block text-xs font-medium text-gray-500 mb-1">Name der VM</label><input type="text" id="createVmName" class="w-full bg-darkbg border border-darkborder rounded p-2 text-white text-sm" required></div><div class="grid grid-cols-2 gap-4"><div><label class="block text-xs font-medium text-gray-500 mb-1">RAM (MB)</label><input type="number" id="createVmRam" value="2048" class="w-full bg-darkbg border border-darkborder rounded p-2 text-white text-sm" required></div><div><label class="block text-xs font-medium text-gray-500 mb-1">CPU Cores</label><input type="number" id="createVmCores" value="2" class="w-full bg-darkbg border border-darkborder rounded p-2 text-white text-sm" required></div></div><button type="submit" class="w-full bg-proxmox hover:bg-orange-600 text-white font-bold py-3 rounded mt-4 transition-colors">Erstellen</button></form></div></div></div>
<div id="nodeTopologyModal" class="fixed inset-0 bg-black/80 hidden z-[65] flex items-center justify-center backdrop-blur-sm"><div class="bg-darkcard border border-darkborder rounded-xl shadow-2xl w-full max-w-6xl overflow-hidden flex flex-col max-h-[90vh]"><div class="p-5 border-b border-darkborder flex justify-between items-center bg-darkbg"><h2 class="text-xl font-bold text-white flex items-center gap-2"><svg class="w-6 h-6 text-proxmox" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"></path></svg> Cluster Topologie</h2><button onclick="closeNodeTopology()" class="text-gray-400 hover:text-white">✕</button></div><div class="p-6 overflow-y-auto flex-1 bg-darkbg"><div id="nodeTopologyContainer" class="space-y-6"></div></div></div></div>
<div id="liveGraphModal" class="fixed inset-0 bg-black/90 hidden z-[90] flex items-center justify-center backdrop-blur-md"><div class="bg-darkcard border border-darkborder rounded-xl shadow-2xl w-full max-w-6xl overflow-hidden flex flex-col max-h-[90vh]"><div class="p-5 border-b border-darkborder flex justify-between items-center bg-darkbg"><h2 class="text-xl font-bold text-white flex items-center gap-2"><span id="graphStatusDot" class="w-2 h-2 rounded-full bg-green-500 animate-pulse"></span><span id="graphModalTitle">Performance</span></h2><button onclick="closeLiveGraph()" class="text-gray-400 hover:text-white transition-colors text-2xl">✕</button></div><div class="flex border-b border-darkborder bg-darkbg px-5 pt-4"><button onclick="switchGraphTab('live')" id="btn-graph-live" class="px-4 py-2 border-b-2 border-proxmox text-white font-bold">Live</button><button onclick="switchGraphTab('day')" id="btn-graph-day" class="px-4 py-2 border-b-2 border-transparent text-gray-400 hover:text-white font-bold">24h</button></div><div class="p-6 bg-darkbg overflow-y-auto"><div class="grid grid-cols-1 lg:grid-cols-2 gap-6"><div class="bg-darkcard border border-darkborder rounded-xl p-4 shadow-lg"><h3 class="text-white font-bold mb-3 text-sm">CPU & RAM (%)</h3><div class="relative w-full h-[250px]"><canvas id="perfChart"></canvas></div></div><div class="bg-darkcard border border-darkborder rounded-xl p-4 shadow-lg"><h3 class="text-white font-bold mb-3 text-sm">Netzwerk Traffic</h3><div class="relative w-full h-[250px]"><canvas id="netChart"></canvas></div></div></div></div></div></div>
<div id="liveGraphModal" class="fixed inset-0 bg-black/90 hidden z-[90] flex items-center justify-center backdrop-blur-md"><div class="bg-darkcard border border-darkborder rounded-xl shadow-2xl w-full max-w-6xl overflow-hidden flex flex-col max-h-[90vh]"><div class="p-5 border-b border-darkborder flex justify-between items-center bg-darkbg"><h2 class="text-xl font-bold text-white flex items-center gap-2"><span id="graphStatusDot" class="w-2 h-2 rounded-full bg-green-500 animate-pulse"></span><span id="graphModalTitle">Performance</span></h2><button onclick="closeLiveGraph()" class="text-gray-400 hover:text-white transition-colors text-2xl">✕</button></div><div class="flex border-b border-darkborder bg-darkbg px-5 pt-4"><button onclick="switchGraphTab('live')" id="btn-graph-live" class="px-4 py-2 border-b-2 border-proxmox text-white font-bold">Live</button><button onclick="switchGraphTab('day')" id="btn-graph-day" class="px-4 py-2 border-b-2 border-transparent text-gray-400 hover:text-white font-bold transition-colors">24 Stunden</button><button onclick="switchGraphTab('week')" id="btn-graph-week" class="px-4 py-2 border-b-2 border-transparent text-gray-400 hover:text-white font-bold transition-colors">7 Tage</button></div><div class="p-6 bg-darkbg overflow-y-auto"><div class="grid grid-cols-1 lg:grid-cols-2 gap-6"><div class="bg-darkcard border border-darkborder rounded-xl p-4 shadow-lg"><h3 class="text-white font-bold mb-3 text-sm">CPU & RAM (%)</h3><div class="relative w-full h-[250px]"><canvas id="perfChart"></canvas></div></div><div class="bg-darkcard border border-darkborder rounded-xl p-4 shadow-lg"><h3 class="text-white font-bold mb-3 text-sm">Netzwerk Traffic</h3><div class="relative w-full h-[250px]"><canvas id="netChart"></canvas></div></div></div><p id="graphSubText" class="text-center text-xs text-gray-500 mt-4">Metriken werden live abgefragt.</p></div></div></div>
<div id="pbsDatastoreModal" class="fixed inset-0 bg-black/80 hidden z-[70] flex items-center justify-center backdrop-blur-sm"><div class="bg-darkcard border border-darkborder rounded-xl shadow-2xl w-full max-w-7xl overflow-hidden flex flex-col max-h-[95vh]"><div class="p-5 border-b border-darkborder flex justify-between items-center bg-darkbg"><h2 class="text-xl font-bold text-white flex items-center gap-2"><svg class="w-6 h-6 text-pbs" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7v8a2 2 0 002 2h6M8 7V5a2 2 0 012-2h4.586a1 1 0 01.707.293l4.414 4.414a1 1 0 01.293.707V15a2 2 0 01-2 2h-2M8 7H6a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2v-2"></path></svg> <span id="pbsModalTitle">Datastore</span> <button onclick="openLiveGraph('node', 0, 'localhost', document.getElementById('pbsNodeId').value, 'PBS Server')" class="ml-4 text-xs bg-darkcard border border-darkborder hover:border-pbs text-white px-3 py-1.5 rounded transition-colors">📈 Performance Historie</button></h2><button onclick="closePbsDatastore()" class="text-gray-400 hover:text-white transition-colors text-2xl">✕</button></div><div class="p-6 overflow-y-auto space-y-6 flex-1 bg-darkbg"><input type="hidden" id="pbsNodeId"><input type="hidden" id="pbsStoreName"><div class="grid grid-cols-1 xl:grid-cols-3 gap-6"><div class="bg-darkcard border border-darkborder rounded-xl p-5 shadow-lg flex flex-col h-[350px]"><h3 class="text-white font-bold mb-4 flex justify-between items-center">🗄️ Gespeicherte Backups</h3><div id="pbsBackupsContainer" class="space-y-2 overflow-y-auto flex-1 pr-2">Lade...</div></div><div class="bg-darkcard border border-darkborder rounded-xl p-5 shadow-lg flex flex-col h-[350px]"><h3 class="text-white font-bold mb-4">🧹 Verify & Garbage Collection</h3><div id="pbsVerifyGcContainer" class="space-y-2 overflow-y-auto flex-1 pr-2">Lade...</div></div><div class="bg-darkcard border border-darkborder rounded-xl p-5 shadow-lg flex flex-col h-[350px] border-l-4 border-l-blue-500"><h3 class="text-white font-bold mb-4">🔄 Eingerichtete Sync Jobs</h3><div id="pbsSyncContainer" class="space-y-2 overflow-y-auto flex-1 pr-2">Lade...</div></div></div><div class="bg-darkcard border border-darkborder rounded-xl p-5 shadow-lg mt-6"><h3 class="text-white font-bold mb-4">📋 Letzte 100 PBS Jobs (Historie)</h3><div class="max-h-60 overflow-y-auto pr-2"><table class="w-full text-left text-sm text-gray-400"><thead class="bg-darkbg border-b border-darkborder text-xs uppercase sticky top-0"><tr><th class="px-4 py-2">Start</th><th class="px-4 py-2">Job Typ</th><th class="px-4 py-2">Status</th><th class="px-4 py-2">Dauer</th><th class="px-4 py-2 text-right">Log</th></tr></thead><tbody id="pbsJobsTable" class="divide-y divide-darkborder"></tbody></table></div></div></div></div></div>
<div id="taskLogModal" class="fixed inset-0 bg-black/90 hidden z-[100] flex items-center justify-center backdrop-blur-sm"><div class="bg-darkcard border border-darkborder rounded-xl shadow-2xl w-full max-w-5xl overflow-hidden flex flex-col h-[85vh]"><div class="p-4 border-b border-darkborder flex justify-between items-center bg-darkbg"><h2 class="text-lg font-bold text-white flex items-center gap-2">📄 Task Log Viewer</h2><button onclick="closeTaskLog()" class="text-gray-400 hover:text-white transition-colors text-2xl">✕</button></div><div class="p-4 bg-black flex-1 overflow-y-auto"><pre id="taskLogContent" class="text-xs text-green-400 font-mono whitespace-pre-wrap">Lade Log...</pre></div></div></div>
<div id="pmgManagerModal" class="fixed inset-0 bg-black/80 hidden z-[75] flex items-center justify-center backdrop-blur-sm"><div class="bg-darkcard border border-darkborder rounded-xl shadow-2xl w-full max-w-7xl overflow-hidden flex flex-col h-[95vh]"><div class="p-5 border-b border-darkborder flex justify-between items-center bg-darkbg"><h2 class="text-xl font-bold text-white flex items-center gap-2"><svg class="w-6 h-6 text-pmg" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path></svg> <span id="pmgModalTitle">Mail Gateway</span> <button onclick="openLiveGraph('node', 0, document.getElementById('pmgInternalName').value, document.getElementById('pmgNodeId').value, document.getElementById('pmgHostName').value)" class="ml-4 text-xs bg-darkcard border border-darkborder hover:border-pmg text-white px-3 py-1.5 rounded transition-colors">📈 Performance Historie</button></h2><button onclick="closePmgManager()" class="text-gray-400 hover:text-white transition-colors text-2xl">✕</button></div><div class="flex border-b border-darkborder bg-darkbg px-5 pt-4"><button onclick="switchPmgTab('spam')" id="btn-pmg-spam" class="px-4 py-2 border-b-2 border-pmg text-white font-bold">🛡️ Quarantäne</button><button onclick="switchPmgTab('tracking')" id="btn-pmg-tracking" class="px-4 py-2 border-b-2 border-transparent text-gray-400 hover:text-white font-bold transition-colors">🔎 Tracking Center</button><button onclick="switchPmgTab('queues')" id="btn-pmg-queues" class="px-4 py-2 border-b-2 border-transparent text-gray-400 hover:text-white font-bold transition-colors">📨 Mail Queues</button><button onclick="switchPmgTab('settings')" id="btn-pmg-settings" class="px-4 py-2 border-b-2 border-transparent text-gray-400 hover:text-white font-bold transition-colors ml-auto">⚙️ Einstellungen</button></div><div class="p-6 overflow-y-auto flex-1 bg-darkbg"><input type="hidden" id="pmgNodeId"><input type="hidden" id="pmgHostName"><input type="hidden" id="pmgInternalName"><div id="pmg-tab-spam" class="h-full flex flex-col"><div class="flex justify-between items-center mb-4"><h3 class="text-white font-bold">Zurückgehaltene Mails</h3><button onclick="loadPmgSpamQueue(document.getElementById('pmgNodeId').value)" class="text-xs bg-darkcard border border-darkborder hover:border-pmg text-white px-3 py-1.5 rounded transition-colors">🔄 Aktualisieren</button></div><div class="overflow-y-auto flex-1 bg-darkcard border border-darkborder rounded-lg relative"><table class="w-full text-left text-sm text-gray-400"><thead class="bg-darkbg border-b border-darkborder text-xs uppercase sticky top-0 z-10"><tr><th class="px-4 py-3">Datum</th><th class="px-4 py-3">Absender</th><th class="px-4 py-3">Empfänger</th><th class="px-4 py-3">Betreff</th><th class="px-4 py-3 text-center">Score</th><th class="px-4 py-3 text-right">Aktion</th></tr></thead><tbody id="pmgSpamTable" class="divide-y divide-darkborder"></tbody></table></div></div><div id="pmg-tab-tracking" class="hidden h-full flex flex-col"><div class="flex gap-4 mb-4 items-end"><div class="flex-1"><label class="block text-xs font-medium text-gray-500 mb-1">Nach E-Mail Adresse filtern (Letzte 24h)</label><input type="text" id="pmgTrackingFilter" placeholder="z.B. spammer@evil.com oder user@firma.de" class="w-full bg-darkcard border border-darkborder rounded p-2 text-white text-sm" onkeydown="if(event.key === 'Enter') loadPmgTracking()"></div><button onclick="loadPmgTracking()" class="bg-pmg hover:bg-blue-600 text-white font-bold py-2 px-6 rounded text-sm transition-colors">Suchen</button></div><div class="overflow-y-auto flex-1 bg-darkcard border border-darkborder rounded-lg relative"><table class="w-full text-left text-sm text-gray-400"><thead class="bg-darkbg border-b border-darkborder text-xs uppercase sticky top-0 z-10"><tr><th class="px-4 py-3">Zeit</th><th class="px-4 py-3">Absender</th><th class="px-4 py-3">Empfänger</th><th class="px-4 py-3">Status</th></tr></thead><tbody id="pmgTrackingTable" class="divide-y divide-darkborder"></tbody></table></div></div><div id="pmg-tab-queues" class="hidden h-full flex flex-col"><div class="flex justify-between items-center mb-4"><h3 class="text-white font-bold">Aktuelle Postfix Warteschlangen</h3><button onclick="loadPmgQueues()" class="text-xs bg-darkcard border border-darkborder hover:border-pmg text-white px-3 py-1.5 rounded transition-colors">🔄 Aktualisieren</button></div><div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6" id="pmgQueuesSummary"><div class="bg-darkcard border border-darkborder rounded-xl p-5 text-center animate-pulse"><p class="text-gray-500">Lade...</p></div></div><div class="overflow-y-auto flex-1 bg-darkcard border border-darkborder rounded-lg relative"><table class="w-full text-left text-sm text-gray-400"><thead class="bg-darkbg Line truncated
<div id="updateManagerModal" class="fixed inset-0 bg-black/80 hidden z-[85] flex items-center justify-center backdrop-blur-sm"><div class="bg-darkcard border border-darkborder rounded-xl shadow-2xl w-full max-w-5xl overflow-hidden flex flex-col max-h-[90vh]"><div class="p-5 border-b border-darkborder flex justify-between items-center bg-darkbg"><h2 class="text-xl font-bold text-white flex items-center gap-2">📦 System Updates (APT)</h2><button onclick="closeUpdateManager()" class="text-gray-400 hover:text-white transition-colors text-2xl">✕</button></div><div class="p-6 overflow-y-auto flex-1 bg-darkbg" id="updateManagerContent"><div class="text-center py-10 text-gray-500 animate-pulse">Prüfe alle Nodes auf Paket-Updates...</div></div></div></div>
<div id="cronManagerModal" class="fixed inset-0 bg-black/80 hidden z-50 flex items-center justify-center backdrop-blur-sm"><div class="bg-darkcard border border-darkborder rounded-xl shadow-2xl w-full max-w-5xl overflow-hidden flex flex-col max-h-[90vh]"><div class="p-5 border-b border-darkborder flex justify-between items-center bg-darkbg"><h2 class="text-xl font-bold text-white flex items-center gap-2">⏱️ Task Scheduler & Automatisierung</h2><button onclick="closeCronManager()" class="text-gray-400 hover:text-white transition-colors">✕</button></div><div class="p-6 overflow-y-auto flex-1 flex flex-col gap-6 bg-darkbg"><div class="bg-darkcard p-5 rounded-xl border border-darkborder shadow-lg"><h3 class="text-white font-bold mb-4 border-b border-darkborder pb-2">Neuen Cron-Job anlegen</h3><form id="addCronJobForm" class="space-y-4"><div class="grid grid-cols-1 md:grid-cols-2 gap-4"><div><label class="block text-xs font-medium text-gray-500 mb-1">Name / Beschreibung</label><input type="text" id="cronName" class="w-full bg-darkbg border border-darkborder rounded p-2 text-white text-sm" placeholder="z.B. Nächtlicher Reboot" required></div><div><label class="block text-xs font-medium text-gray-500 mb-1">Ziel-Host (PVE)</label><select id="cronNodeId" class="w-full bg-darkbg border border-darkborder rounded p-2 text-white text-sm" required><option value="">Lade Hosts...</option></select></div></div><div class="grid grid-cols-1 md:grid-cols-4 gap-4"><div><label class="block text-xs font-medium text-gray-500 mb-1">Aktion</label><select id="cronAction" onchange="checkCronAction()" class="w-full bg-darkbg border border-darkborder rounded p-2 text-white text-sm" required><option value="reboot_node">Ganzen Server neustarten</option><option value="start_vm">VM Starten</option><option value="stop_vm">VM Stoppen</option><option value="reboot_vm">VM Neustarten</option></select></div><div id="cronVmBlock" class="hidden"><label class="block text-xs font-medium text-gray-500 mb-1">Ziel VM-ID</label><input type="number" id="cronTargetVmid" class="w-full bg-darkbg border border-darkborder rounded p-2 text-white text-sm" placeholder="z.B. 100"></div><div><label class="block text-xs font-medium text-gray-500 mb-1">Uhrzeit</label><input type="time" id="cronTime" value="03:00" class="w-full bg-darkbg border border-darkborder rounded p-2 text-white text-sm" required></div><div><label class="block text-xs font-medium text-gray-500 mb-1">Tage / Datum</label><select id="cronDays" onchange="toggleCronDate()" class="w-full bg-darkbg border border-darkborder rounded p-2 text-white text-sm" required><option value="*">Jeden Tag</option><option value="1-5">Montag - Freitag</option><option value="6,0">Wochenende</option><option value="1">Jeden Montag</option><option value="specific">📅 Einmalig (Bestimmtes Datum)</option></select><input type="date" id="cronSpecificDate" class="w-full bg-darkbg border border-darkborder rounded p-2 text-white text-sm mt-2 hidden"></div></div><button type="submit" class="bg-proxmox hover:bg-orange-600 text-white font-bold py-2 px-6 rounded transition-colors text-sm w-full md:w-auto">Job Speichern</button></form></div><div class="bg-darkcard border border-darkborder rounded-xl shadow-lg flex-1 overflow-hidden flex flex-col"><table class="w-full text-left text-sm text-gray-400"><thead class="bg-darkbg border-b border-darkborder text-xs uppercase sticky top-0"><tr><th class="px-4 py-3">Status</th><th class="px-4 py-3">Beschreibung</th><th class="px-4 py-3">Host / Ziel</th><th class="px-4 py-3">Cron</th><th class="px-4 py-3">Letzter Lauf</th><th class="px-4 py-3 text-right">Löschen</th></tr></thead><tbody id="cronTableBody" class="divide-y divide-darkborder"></tbody></table></div></div></div></div>
<div id="auditLogModal" class="fixed inset-0 bg-black/80 hidden z-[100] flex items-center justify-center backdrop-blur-sm"><div class="bg-darkcard border border-darkborder rounded-xl shadow-2xl w-full max-w-5xl overflow-hidden flex flex-col max-h-[90vh]"><div class="p-5 border-b border-darkborder flex justify-between items-center bg-darkbg"><h2 class="text-xl font-bold text-white flex items-center gap-2"><svg class="w-6 h-6 text-proxmox" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path></svg> Aktivitäten-Protokoll (Audit Log)</h2><button onclick="closeAuditLog()" class="text-gray-400 hover:text-white transition-colors">✕</button></div><div class="p-6 overflow-y-auto flex-1 bg-darkbg"><div class="bg-darkcard border border-darkborder rounded-xl shadow-lg flex-1 overflow-hidden flex flex-col"><table class="w-full text-left text-sm text-gray-400"><thead class="bg-darkbg border-b border-darkborder text-xs uppercase sticky top-0"><tr><th class="px-4 py-3">Datum / Uhrzeit</th><th class="px-4 py-3">Benutzer</th><th class="px-4 py-3">Aktion</th><th class="px-4 py-3">Ziel / Details</th></tr></thead><tbody id="auditLogTableBody" class="divide-y divide-darkborder"></tbody></table></div></div></div></div>
<div id="passwordModal" class="fixed inset-0 bg-black/80 hidden z-[100] flex items-center justify-center backdrop-blur-sm"><div class="bg-darkcard border border-darkborder rounded-xl shadow-2xl w-full max-w-sm overflow-hidden flex flex-col"><div class="p-5 border-b border-darkborder flex justify-between items-center bg-darkbg"><h2 class="text-lg font-bold text-white flex items-center gap-2">🔑 Passwort ändern</h2><button onclick="closePasswordModal()" class="text-gray-400 hover:text-white transition-colors text-xl">✕</button></div><div class="p-6"><form id="changePasswordForm" class="space-y-4"><div><label class="block text-xs font-medium text-gray-500 mb-1">Aktuelles Passwort</label><input type="password" id="oldPassword" class="w-full bg-darkbg border border-darkborder rounded p-2 text-white text-sm focus:border-proxmox" required></div><div><label class="block text-xs font-medium text-gray-500 mb-1">Neues Passwort</label><input type="password" id="newPassword" class="w-full bg-darkbg border border-darkborder rounded p-2 text-white text-sm focus:border-proxmox" required></div><div><label class="block text-xs font-medium text-gray-500 mb-1">Neues Passwort bestätigen</label><input type="password" id="newPasswordConfirm" class="w-full bg-darkbg border border-darkborder rounded p-2 text-white text-sm focus:border-proxmox" required></div><button type="submit" class="w-full bg-proxmox hover:bg-orange-600 text-white font-bold py-2.5 rounded mt-2 transition-colors">Passwort aktualisieren</button></form></div></div></div>