198 lines
6.8 KiB
HTML
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">← 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>
|