Erster Commit
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
<!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>
|
||||
Reference in New Issue
Block a user