Release v1.2: Manageing Node Page.

This commit is contained in:
root committed 2026-09-10 08:01:36 +02:00
1 parent 5fcd306f5d
commit 487c49f6e7
2 files changed
+84 -48

No files matched your search

+33 -23
View File
@@ -37,7 +37,6 @@ if (window.APP.isLoggedIn && window.APP.nodeCount > 0) {
}); });
} }
// NEU: Dynamische Sidebar für PVE Nodes
async function loadSidebarNodes() { async function loadSidebarNodes() {
const container = document.getElementById('sidebar-pve-nodes'); if(!container) return; const container = document.getElementById('sidebar-pve-nodes'); if(!container) return;
try { try {
@@ -64,31 +63,39 @@ if (window.APP.isLoggedIn && window.APP.nodeCount > 0) {
if(nav) nav.classList.remove('tab-active', 'tab-active-pbs', 'tab-active-pmg'); if(nav) nav.classList.remove('tab-active', 'tab-active-pbs', 'tab-active-pmg');
} }
}); });
// Reset Sidebar Highlights for nodes
document.querySelectorAll('[id^="nav-node-"]').forEach(el => el.classList.remove('text-white', 'font-bold')); 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') fetchPbsStats(); if(tab === 'pmg') fetchPmgStats();
} }
// NEU: Node View (Spezifischer Host)
window.openNodeView = async function(nodeId, host) { window.openNodeView = async function(nodeId, host) {
switchTab('node-view'); switchTab('node-view');
document.querySelectorAll('[id^="nav-node-"]').forEach(el => el.classList.remove('text-white', 'font-bold')); document.querySelectorAll('[id^="nav-node-"]').forEach(el => el.classList.remove('text-white', 'font-bold'));
document.getElementById(`nav-node-${host}`).classList.add('text-white', 'font-bold'); const activeNav = document.getElementById(`nav-node-${host}`);
if(activeNav) activeNav.classList.add('text-white', 'font-bold');
document.getElementById('nodeViewTitle').innerText = 'Host: ' + host; document.getElementById('nodeViewTitle').innerText = 'Host: ' + host;
document.getElementById('nodeViewHeaderBtn').onclick = () => openLiveGraph('node', 0, host, nodeId, host);
document.getElementById('nodeViewBackupBtn').onclick = () => openNodeBackups(nodeId, host); // REPARIERT: EventListener sauber binden, nicht überschreiben
const headerBtn = document.getElementById('nodeViewHeaderBtn');
const backupBtn = document.getElementById('nodeViewBackupBtn');
// Remove old listeners by cloning
const newHeaderBtn = headerBtn.cloneNode(true);
headerBtn.parentNode.replaceChild(newHeaderBtn, headerBtn);
newHeaderBtn.addEventListener('click', () => openLiveGraph('node', 0, host, nodeId, host));
const newBackupBtn = backupBtn.cloneNode(true);
backupBtn.parentNode.replaceChild(newBackupBtn, backupBtn);
newBackupBtn.addEventListener('click', () => openNodeBackups(nodeId, host));
// Vorbelegung für "Neue VM" Modal
window.currentSelectedNodeId = nodeId; window.currentSelectedHost = host; window.currentSelectedNodeId = nodeId; window.currentSelectedHost = host;
const storContainer = document.getElementById('nodeStoragesContainer'); storContainer.innerHTML = 'Lade...'; 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">Lade VMs...</td></tr>'; 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>';
try { try {
const res = await (await fetch(`api.php?action=get_node_dashboard&node_id=${nodeId}&host=${host}`)).json(); const res = await (await fetch(`api.php?action=get_node_dashboard&node_id=${nodeId}&host=${host}`)).json();
if(res.success) { if(res.success) {
// Storages rendern
storContainer.innerHTML = ''; storContainer.innerHTML = '';
res.storages.forEach(st => { res.storages.forEach(st => {
const pct = st.total > 0 ? ((st.used / st.total) * 100).toFixed(1) : 0; const pct = st.total > 0 ? ((st.used / st.total) * 100).toFixed(1) : 0;
@@ -98,15 +105,14 @@ if (window.APP.isLoggedIn && window.APP.nodeCount > 0) {
<div class="text-xs text-gray-500">${st.content}</div></div>`; <div class="text-xs text-gray-500">${st.content}</div></div>`;
}); });
// VMs rendern
vmsBody.innerHTML = ''; 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>'; 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 => { res.vms.forEach(vm => {
const isRunning = vm.status === 'running'; const typeStr = vm.type === 'lxc' ? '📦 LXC' : '🖥️ QEMU'; 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>'; 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">⏹️</button>` : `<button onclick="sendVmCommand('${vm.vmid}', '${vm.host}', '${vm.type}', 'start', ${vm.node_id})" class="text-green-500 px-2">▶️</button>`; let actionButtons = isRunning ? `<button onclick="sendVmCommand('${vm.vmid}', '${vm.host}', '${vm.type}', 'stop', ${vm.node_id})" class="text-red-500 hover:text-red-400 px-2 text-lg transition-colors">⏹️</button>` : `<button onclick="sendVmCommand('${vm.vmid}', '${vm.host}', '${vm.type}', 'start', ${vm.node_id})" class="text-green-500 hover:text-green-400 px-2 text-lg transition-colors">▶️</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">⚙️</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 transition-colors">⚙️</button>`;
vmsBody.innerHTML += `<tr class="border-b border-darkborder/50 hover:bg-darkbg"><td class="py-2 px-3 text-white">${vm.vmid}</td><td class="py-2 px-3 text-white font-bold">${vm.name}</td><td class="py-2 px-3 text-gray-400">${typeStr}</td><td class="py-2 px-3">${statusBadge}</td><td class="py-2 px-3 text-right">${actionButtons}</td></tr>`; 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) {} } catch(e) {}
@@ -120,7 +126,7 @@ if (window.APP.isLoggedIn && window.APP.nodeCount > 0) {
let prevGlobalTime = null; let prevGlobalTime = null;
async function fetchGlobalStats() { async function fetchGlobalStats() {
if(document.getElementById('tab-pve').classList.contains('hidden')) return; if(document.getElementById('tab-pve') && document.getElementById('tab-pve').classList.contains('hidden')) return;
try { try {
const res = await (await fetch('api.php?action=get_stats')).json(); const res = await (await fetch('api.php?action=get_stats')).json();
if(res.success && res.data) { if(res.success && res.data) {
@@ -176,12 +182,12 @@ if (window.APP.isLoggedIn && window.APP.nodeCount > 0) {
window.closeNodeManager = function() { nodeModal.classList.add('hidden'); document.getElementById('addNodeFormContainer').classList.add('hidden'); } window.closeNodeManager = function() { nodeModal.classList.add('hidden'); document.getElementById('addNodeFormContainer').classList.add('hidden'); }
window.toggleAddNodeForm = function() { document.getElementById('addNodeFormContainer').classList.toggle('hidden'); } window.toggleAddNodeForm = function() { document.getElementById('addNodeFormContainer').classList.toggle('hidden'); }
async function loadNodesIntoTable() { try { const res = await (await fetch('api.php?action=get_nodes')).json(); if(res.success) { const tbody = document.getElementById('nodeTableBody'); tbody.innerHTML = ''; if(res.data.length === 0) { tbody.innerHTML = '<tr><td colspan="4" class="px-4 py-4 text-center">Keine Server.</td></tr>'; return; } res.data.forEach(node => { let typeBadge = node.type === 'pbs' ? '<span class="bg-purple-500/20 text-pbs px-2 py-0.5 rounded text-xs font-bold">PBS Backup</span>' : (node.type === 'pmg' ? '<span class="bg-blue-500/20 text-pmg px-2 py-0.5 rounded text-xs font-bold">PMG Mail</span>' : '<span class="bg-proxmox/20 text-proxmox px-2 py-0.5 rounded text-xs font-bold">PVE Node</span>'); tbody.innerHTML += `<tr class="hover:bg-darkcard/50 transition-colors"><td class="px-4 py-3 font-medium text-white">${node.name}</td><td class="px-4 py-3">${typeBadge}</td><td class="px-4 py-3">${node.ip_address}</td><td class="px-4 py-3 text-right"><button onclick="deleteNode(${node.id}, '${node.name}')" class="text-red-500 hover:text-red-400 text-sm font-medium">Löschen</button></td></tr>`; }); } } catch (err) {} } async function loadNodesIntoTable() { try { const res = await (await fetch('api.php?action=get_nodes')).json(); if(res.success) { const tbody = document.getElementById('nodeTableBody'); tbody.innerHTML = ''; if(res.data.length === 0) { tbody.innerHTML = '<tr><td colspan="4" class="px-4 py-4 text-center">Keine Server.</td></tr>'; return; } res.data.forEach(node => { let typeBadge = node.type === 'pbs' ? '<span class="bg-purple-500/20 text-pbs px-2 py-0.5 rounded text-xs font-bold">PBS Backup</span>' : (node.type === 'pmg' ? '<span class="bg-blue-500/20 text-pmg px-2 py-0.5 rounded text-xs font-bold">PMG Mail</span>' : '<span class="bg-proxmox/20 text-proxmox px-2 py-0.5 rounded text-xs font-bold">PVE Node</span>'); tbody.innerHTML += `<tr class="hover:bg-darkcard/50 transition-colors"><td class="px-4 py-3 font-medium text-white">${node.name}</td><td class="px-4 py-3">${typeBadge}</td><td class="px-4 py-3">${node.ip_address}</td><td class="px-4 py-3 text-right"><button onclick="deleteNode(${node.id}, '${node.name}')" class="text-red-500 hover:text-red-400 text-sm font-medium">Löschen</button></td></tr>`; }); } } catch (err) {} }
window.deleteNode = async function(id, name) { if(!confirm(`Server '${name}' löschen?`)) return; const fd = new FormData(); fd.append('id', id); await fetch('api.php?action=delete_node', { method: 'POST', body: fd }); loadNodesIntoTable(); } window.deleteNode = async function(id, name) { if(!confirm(`Server '${name}' löschen?`)) return; const fd = new FormData(); fd.append('id', id); await fetch('api.php?action=delete_node', { method: 'POST', body: fd }); loadNodesIntoTable(); loadSidebarNodes(); }
const addNewNodeForm = document.getElementById('addNewNodeForm'); const addNewNodeForm = document.getElementById('addNewNodeForm');
if(addNewNodeForm) { addNewNodeForm.addEventListener('submit', async function(e) { e.preventDefault(); const btn = this.querySelector('button[type="submit"]'); const oTxt = btn.innerText; btn.innerText = 'Verbinde...'; const fd = new FormData(); fd.append('name', document.getElementById('newNodeName').value); fd.append('ip', document.getElementById('newNodeIp').value); fd.append('user', document.getElementById('newNodeUser').value); fd.append('pass', document.getElementById('newNodePass').value); fd.append('type', document.getElementById('newNodeType').value); try { const res = await (await fetch('api.php?action=add_node', { method: 'POST', body: fd })).json(); if(res.success) { addNewNodeForm.reset(); toggleAddNodeForm(); loadNodesIntoTable(); loadSidebarNodes(); alert('Erfolgreich angebunden!'); } else alert(res.error); } catch(err) {} btn.innerText = oTxt; }); } if(addNewNodeForm) { addNewNodeForm.addEventListener('submit', async function(e) { e.preventDefault(); const btn = this.querySelector('button[type="submit"]'); const oTxt = btn.innerText; btn.innerText = 'Verbinde...'; const fd = new FormData(); fd.append('name', document.getElementById('newNodeName').value); fd.append('ip', document.getElementById('newNodeIp').value); fd.append('user', document.getElementById('newNodeUser').value); fd.append('pass', document.getElementById('newNodePass').value); fd.append('type', document.getElementById('newNodeType').value); try { const res = await (await fetch('api.php?action=add_node', { method: 'POST', body: fd })).json(); if(res.success) { addNewNodeForm.reset(); toggleAddNodeForm(); loadNodesIntoTable(); loadSidebarNodes(); alert('Erfolgreich angebunden!'); } else alert(res.error); } catch(err) {} btn.innerText = oTxt; }); }
// VM Erstellung (jetzt mit automatischer Node-Vorauswahl aus dem Sidebar-View) // VM Erstellung
const createVmModal = document.getElementById('createVmModal'); const createVmModal = document.getElementById('createVmModal');
window.openCreateVm = async function() { window.openCreateVm = async function() {
createVmModal.classList.remove('hidden'); createVmModal.classList.remove('hidden');
@@ -201,7 +207,7 @@ if (window.APP.isLoggedIn && window.APP.nodeCount > 0) {
const createVmForm = document.getElementById('createVmForm'); const createVmForm = document.getElementById('createVmForm');
if(createVmForm) { createVmForm.addEventListener('submit', async function(e) { e.preventDefault(); const hostDataStr = document.getElementById('createVmHost').value; if(!hostDataStr) return; const hostData = JSON.parse(hostDataStr); const btn = this.querySelector('button[type="submit"]'); const oTxt = btn.innerText; btn.innerText = 'Richte VM ein...'; const fd = new FormData(); fd.append('node_id', hostData.id); fd.append('host', hostData.host); fd.append('name', document.getElementById('createVmName').value); fd.append('memory', document.getElementById('createVmRam').value); fd.append('cores', document.getElementById('createVmCores').value); try { const res = await (await fetch('api.php?action=create_vm', { method: 'POST', body: fd })).json(); if(res.success) { alert(`Erfolgreich! VM-ID: ${res.vmid}`); createVmForm.reset(); closeCreateVm(); if(window.currentSelectedHost === hostData.host) openNodeView(hostData.id, hostData.host); } else alert(res.error); } catch(e) {} finally { btn.innerText = oTxt; } }); } if(createVmForm) { createVmForm.addEventListener('submit', async function(e) { e.preventDefault(); const hostDataStr = document.getElementById('createVmHost').value; if(!hostDataStr) return; const hostData = JSON.parse(hostDataStr); const btn = this.querySelector('button[type="submit"]'); const oTxt = btn.innerText; btn.innerText = 'Richte VM ein...'; const fd = new FormData(); fd.append('node_id', hostData.id); fd.append('host', hostData.host); fd.append('name', document.getElementById('createVmName').value); fd.append('memory', document.getElementById('createVmRam').value); fd.append('cores', document.getElementById('createVmCores').value); try { const res = await (await fetch('api.php?action=create_vm', { method: 'POST', body: fd })).json(); if(res.success) { alert(`Erfolgreich! VM-ID: ${res.vmid}`); createVmForm.reset(); closeCreateVm(); if(window.currentSelectedHost === hostData.host) openNodeView(hostData.id, hostData.host); } else alert(res.error); } catch(e) {} finally { btn.innerText = oTxt; } }); }
// VM EDIT MODAL (Erweitert um ISO & OnBoot) // VM EDIT MODAL
const settingsModal = document.getElementById('vmSettingsModal'); const settingsModal = document.getElementById('vmSettingsModal');
window.openVmSettings = async function(vmid, host, type, nodeId, name) { window.openVmSettings = async function(vmid, host, type, nodeId, name) {
settingsModal.classList.remove('hidden'); document.getElementById('settingsModalTitle').innerText = 'Einstellungen: ' + name; settingsModal.classList.remove('hidden'); document.getElementById('settingsModalTitle').innerText = 'Einstellungen: ' + name;
@@ -244,6 +250,8 @@ if (window.APP.isLoggedIn && window.APP.nodeCount > 0) {
const res = await (await fetch('api.php?action=update_vm_config', { method: 'POST', body: fd })).json(); const res = await (await fetch('api.php?action=update_vm_config', { method: 'POST', body: fd })).json();
if(res.success) { alert('Gespeichert!'); if(window.currentSelectedHost) openNodeView(fd.get('node_id'), fd.get('host')); } else alert(res.error); if(res.success) { alert('Gespeichert!'); if(window.currentSelectedHost) openNodeView(fd.get('node_id'), fd.get('host')); } else alert(res.error);
} }
window.saveNetwork = async function(disconnect) { let net = document.getElementById('setRawNet0').value; if (!net) return; if (disconnect) { if (!net.includes('link_down=1')) net += ',link_down=1'; } else { net = net.replace(',link_down=1', ''); } const fd = new FormData(); fd.append('vmid', document.getElementById('setVmid').value); fd.append('host', document.getElementById('setHost').value); fd.append('type', document.getElementById('setType').value); fd.append('node_id', document.getElementById('setNodeId').value); fd.append('net0', net); const res = await (await fetch('api.php?action=update_vm_config', { method: 'POST', body: fd })).json(); if(res.success) { alert('Netzwerkstatus geändert!'); openVmSettings(fd.get('vmid'), fd.get('host'), fd.get('type'), fd.get('node_id'), document.getElementById('settingsModalTitle').innerText.replace('Einstellungen: ', '')); } }
window.addNic = async function() { const bridge = document.getElementById('addNicBridge').value.trim(); if (!bridge) return; const fd = new FormData(); fd.append('vmid', document.getElementById('setVmid').value); fd.append('host', document.getElementById('setHost').value); fd.append('type', document.getElementById('setType').value); fd.append('node_id', document.getElementById('setNodeId').value); fd.append('bridge', bridge); try { const res = await (await fetch('api.php?action=add_vm_nic', { method: 'POST', body: fd })).json(); if(res.success) { alert(`Erfolgreich in Slot: ${res.slot}`); openVmSettings(fd.get('vmid'), fd.get('host'), fd.get('type'), fd.get('node_id'), document.getElementById('settingsModalTitle').innerText.replace('Einstellungen: ', '')); } else alert(res.error); } catch(e) {} }
// Node Backups Manager // Node Backups Manager
window.openNodeBackups = async function(nodeId, host) { window.openNodeBackups = async function(nodeId, host) {
@@ -261,7 +269,7 @@ if (window.APP.isLoggedIn && window.APP.nodeCount > 0) {
<td class="py-2 px-3 text-proxmox">${j.schedule || 'Manuell'}</td> <td class="py-2 px-3 text-proxmox">${j.schedule || 'Manuell'}</td>
<td class="py-2 px-3">${j.storage}</td> <td class="py-2 px-3">${j.storage}</td>
<td class="py-2 px-3 text-gray-400">${j.vmid || 'Alle VMs'}</td> <td class="py-2 px-3 text-gray-400">${j.vmid || 'Alle VMs'}</td>
<td class="py-2 px-3 text-right"><span class="text-xs bg-darkcard px-2 py-1 rounded">Read-Only via API</span></td> <td class="py-2 px-3 text-right"><span class="text-xs bg-darkcard px-2 py-1 rounded text-gray-500 border border-darkborder">Read-Only via API</span></td>
</tr>`; </tr>`;
}); });
} }
@@ -335,7 +343,9 @@ if (window.APP.isLoggedIn && window.APP.nodeCount > 0) {
window.sendVmCommand = async function(vmid, host, type, cmd, nodeId) { if(!confirm(`Maschine '${vmid}' wirklich ${cmd}?`)) return; const fd = new FormData(); fd.append('vmid', vmid); fd.append('host', host); fd.append('type', type); fd.append('cmd', cmd); fd.append('node_id', nodeId); try { const res = await (await fetch('api.php?action=vm_action', { method: 'POST', body: fd })).json(); if(res.success) { setTimeout(() => { if(window.currentSelectedHost === host) openNodeView(nodeId, host); }, 2000); } else alert(res.error); } catch (err) {} } window.sendVmCommand = async function(vmid, host, type, cmd, nodeId) { if(!confirm(`Maschine '${vmid}' wirklich ${cmd}?`)) return; const fd = new FormData(); fd.append('vmid', vmid); fd.append('host', host); fd.append('type', type); fd.append('cmd', cmd); fd.append('node_id', nodeId); try { const res = await (await fetch('api.php?action=vm_action', { method: 'POST', body: fd })).json(); if(res.success) { setTimeout(() => { if(window.currentSelectedHost === host) openNodeView(nodeId, host); }, 2000); } else alert(res.error); } catch (err) {} }
// Bestehende Dummy-Funktionen für andere Module der Übersichtlichkeit halber: // Platzhalter für Modals, die im Header aufgerufen werden
window.openPmgManager = function() {} // Siehe original code window.openUserManager = function() {}
window.openPbsDatastore = function() {} // Siehe original code window.openPmgManager = function() {}
window.openPbsDatastore = function() {}
window.openNodeTopology = function() {}
} }
+51 -25
View File
@@ -47,24 +47,23 @@ $nodeCount = $stmt->fetchColumn();
<main class="flex-grow p-6 flex items-center justify-center max-w-[1600px] w-full mx-auto"> <main class="flex-grow p-6 flex items-center justify-center max-w-[1600px] w-full mx-auto">
<?php if (!$isLoggedIn): ?> <?php if (!$isLoggedIn): ?>
<!-- Login Form (Unverändert) --> <!-- Login -->
<div class="bg-darkcard border border-darkborder rounded-xl shadow-2xl p-8 max-w-sm w-full"><h2 class="text-2xl font-bold mb-2 text-center text-white">Login</h2><form id="loginForm" class="space-y-4"><div><label class="block text-sm font-medium text-gray-400 mb-1">Benutzer</label><input type="text" id="loginUser" class="w-full bg-darkbg border border-darkborder rounded p-2.5 text-white" required></div><div><label class="block text-sm font-medium text-gray-400 mb-1">Passwort</label><input type="password" id="loginPass" class="w-full bg-darkbg border border-darkborder rounded p-2.5 text-white" required></div><button type="submit" class="w-full bg-proxmox hover:bg-orange-600 text-white font-bold py-3 px-4 rounded-lg mt-4">Einloggen</button></form></div> <div class="bg-darkcard border border-darkborder rounded-xl shadow-2xl p-8 max-w-sm w-full"><h2 class="text-2xl font-bold mb-2 text-center text-white">Login</h2><form id="loginForm" class="space-y-4"><div><label class="block text-sm font-medium text-gray-400 mb-1">Benutzer</label><input type="text" id="loginUser" class="w-full bg-darkbg border border-darkborder rounded p-2.5 text-white" required></div><div><label class="block text-sm font-medium text-gray-400 mb-1">Passwort</label><input type="password" id="loginPass" class="w-full bg-darkbg border border-darkborder rounded p-2.5 text-white" required></div><button type="submit" class="w-full bg-proxmox hover:bg-orange-600 text-white font-bold py-3 px-4 rounded-lg mt-4">Einloggen</button></form></div>
<?php elseif ($nodeCount == 0): ?> <?php elseif ($nodeCount == 0): ?>
<!-- Setup Form (Unverändert) --> <!-- Setup -->
<div class="bg-darkcard border border-darkborder rounded-xl shadow-2xl p-8 max-w-md w-full"><h2 class="text-2xl font-bold mb-2 text-white">Willkommen! 👋</h2><form id="setupForm" class="space-y-4"><div><label class="block text-sm font-medium text-gray-400 mb-1">Name</label><input type="text" id="nodeName" class="w-full bg-darkbg border border-darkborder rounded p-2.5 text-white" required></div><div><label class="block text-sm font-medium text-gray-400 mb-1">IP</label><input type="text" id="nodeIp" class="w-full bg-darkbg border border-darkborder rounded p-2.5 text-white" required></div><div><label class="block text-sm font-medium text-gray-400 mb-1">Benutzer</label><input type="text" id="nodeUser" value="root@pam" class="w-full bg-darkbg border border-darkborder rounded p-2.5 text-white" required></div><div><label class="block text-sm font-medium text-gray-400 mb-1">Passwort</label><input type="password" id="nodePass" class="w-full bg-darkbg border border-darkborder rounded p-2.5 text-white" required></div><button type="submit" class="w-full bg-proxmox text-white font-bold py-3 rounded-lg mt-6">Node verbinden</button></form></div> <div class="bg-darkcard border border-darkborder rounded-xl shadow-2xl p-8 max-w-md w-full"><h2 class="text-2xl font-bold mb-2 text-white">Willkommen! 👋</h2><form id="setupForm" class="space-y-4"><div><label class="block text-sm font-medium text-gray-400 mb-1">Name</label><input type="text" id="nodeName" class="w-full bg-darkbg border border-darkborder rounded p-2.5 text-white" required></div><div><label class="block text-sm font-medium text-gray-400 mb-1">IP</label><input type="text" id="nodeIp" class="w-full bg-darkbg border border-darkborder rounded p-2.5 text-white" required></div><div><label class="block text-sm font-medium text-gray-400 mb-1">Benutzer</label><input type="text" id="nodeUser" value="root@pam" class="w-full bg-darkbg border border-darkborder rounded p-2.5 text-white" required></div><div><label class="block text-sm font-medium text-gray-400 mb-1">Passwort</label><input type="password" id="nodePass" class="w-full bg-darkbg border border-darkborder rounded p-2.5 text-white" required></div><button type="submit" class="w-full bg-proxmox text-white font-bold py-3 rounded-lg mt-6">Node verbinden</button></form></div>
<?php else: ?> <?php else: ?>
<div class="w-full h-[85vh] flex bg-darkbg overflow-hidden rounded-xl border border-darkborder shadow-2xl"> <div class="w-full h-[85vh] flex bg-darkbg overflow-hidden rounded-xl border border-darkborder shadow-2xl">
<!-- SIDEBAR --> <!-- SIDEBAR -->
<aside class="w-64 bg-darkcard border-r border-darkborder flex flex-col"> <aside class="w-64 bg-darkcard border-r border-darkborder flex flex-col shrink-0">
<nav class="flex-1 overflow-y-auto space-y-2 py-4"> <nav class="flex-1 overflow-y-auto space-y-2 py-4">
<div class="px-4 mb-2"><span class="text-xs font-bold text-gray-500 uppercase tracking-wider">Cluster Übersicht</span></div> <div class="px-4 mb-2"><span class="text-xs font-bold text-gray-500 uppercase tracking-wider">Cluster Übersicht</span></div>
<a href="#" id="nav-tab-pve" onclick="switchTab('pve')" class="tab-active flex items-center gap-3 text-gray-400 hover:text-white px-3 py-2 rounded-r-lg transition-colors">📊 PVE Cluster</a> <a href="#" id="nav-tab-pve" onclick="switchTab('pve')" class="tab-active flex items-center gap-3 text-gray-400 hover:text-white px-3 py-2 rounded-r-lg transition-colors">📊 PVE Cluster</a>
<!-- NEU: DYNAMISCHE PVE NODES -->
<div class="mt-4 border-t border-darkborder pt-4"> <div class="mt-4 border-t border-darkborder pt-4">
<div class="px-4 mb-2"><span class="text-xs font-bold text-gray-500 uppercase tracking-wider">PVE Nodes</span></div> <div class="px-4 mb-2"><span class="text-xs font-bold text-gray-500 uppercase tracking-wider">PVE Nodes</span></div>
<div id="sidebar-pve-nodes" class="space-y-1"> <div id="sidebar-pve-nodes" class="space-y-1">
<!-- Nodes werden per JS geladen --> <!-- Nodes per JS -->
</div> </div>
</div> </div>
@@ -75,6 +74,8 @@ $nodeCount = $stmt->fetchColumn();
</div> </div>
</nav> </nav>
<div class="p-4 border-t border-darkborder space-y-2 text-sm"> <div class="p-4 border-t border-darkborder space-y-2 text-sm">
<a href="#" onclick="openNodeTopology()" class="block text-gray-400 hover:text-white">Cluster Topologie</a>
<a href="#" onclick="openVmManager()" class="block text-gray-400 hover:text-white">VMs verwalten</a>
<a href="#" onclick="openNodeManager()" class="block text-gray-400 hover:text-white">Server & API</a> <a href="#" onclick="openNodeManager()" class="block text-gray-400 hover:text-white">Server & API</a>
<?php if(($_SESSION['role'] ?? '') === 'admin'): ?> <?php if(($_SESSION['role'] ?? '') === 'admin'): ?>
<a href="#" onclick="openCronManager()" class="block text-gray-400 hover:text-white">Task Scheduler</a> <a href="#" onclick="openCronManager()" class="block text-gray-400 hover:text-white">Task Scheduler</a>
@@ -84,23 +85,42 @@ $nodeCount = $stmt->fetchColumn();
</aside> </aside>
<div class="flex-1 flex flex-col overflow-hidden relative"> <div class="flex-1 flex flex-col overflow-hidden relative">
<main class="absolute inset-0 overflow-x-hidden overflow-y-auto bg-darkbg p-6 w-full"> <main class="absolute inset-0 overflow-x-hidden overflow-y-auto bg-darkbg p-6 flex gap-6 w-full">
<!-- TAB 1: PVE CLUSTER (Global) --> <!-- TAB 1: PVE CLUSTER (Global) -->
<div id="tab-pve" class="flex flex-col min-w-0 transition-opacity duration-300"> <div id="tab-pve" class="flex-1 flex flex-col min-w-0 transition-opacity duration-300">
<!-- [Alter Dashboard Inhalt bleibt unverändert erhalten - siehe app.js für injection] --> <!-- Top Kachel Cluster Status -->
<div class="mb-6 bg-darkcard border border-darkborder rounded-xl p-6 shadow-lg flex flex-col md:flex-row justify-between items-center gap-6">
<div class="flex items-center gap-4">
<div class="p-3 bg-blue-500/10 rounded-xl"><svg class="w-8 h-8 text-blue-500" 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></div>
<div><h2 class="text-xl font-bold text-white">Gesamtübersicht</h2><p class="text-gray-400 text-sm">Cluster & Standalone Nodes</p></div>
</div>
<div class="flex flex-wrap justify-center gap-6 md:gap-12">
<div class="text-center"><p class="text-gray-400 text-xs font-bold uppercase mb-1">Server (Nodes)</p><p class="text-2xl font-bold text-white"><span id="stat-nodes-online" class="text-green-500">0</span><span class="text-gray-600 mx-1">/</span><span id="stat-nodes-total" class="text-gray-300">0</span></p></div>
<div class="hidden md:block w-px bg-darkborder"></div>
<div class="text-center"><p class="text-gray-400 text-xs font-bold uppercase mb-1">Total VMs/LXC</p><p class="text-2xl font-bold text-white" id="stat-vms-total">0</p></div>
<div class="text-center"><p class="text-gray-400 text-xs font-bold uppercase mb-1">Online</p><p class="text-2xl font-bold text-green-500" id="stat-vms-run">0</p></div>
<div class="text-center"><p class="text-gray-400 text-xs font-bold uppercase mb-1">Offline</p><p class="text-2xl font-bold text-red-500" id="stat-vms-stop">0</p></div>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-6"> <div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-6">
<div class="bg-darkcard border border-darkborder rounded-xl p-5 shadow-lg"><p class="text-gray-400 text-sm font-medium">Cluster CPU Cores</p><h3 id="stat-cpu-text" class="text-2xl font-bold text-white mt-1">Lade...</h3><div class="w-full bg-darkbg rounded-full h-2 mt-4"><div id="stat-cpu-bar" class="bg-blue-500 h-2 rounded-full" style="width: 0%"></div></div></div> <div class="bg-darkcard border border-darkborder rounded-xl p-5 shadow-lg"><p class="text-gray-400 text-sm font-medium">Cluster CPU Cores</p><h3 id="stat-cpu-text" class="text-2xl font-bold text-white mt-1">Lade...</h3><div class="w-full bg-darkbg rounded-full h-2 mt-4"><div id="stat-cpu-bar" class="bg-blue-500 h-2 rounded-full" style="width: 0%"></div></div></div>
<div class="bg-darkcard border border-darkborder rounded-xl p-5 shadow-lg"><p class="text-gray-400 text-sm font-medium">Globaler RAM</p><h3 id="stat-ram-text" class="text-2xl font-bold text-white mt-1">Lade...</h3><div class="w-full bg-darkbg rounded-full h-2 mt-4"><div id="stat-ram-bar" class="bg-proxmox h-2 rounded-full" style="width: 0%"></div></div></div> <div class="bg-darkcard border border-darkborder rounded-xl p-5 shadow-lg"><p class="text-gray-400 text-sm font-medium">Globaler RAM</p><h3 id="stat-ram-text" class="text-2xl font-bold text-white mt-1">Lade...</h3><div class="w-full bg-darkbg rounded-full h-2 mt-4"><div id="stat-ram-bar" class="bg-proxmox h-2 rounded-full" style="width: 0%"></div></div></div>
<div class="bg-darkcard border border-darkborder rounded-xl p-5 shadow-lg"><p class="text-gray-400 text-sm font-medium">Datacenter Storage</p><h3 id="stat-disk-text" class="text-2xl font-bold text-white mt-1">Lade...</h3><div class="w-full bg-darkbg rounded-full h-2 mt-4"><div id="stat-disk-bar" class="bg-emerald-500 h-2 rounded-full" style="width: 0%"></div></div></div> <div class="bg-darkcard border border-darkborder rounded-xl p-5 shadow-lg"><p class="text-gray-400 text-sm font-medium">Datacenter Storage</p><h3 id="stat-disk-text" class="text-2xl font-bold text-white mt-1">Lade...</h3><div class="w-full bg-darkbg rounded-full h-2 mt-4"><div id="stat-disk-bar" class="bg-emerald-500 h-2 rounded-full" style="width: 0%"></div></div></div>
<div onclick="openCreateVm()" class="bg-proxmox/10 border border-proxmox/30 hover:border-proxmox rounded-xl p-5 shadow-lg cursor-pointer flex flex-col items-center justify-center text-proxmox transition-colors"> <div onclick="openCreateVm()" class="bg-proxmox/10 border border-proxmox/30 hover:border-proxmox rounded-xl p-5 shadow-lg cursor-pointer flex flex-col items-center justify-center text-proxmox transition-colors"><span class="text-3xl font-bold mb-1">+</span><span class="font-bold">Neue VM anlegen</span></div>
<span class="text-3xl font-bold mb-1">+</span><span class="font-bold">Neue VM anlegen</span>
</div>
</div>
</div> </div>
<!-- NEU: TAB NODE VIEW --> <div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<div id="tab-node-view" class="hidden opacity-0 flex flex-col min-w-0 transition-opacity duration-300"> <div class="bg-darkcard border border-darkborder rounded-xl p-5 shadow-lg min-h-[300px] flex flex-col justify-center"><div class="flex justify-between items-center mb-4"><h3 class="text-white font-bold">Live Cluster Auslastung</h3><span class="text-xs text-gray-500 flex items-center gap-2"><span class="w-2 h-2 rounded-full bg-blue-500 animate-pulse"></span> CPU / RAM</span></div><div class="relative h-full w-full min-h-[220px]"><canvas id="liveChart"></canvas></div></div>
<div class="bg-darkcard border border-darkborder rounded-xl p-5 shadow-lg min-h-[300px] flex flex-col justify-center"><div class="flex justify-between items-center mb-4"><h3 class="text-white font-bold">Live Netzwerk Traffic</h3><span class="text-xs text-gray-500 flex items-center gap-2"><span class="w-2 h-2 rounded-full bg-emerald-500 animate-pulse"></span> MB/s Total pro Node</span></div><div class="relative h-full w-full min-h-[220px]"><canvas id="liveNetChart"></canvas></div></div>
</div>
<div class="bg-darkcard border border-darkborder rounded-xl p-5 shadow-lg"><h3 class="text-white font-bold mb-4">🔥 Top 5 Ressourcen-Fresser</h3><div id="top-vms-container" class="space-y-3"><p class="text-gray-400 text-sm">Lädt Live-Daten von Proxmox API...</p></div></div>
</div>
<!-- TAB NODE VIEW -->
<div id="tab-node-view" class="hidden opacity-0 flex-1 flex flex-col min-w-0 transition-opacity duration-300">
<div class="flex justify-between items-center border-b border-darkborder pb-4 mb-6"> <div class="flex justify-between items-center border-b border-darkborder pb-4 mb-6">
<h2 id="nodeViewTitle" class="text-2xl font-bold text-white">Host: ...</h2> <h2 id="nodeViewTitle" class="text-2xl font-bold text-white">Host: ...</h2>
<div class="space-x-3"> <div class="space-x-3">
@@ -111,20 +131,17 @@ $nodeCount = $stmt->fetchColumn();
</div> </div>
<div class="grid grid-cols-1 xl:grid-cols-4 gap-6"> <div class="grid grid-cols-1 xl:grid-cols-4 gap-6">
<!-- VMs auf dem Node -->
<div class="xl:col-span-3 bg-darkcard border border-darkborder rounded-xl shadow-lg flex flex-col h-[600px]"> <div class="xl:col-span-3 bg-darkcard border border-darkborder rounded-xl shadow-lg flex flex-col h-[600px]">
<div class="p-4 border-b border-darkborder"><h3 class="text-white font-bold">Laufende & Gestoppte Maschinen</h3></div> <div class="p-4 border-b border-darkborder"><h3 class="text-white font-bold">Laufende & Gestoppte Maschinen</h3></div>
<div class="overflow-y-auto flex-1"> <div class="overflow-y-auto flex-1">
<table class="w-full text-left text-sm text-gray-400"> <table class="w-full text-left text-sm text-gray-400">
<thead class="bg-darkbg text-xs uppercase sticky top-0 border-b border-darkborder"> <thead class="bg-darkbg text-xs uppercase sticky top-0 border-b border-darkborder z-10">
<tr><th class="py-2 px-3">ID</th><th class="py-2 px-3">Name</th><th class="py-2 px-3">Typ</th><th class="py-2 px-3">Status</th><th class="py-2 px-3 text-right">Aktionen</th></tr> <tr><th class="py-2 px-3">ID</th><th class="py-2 px-3">Name</th><th class="py-2 px-3">Typ</th><th class="py-2 px-3">Status</th><th class="py-2 px-3 text-right">Aktionen</th></tr>
</thead> </thead>
<tbody id="nodeVmsTableBody" class="divide-y divide-darkborder/50"></tbody> <tbody id="nodeVmsTableBody" class="divide-y divide-darkborder/50"></tbody>
</table> </table>
</div> </div>
</div> </div>
<!-- Storages -->
<div class="bg-darkcard border border-darkborder rounded-xl shadow-lg p-4 flex flex-col h-[600px]"> <div class="bg-darkcard border border-darkborder rounded-xl shadow-lg p-4 flex flex-col h-[600px]">
<h3 class="text-white font-bold mb-4 border-b border-darkborder pb-2">💾 Storages</h3> <h3 class="text-white font-bold mb-4 border-b border-darkborder pb-2">💾 Storages</h3>
<div id="nodeStoragesContainer" class="space-y-3 overflow-y-auto pr-2"></div> <div id="nodeStoragesContainer" class="space-y-3 overflow-y-auto pr-2"></div>
@@ -132,16 +149,25 @@ $nodeCount = $stmt->fetchColumn();
</div> </div>
</div> </div>
<!-- TAB PBS / PMG (Platzhalter für den bestehenden Code) --> <!-- TAB PBS -->
<div id="tab-pbs" class="hidden opacity-0 flex flex-col min-w-0 transition-opacity duration-300"> <div id="tab-pbs" class="hidden opacity-0 flex-1 flex flex-col min-w-0 transition-opacity duration-300">
<div class="mb-6 border-b border-darkborder pb-4"><h2 class="text-2xl font-bold text-white">Backup Datastores</h2></div> <div class="mb-6 border-b border-darkborder pb-4"><h2 class="text-2xl font-bold text-white">Backup Datastores</h2></div>
<div id="pbs-datastores-container" class="grid grid-cols-1 md:grid-cols-3 gap-6"></div> <div id="pbs-datastores-container" class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6"></div>
</div>
<div id="tab-pmg" class="hidden opacity-0 flex flex-col min-w-0 transition-opacity duration-300">
<div class="mb-6 border-b border-darkborder pb-4"><h2 class="text-2xl font-bold text-white">Mail Gateway Status</h2></div>
<div id="pmg-nodes-container" class="grid grid-cols-1 md:grid-cols-3 gap-6"></div>
</div> </div>
<!-- TAB PMG -->
<div id="tab-pmg" class="hidden opacity-0 flex-1 flex flex-col min-w-0 transition-opacity duration-300">
<div class="mb-6 border-b border-darkborder pb-4"><h2 class="text-2xl font-bold text-white">Mail Gateway Status</h2></div>
<div id="pmg-nodes-container" class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6"></div>
</div>
<!-- RECHTS: JOBS (Immer sichtbar, es sei denn das Fenster ist zu schmal) -->
<div class="w-80 shrink-0 flex flex-col hidden lg:flex">
<div class="bg-darkcard border border-darkborder rounded-xl p-5 shadow-lg flex-1 overflow-y-auto">
<h3 class="text-white font-bold mb-4 sticky top-0 bg-darkcard pb-2 border-b border-darkborder z-10">Letzte Jobs</h3>
<div id="recent-jobs-container" class="space-y-3"><p class="text-gray-400 text-sm">Lädt Job-Historie...</p></div>
</div>
</div>
</main> </main>
</div> </div>
</div> </div>