Files
2026-07-22 01:12:45 +02:00

301 lines
10 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
require_once "config/session.php";
if (!isset($_SESSION["loggedin"]) || $_SESSION["loggedin"] !== true) {
header("location: index.php");
exit;
}
include('navbar.php');
?>
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>ANJUMA • Infrastructure</title>
<!-- Bootstrap 5 CSS -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">
<!-- DataTables (Bootstrap 5) CSS -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/datatables.net-bs5@1.13.10/css/dataTables.bootstrap5.min.css">
<!-- jQuery (muss vor DataTables) -->
<script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js"></script>
<!-- DataTables -->
<script src="https://cdn.jsdelivr.net/npm/datatables.net@1.13.10/js/jquery.dataTables.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/datatables.net-bs5@1.13.10/js/dataTables.bootstrap5.min.js"></script>
<!-- Bootstrap 5 JS (bundle enthält Popper) -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<style>
.badge-status { text-transform: uppercase; letter-spacing: .04em; }
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; }
.dt-nowrap td { white-space: nowrap; }
</style>
</head>
<body class="bg-light">
<div class="container-fluid py-4">
<div class="d-flex align-items-center justify-content-between mb-3">
<div>
<h3 class="mb-0">Infrastructure</h3>
<div class="text-muted">Proxmox Node: <span class="mono">pve</span> • ANJUMA-Range: <span class="mono">100299</span></div>
</div>
<div class="d-flex gap-2">
<button id="btnRefresh" class="btn btn-outline-secondary btn-sm">↻ Refresh</button>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="autoRefresh" checked>
<label class="form-check-label" for="autoRefresh">Auto-Refresh</label>
</div>
</div>
</div>
<div class="card shadow-sm">
<div class="card-body">
<table id="tblInfra" class="table table-striped table-hover align-middle dt-nowrap w-100">
<thead>
<tr>
<th>VMID</th>
<th>Typ</th>
<th>Name</th>
<th>Status</th>
<th>CPU</th>
<th>RAM</th>
<th>Uptime</th>
<th>Actions</th>
</tr>
</thead>
<tbody><!-- via JS --></tbody>
</table>
<div id="lastUpdate" class="text-muted small mt-2"></div>
</div>
</div>
</div>
<!-- Confirm Modal -->
<div class="modal fade" id="confirmModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="confirmTitle">Aktion bestätigen</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Schließen"></button>
</div>
<div class="modal-body" id="confirmBody"></div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Abbrechen</button>
<button type="button" class="btn btn-danger" id="confirmDo">Ausführen</button>
</div>
</div>
</div>
</div>
<!-- Toast -->
<div class="position-fixed bottom-0 end-0 p-3" style="z-index: 1080">
<div id="toast" class="toast align-items-center text-bg-dark border-0" role="alert" aria-live="assertive" aria-atomic="true">
<div class="d-flex">
<div class="toast-body" id="toastBody">...</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
</div>
</div>
<!-- JS -->
<script src="/assets/bootstrap.bundle.min.js"></script>
<script src="/assets/datatables/jquery-3.7.1.min.js"></script>
<script src="/assets/datatables/jquery.dataTables.min.js"></script>
<script src="/assets/datatables/dataTables.bootstrap5.min.js"></script>
<script>
const API_LIST = '/api/pve_list.php';
const API_ACTION = '/api/pve_action.php';
let dt;
let refreshTimer = null;
const toastEl = document.getElementById('toast');
const toast = new bootstrap.Toast(toastEl, { delay: 3500 });
function showToast(msg) {
document.getElementById('toastBody').textContent = msg;
toast.show();
}
function fmtType(t) {
return t === 'lxc'
? '<span class="badge text-bg-primary">LXC</span>'
: '<span class="badge text-bg-info">VM</span>';
}
function fmtStatus(s) {
const st = (s || 'unknown').toLowerCase();
if (st === 'running') return '<span class="badge text-bg-success badge-status">RUNNING</span>';
if (st === 'stopped') return '<span class="badge text-bg-secondary badge-status">STOPPED</span>';
return '<span class="badge text-bg-warning badge-status">UNKNOWN</span>';
}
function fmtCpu(cpu) {
// Proxmox liefert cpu oft als 0.x => grob in %
const p = Math.round((Number(cpu) || 0) * 100);
return `${p}%`;
}
function fmtRam(mem, maxmem) {
const m = Number(mem) || 0;
const mm = Number(maxmem) || 0;
if (!mm) return '-';
const used = (m / 1024 / 1024 / 1024);
const total = (mm / 1024 / 1024 / 1024);
return `${used.toFixed(2)} / ${total.toFixed(2)} GB`;
}
function fmtUptime(sec) {
sec = Number(sec) || 0;
if (sec <= 0) return '-';
const d = Math.floor(sec / 86400); sec %= 86400;
const h = Math.floor(sec / 3600); sec %= 3600;
const m = Math.floor(sec / 60);
if (d > 0) return `${d}d ${h}h`;
if (h > 0) return `${h}h ${m}m`;
return `${m}m`;
}
function actionsHtml(row) {
const vmid = row.vmid;
const status = (row.status || '').toLowerCase();
const disabledStart = status === 'running' ? 'disabled' : '';
const disabledStop = status === 'stopped' ? 'disabled' : '';
const disabledReboot = status === 'stopped' ? 'disabled' : '';
return `
<div class="btn-group btn-group-sm" role="group">
<button class="btn btn-outline-success act" data-action="start" data-vmid="${vmid}" ${disabledStart}>Start</button>
<button class="btn btn-outline-warning act" data-action="stop" data-vmid="${vmid}" ${disabledStop}>Stop</button>
<button class="btn btn-outline-danger act" data-action="reboot" data-vmid="${vmid}" ${disabledReboot}>Reboot</button>
</div>
`;
}
async function loadList() {
const res = await fetch(API_LIST, { cache: 'no-store' });
const json = await res.json();
if (!json.ok) throw new Error(json.error || 'List failed');
return json.items || [];
}
async function refreshTable() {
try {
const items = await loadList();
// DataTables: bestehende Rows ersetzen
dt.clear();
dt.rows.add(items);
dt.draw(false);
document.getElementById('lastUpdate').textContent =
'Letztes Update: ' + new Date().toLocaleTimeString();
} catch (e) {
showToast('Fehler beim Laden: ' + e.message);
}
}
function openConfirm(vmid, action, name) {
const title = `${action.toUpperCase()} • ${vmid}`;
const body = `
<div>Aktion <b>${action}</b> für <b>${name || 'VM/CT'}</b> (VMID <span class="mono">${vmid}</span>) ausführen?</div>
<div class="text-muted small mt-2">Tipp: Reboot ist “soft”, Stop ist “hart”.</div>
`;
document.getElementById('confirmTitle').textContent = title;
document.getElementById('confirmBody').innerHTML = body;
const btn = document.getElementById('confirmDo');
btn.className = 'btn ' + (action === 'stop' ? 'btn-warning' : action === 'start' ? 'btn-success' : 'btn-danger');
btn.textContent = 'Ja, ausführen';
btn.onclick = () => doAction(vmid, action);
new bootstrap.Modal(document.getElementById('confirmModal')).show();
}
async function doAction(vmid, action) {
try {
const fd = new FormData();
fd.append('vmid', vmid);
fd.append('action', action);
const res = await fetch(API_ACTION, { method: 'POST', body: fd });
const json = await res.json();
if (!json.ok) throw new Error(json.error || 'Action failed');
showToast(`OK: ${action} für ${vmid}` + (json.upid ? ` (UPID)` : ''));
// nach kurzer Zeit refreshen
setTimeout(refreshTable, 1200);
// modal schließen
bootstrap.Modal.getInstance(document.getElementById('confirmModal'))?.hide();
} catch (e) {
showToast('Fehler: ' + e.message);
}
}
document.addEventListener('click', (ev) => {
const btn = ev.target.closest('.act');
if (!btn) return;
const vmid = btn.dataset.vmid;
const action = btn.dataset.action;
// Name aus Tabelle holen
const row = dt.row($(btn).closest('tr')).data();
openConfirm(vmid, action, row?.name);
});
document.getElementById('btnRefresh').addEventListener('click', refreshTable);
function startAutoRefresh() {
stopAutoRefresh();
refreshTimer = setInterval(() => {
if (document.getElementById('autoRefresh').checked) refreshTable();
}, 5000);
}
function stopAutoRefresh() {
if (refreshTimer) clearInterval(refreshTimer);
refreshTimer = null;
}
document.getElementById('autoRefresh').addEventListener('change', (e) => {
if (e.target.checked) startAutoRefresh();
else stopAutoRefresh();
});
// Init
$(async function() {
dt = $('#tblInfra').DataTable({
pageLength: 25,
order: [[0, 'asc']],
columns: [
{ data: 'vmid', className: 'mono' },
{ data: 'type', render: (d) => fmtType(d) },
{ data: 'name' },
{ data: 'status', render: (d) => fmtStatus(d) },
{ data: 'cpu', render: (d) => fmtCpu(d) },
{ data: null, render: (row) => fmtRam(row.mem, row.maxmem) },
{ data: 'uptime', render: (d) => fmtUptime(d) },
{ data: null, orderable: false, searchable: false, render: (row) => actionsHtml(row) },
],
});
await refreshTable();
startAutoRefresh();
});
</script>
</body>
</html>