initial commit

This commit is contained in:
2026-07-22 01:12:45 +02:00
commit 124f2746ac
357 changed files with 159669 additions and 0 deletions
+300
View File
@@ -0,0 +1,300 @@
<?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>