initial commit
This commit is contained in:
@@ -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">100–299</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>
|
||||
Reference in New Issue
Block a user