From 1e2d29566563a6c125f4ac326f1e99d8f0fc9d57 Mon Sep 17 00:00:00 2001 From: D4rkst3r Date: Sat, 1 Aug 2026 04:09:19 +0200 Subject: [PATCH] Erreichbarkeits-Balken auf der Server-Seite MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ein Strich pro Stunde ueber 7 Tage, gruen/gelb/rot je nachdem, ob in der Stunde alle, ein Teil oder keine Messung erreichbar war. Die Daten lagen laengst da: player_history haelt 7 Tage vor, die API schnitt aber auf 24 Stunden ab. Statt Rohdaten (ueber eine Woche rund 5000 Zeilen pro Server) fasst uptimeBuckets() in SQL stundenweise zusammen — hoechstens 168 Zeilen. - Stunden ohne Messung werden als Luecke gezeichnet, nicht als "online". Sonst saehe eine Bot-Auszeit aus wie ein einwandfrei laufender Server. - Die alte 24h-Uptime-Zahl neben dem Peak ist raus: zweimal dieselbe Groesse ueber verschiedene Zeitraeume verwirrt nur. - Tooltip zeigt Ortszeit. Die Eimer werden in UTC abgeglichen (SQLite rechnet so), angezeigt wird lokal — sonst stuende dort eine Stunde, die niemand wiedererkennt. Zwei Fehler beim Bauen gefunden und behoben: - .srv-card ist ein Grid-Item mit min-width:auto und wuchs durch die 168 Striche auf 724 statt 335 px, die Karte sprengte auf dem Handy das Raster. - "Uhr" stand hart deutsch im Tooltip. Co-Authored-By: Claude Fable 5 --- frontend/src/locales/de.js | 3 ++ frontend/src/locales/en.js | 3 ++ frontend/src/pages/Server.jsx | 82 ++++++++++++++++++++++++++++++----- frontend/src/style.css | 34 +++++++++++++++ src/db.js | 12 +++++ src/web/api.js | 5 ++- 6 files changed, 126 insertions(+), 13 deletions(-) diff --git a/frontend/src/locales/de.js b/frontend/src/locales/de.js index 3f08c14..c9f421b 100644 --- a/frontend/src/locales/de.js +++ b/frontend/src/locales/de.js @@ -124,6 +124,9 @@ export default { 'server.players24h': 'Spielerzahl · letzte 24h', 'server.peak': 'Peak', 'server.uptime': 'Uptime', + 'server.days7': 'vor 7 Tagen', + 'server.now': 'jetzt', + 'server.noData': 'keine Messung', 'server.connect': 'Connect', 'server.copyAddress': 'Adresse kopieren', 'server.copied': 'Kopiert!', diff --git a/frontend/src/locales/en.js b/frontend/src/locales/en.js index 5405edb..d014071 100644 --- a/frontend/src/locales/en.js +++ b/frontend/src/locales/en.js @@ -123,6 +123,9 @@ export default { 'server.players24h': 'Player count · last 24h', 'server.peak': 'Peak', 'server.uptime': 'Uptime', + 'server.days7': '7 days ago', + 'server.now': 'now', + 'server.noData': 'no reading', 'server.connect': 'Connect', 'server.copyAddress': 'Copy address', 'server.copied': 'Copied!', diff --git a/frontend/src/pages/Server.jsx b/frontend/src/pages/Server.jsx index ea86aa4..7340d5a 100644 --- a/frontend/src/pages/Server.jsx +++ b/frontend/src/pages/Server.jsx @@ -38,14 +38,73 @@ function HistoryChart({ history, max }) { ); } -/** Uptime (%) und Spieler-Peak aus den 24h-Samples */ +/** + * Erreichbarkeits-Balken: ein Strich pro Stunde der letzten 7 Tage. + * + * Der Server liefert nur Stunden, in denen wirklich gemessen wurde. Fehlende + * werden hier als Lücke gezeichnet statt als „online" — sonst sähe eine + * Bot-Auszeit aus wie ein einwandfrei laufender Server. + */ +function UptimeTrack({ buckets }) { + const { t } = useT(); + const { tag, datum } = useFmt(); + if (!buckets?.length) return null; + + const proStunde = new Map(buckets.map((b) => [b.stunde, b])); + const jetzt = new Date(); + jetzt.setMinutes(0, 0, 0); + + const striche = []; + let gemessen = 0; + let erreichbar = 0; + for (let i = 167; i >= 0; i--) { + const d = new Date(jetzt.getTime() - i * 3600000); + // Schlüssel wie strftime('%Y-%m-%dT%H') in SQLite — das rechnet in UTC, + // hier also bewusst auch. Angezeigt wird gleich die Ortszeit, sonst + // stünde im Tooltip eine Stunde, die niemand wiedererkennt. + const key = d.toISOString().slice(0, 13); + const b = proStunde.get(key); + let art = 'leer'; + if (b) { + gemessen += b.proben; + erreichbar += b.an; + art = b.an === b.proben ? 'gut' : b.an === 0 ? 'aus' : 'teil'; + } + striche.push({ key, zeit: d, art, b }); + } + + const quote = gemessen > 0 ? (erreichbar / gemessen) * 100 : null; + const titel = (s) => { + const wann = datum(s.zeit, { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' }); + return s.b + ? `${wann} — ${Math.round((s.b.an / s.b.proben) * 100)} %` + : `${wann} — ${t('server.noData')}`; + }; + + return ( +
+
+ {striche.map((s) => ( + + ))} +
+
+ {t('server.days7')} + {quote != null && ( + = 99 ? 'up-good' : quote >= 90 ? '' : 'up-bad'}> + {quote.toLocaleString(tag, { minimumFractionDigits: 1, maximumFractionDigits: 1 })} % + + )} + {t('server.now')} +
+
+ ); +} + +/** Spieler-Peak aus den 24h-Samples (Uptime kommt aus dem Balken oben) */ function historyStats(history) { if (history.length === 0) return null; - const online = history.filter((h) => h.online).length; - return { - uptime: Math.round((online / history.length) * 100), - peak: Math.max(0, ...history.map((h) => h.players ?? 0)), - }; + return { peak: Math.max(0, ...history.map((h) => h.players ?? 0)) }; } function ServerCard({ s }) { @@ -99,15 +158,14 @@ function ServerCard({ s }) { {stats && (
{t('server.players24h')} - - {t('server.peak')} {stats.peak} · {t('server.uptime')}{' '} - = 99 ? 'up-good' : stats.uptime >= 90 ? '' : 'up-bad'}> - {stats.uptime}% - - + {/* Uptime steht jetzt am Balken darunter — zweimal dieselbe + Zahl in verschiedenen Zeiträumen verwirrt nur. */} + {t('server.peak')} {stats.peak}
)} + + {(s.connect_url || s.address) && (
{s.connect_url && ( diff --git a/frontend/src/style.css b/frontend/src/style.css index 6875c94..3f85c9a 100644 --- a/frontend/src/style.css +++ b/frontend/src/style.css @@ -1736,6 +1736,40 @@ label.toggle { .srv-checked { font-family: var(--mono); font-size: .62rem; color: var(--muted2); margin-top: .8rem; } /* ── AUDIT-LOG ─────────────────────────────────────── */ +/* ── ERREICHBARKEITS-BALKEN ────────────────────────── */ +/* Ein Strich pro Stunde, 168 Stück. Auf schmalen Schirmen werden die Striche + dünner als ein Pixel — deshalb scrollt der Balken dort lieber, statt zu + Matsch zu verschmelzen. */ +/* Ohne das sprengt der Balken auf schmalen Schirmen die ganze Karte: Grid-Items + haben min-width:auto und schrumpfen nicht unter ihren Inhalt — 168 Striche à + 3 px sind rund 840 px, und die Karte wuchs auf 724 statt 335 px. */ +.srv-card { min-width: 0; } +.up-track-wrap { margin-top: .9rem; min-width: 0; } +.up-track { + display: flex; gap: 1px; align-items: stretch; + height: 30px; overflow-x: auto; overflow-y: hidden; + scrollbar-width: none; +} +.up-track::-webkit-scrollbar { display: none; } +.up-tick { + flex: 1 0 3px; min-width: 3px; border-radius: 1px; + background: var(--success, #3ba55d); + transition: transform .1s ease; +} +.up-tick:hover { transform: scaleY(1.15); } +.up-tick.teil { background: var(--neon); } +.up-tick.aus { background: #f23f43; } +/* Keine Messung ist kein „läuft" — deshalb sichtbar leer statt grün */ +.up-tick.leer { background: rgba(255, 255, 255, .07); } + +.up-track-foot { + display: flex; justify-content: space-between; align-items: baseline; + font-family: var(--mono); font-size: .62rem; letter-spacing: .08em; + color: var(--muted2); margin-top: .45rem; +} +.up-track-foot .up-good { color: var(--success, #3ba55d); } +.up-track-foot .up-bad { color: #f23f43; } + /* ── SPRACHUMSCHALTER ──────────────────────────────── */ .lang-switch { display: inline-flex; diff --git a/src/db.js b/src/db.js index 505703a..a97e578 100644 --- a/src/db.js +++ b/src/db.js @@ -695,9 +695,21 @@ const playerHistoryStmt = db.prepare(` WHERE server_id = ? AND ts >= datetime('now', ?) ORDER BY ts `); const prunePlayerHistoryStmt = db.prepare(`DELETE FROM player_history WHERE ts < datetime('now', '-7 days')`); +// Erreichbarkeit stundenweise zusammengefasst — für den Uptime-Balken. +// Die Rohdaten (alle 2 min) wären über eine Woche rund 5000 Zeilen pro Server; +// als Stundeneimer sind es höchstens 168 und die Seite bleibt schlank. +const uptimeBucketsStmt = db.prepare(` + SELECT strftime('%Y-%m-%dT%H', ts) AS stunde, + COUNT(*) AS proben, + SUM(online) AS an + FROM player_history + WHERE server_id = ? AND ts >= datetime('now', ?) + GROUP BY stunde ORDER BY stunde +`); export const recordPlayerSample = (serverId, online, players) => insertPlayerSample.run(serverId, online ? 1 : 0, players); export const playerHistory = (serverId, hours = 24) => playerHistoryStmt.all(serverId, `-${hours} hours`); +export const uptimeBuckets = (serverId, hours = 168) => uptimeBucketsStmt.all(serverId, `-${hours} hours`); export const prunePlayerHistory = () => prunePlayerHistoryStmt.run(); // Moderation: Verwarnungen + Sticky-Roles diff --git a/src/web/api.js b/src/web/api.js index aa32403..fd02b14 100644 --- a/src/web/api.js +++ b/src/web/api.js @@ -15,7 +15,7 @@ import { saveWebAdmin, webAdminScopes, listWebAdmins, deleteWebAdmin, saveTemplate, listTemplates, deleteTemplate, getLevelRow, isPlaytester, saveWish, toggleWishVote, myWishVotes, - logAudit, listAudit, playerHistory, deleteUserData, + logAudit, listAudit, playerHistory, uptimeBuckets, deleteUserData, saveSsoApp, listSsoApps, deleteSsoApp, getSsoApp, createService, updateService, listServices, deleteService, savePage, getPage, listPages, listPublishedPages, deletePage, @@ -182,6 +182,9 @@ export function registerApiRoutes(app, client) { ping: r?.ping ?? null, checkedAt: r?.checkedAt ?? null, history: playerHistory(s.id, 24), + // Erreichbarkeit der letzten 7 Tage, stundenweise. Länger geht + // nicht: prunePlayerHistory räumt alles Ältere weg. + uptime: uptimeBuckets(s.id, 168), }; }), }));