Files
bordervillegame/admin_metrics.html
2026-08-22 08:40:29 +02:00

198 lines
6.8 KiB
HTML

<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Server-Metriken</title>
<style>
body {
margin: 0;
background: #111;
color: #eee;
font-family: Arial, sans-serif;
padding: 20px;
}
h1 { margin-top: 0; }
a.back { color: #6fbf73; text-decoration: none; font-size: 14px; }
a.back:hover { text-decoration: underline; }
.stat-row { display: flex; gap: 14px; flex-wrap: wrap; margin: 16px 0; }
.stat-card {
background: #1a1a1a; border: 1px solid #333; border-radius: 8px;
padding: 14px 18px; min-width: 140px;
}
.stat-card .label { color: #888; font-size: 12px; margin-bottom: 4px; }
.stat-card .value { font-size: 24px; font-weight: bold; }
.stat-card .value.warn { color: #e0a83a; }
.stat-card .value.bad { color: #e06c6c; }
.stat-card .value.good { color: #6fbf73; }
.panel {
background: #1a1a1a; border: 1px solid #333; border-radius: 8px;
padding: 16px; margin-top: 16px; max-width: 900px;
}
.panel h3 { margin-top: 0; color: #ccc; font-size: 15px; }
canvas { display: block; background: #141414; border-radius: 4px; }
button { background: #2c7a3d; border: none; color: white; padding: 8px 14px; border-radius: 4px; cursor: pointer; font-size: 13px; }
button:hover { opacity: 0.85; }
.refresh-row { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
#lastUpdate { color: #666; font-size: 12px; }
</style>
</head>
<body>
<a class="back" href="/uebersicht.html">&larr; zurück zur Übersicht</a>
<h1>📊 Server-Metriken</h1>
<div class="refresh-row">
<button onclick="loadMetrics()">Jetzt aktualisieren</button>
<span id="lastUpdate"></span>
</div>
<div class="stat-row">
<div class="stat-card">
<div class="label">Spieler online</div>
<div class="value" id="statPlayers">-</div>
</div>
<div class="stat-card">
<div class="label">Fahrzeuge geladen</div>
<div class="value" id="statCars">-</div>
</div>
<div class="stat-card">
<div class="label">Physik-Tick</div>
<div class="value" id="statTick">-</div>
</div>
<div class="stat-card">
<div class="label">Server-Laufzeit</div>
<div class="value" id="statUptime">-</div>
</div>
</div>
<div class="panel">
<h3>Spieler online (letzte 24h)</h3>
<canvas id="chartPlayers" width="860" height="180"></canvas>
</div>
<div class="panel">
<h3>Physik-Tick-Dauer in ms (letzte 24h) - sollte deutlich unter 50ms bleiben</h3>
<canvas id="chartTick" width="860" height="180"></canvas>
</div>
<div class="panel">
<h3>Arbeitsspeicher in MB (letzte 24h)</h3>
<canvas id="chartRam" width="860" height="180"></canvas>
</div>
<script>
const token = localStorage.getItem("token");
if (!token) {
alert("Bitte zuerst einloggen.");
location.href = "/index.html";
}
async function authFetch(url, options = {}) {
options.headers = { ...(options.headers || {}), "Authorization": "Bearer " + token };
const res = await fetch(url, options);
if (res.status === 401 || res.status === 403) {
alert("Sitzung abgelaufen oder keine Berechtigung. Bitte erneut einloggen.");
location.href = "/index.html";
throw new Error("Nicht autorisiert");
}
return res;
}
function formatUptime(seconds) {
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
return `${h}h ${m}m`;
}
function drawLineChart(canvasId, points, options = {}) {
const canvas = document.getElementById(canvasId);
const ctx = canvas.getContext("2d");
const w = canvas.width;
const h = canvas.height;
const padding = { top: 10, right: 10, bottom: 20, left: 40 };
ctx.clearRect(0, 0, w, h);
if (points.length < 2) {
ctx.fillStyle = "#666";
ctx.font = "13px Arial";
ctx.fillText("Noch nicht genug Daten (Punkte sammeln sich alle 60s)...", 12, h / 2);
return;
}
const values = points.map(p => p.v);
const maxV = options.maxOverride || Math.max(...values, 1) * 1.15;
const minV = 0;
const plotW = w - padding.left - padding.right;
const plotH = h - padding.top - padding.bottom;
// Gitterlinien + Achsenbeschriftung
ctx.strokeStyle = "#2a2a2a";
ctx.fillStyle = "#666";
ctx.font = "10px Arial";
for (let i = 0; i <= 4; i++) {
const y = padding.top + plotH - (i / 4) * plotH;
ctx.beginPath();
ctx.moveTo(padding.left, y);
ctx.lineTo(w - padding.right, y);
ctx.stroke();
ctx.fillText(Math.round(maxV * i / 4), 4, y + 3);
}
// Linie zeichnen
ctx.strokeStyle = options.color || "#5b8def";
ctx.lineWidth = 2;
ctx.beginPath();
points.forEach((p, i) => {
const x = padding.left + (i / (points.length - 1)) * plotW;
const y = padding.top + plotH - (Math.min(p.v, maxV) / maxV) * plotH;
if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
});
ctx.stroke();
// Fläche unter der Linie leicht einfärben
ctx.lineTo(padding.left + plotW, padding.top + plotH);
ctx.lineTo(padding.left, padding.top + plotH);
ctx.closePath();
ctx.fillStyle = (options.color || "#5b8def") + "22";
ctx.fill();
// Zeitachse: erste/letzte/mittlere Uhrzeit anzeigen
ctx.fillStyle = "#666";
const first = new Date(points[0].t).toLocaleTimeString("de-DE", { hour: "2-digit", minute: "2-digit" });
const last = new Date(points[points.length - 1].t).toLocaleTimeString("de-DE", { hour: "2-digit", minute: "2-digit" });
ctx.fillText(first, padding.left, h - 4);
ctx.fillText(last, w - padding.right - 30, h - 4);
}
async function loadMetrics() {
const res = await authFetch("/api/admin/metrics");
const data = await res.json();
if (!data.ok) return;
document.getElementById("statPlayers").textContent = data.current.players;
document.getElementById("statCars").textContent = data.current.cars;
const tickEl = document.getElementById("statTick");
tickEl.textContent = data.current.tickMs + " ms";
tickEl.className = "value " + (data.current.tickMs > 40 ? "bad" : data.current.tickMs > 20 ? "warn" : "good");
document.getElementById("statUptime").textContent = formatUptime(data.current.uptimeSeconds);
const history = data.history || [];
drawLineChart("chartPlayers", history.map(p => ({ t: p.t, v: p.players })), { color: "#5b8def" });
drawLineChart("chartTick", history.map(p => ({ t: p.t, v: p.tickMs })), { color: "#e0a83a", maxOverride: 50 });
drawLineChart("chartRam", history.map(p => ({ t: p.t, v: p.ramMb })), { color: "#6fbf73" });
document.getElementById("lastUpdate").textContent = "Zuletzt aktualisiert: " + new Date().toLocaleTimeString("de-DE");
}
loadMetrics();
setInterval(loadMetrics, 30000); // alle 30s automatisch neu laden
</script>
</body>
</html>