Erreichbarkeits-Balken auf der Server-Seite
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 <noreply@anthropic.com>
This commit is contained in:
@@ -124,6 +124,9 @@ export default {
|
|||||||
'server.players24h': 'Spielerzahl · letzte 24h',
|
'server.players24h': 'Spielerzahl · letzte 24h',
|
||||||
'server.peak': 'Peak',
|
'server.peak': 'Peak',
|
||||||
'server.uptime': 'Uptime',
|
'server.uptime': 'Uptime',
|
||||||
|
'server.days7': 'vor 7 Tagen',
|
||||||
|
'server.now': 'jetzt',
|
||||||
|
'server.noData': 'keine Messung',
|
||||||
'server.connect': 'Connect',
|
'server.connect': 'Connect',
|
||||||
'server.copyAddress': 'Adresse kopieren',
|
'server.copyAddress': 'Adresse kopieren',
|
||||||
'server.copied': 'Kopiert!',
|
'server.copied': 'Kopiert!',
|
||||||
|
|||||||
@@ -123,6 +123,9 @@ export default {
|
|||||||
'server.players24h': 'Player count · last 24h',
|
'server.players24h': 'Player count · last 24h',
|
||||||
'server.peak': 'Peak',
|
'server.peak': 'Peak',
|
||||||
'server.uptime': 'Uptime',
|
'server.uptime': 'Uptime',
|
||||||
|
'server.days7': '7 days ago',
|
||||||
|
'server.now': 'now',
|
||||||
|
'server.noData': 'no reading',
|
||||||
'server.connect': 'Connect',
|
'server.connect': 'Connect',
|
||||||
'server.copyAddress': 'Copy address',
|
'server.copyAddress': 'Copy address',
|
||||||
'server.copied': 'Copied!',
|
'server.copied': 'Copied!',
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<div className="up-track-wrap">
|
||||||
|
<div className="up-track">
|
||||||
|
{striche.map((s) => (
|
||||||
|
<span className={`up-tick ${s.art}`} key={s.key} title={titel(s)} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="up-track-foot">
|
||||||
|
<span>{t('server.days7')}</span>
|
||||||
|
{quote != null && (
|
||||||
|
<span className={quote >= 99 ? 'up-good' : quote >= 90 ? '' : 'up-bad'}>
|
||||||
|
{quote.toLocaleString(tag, { minimumFractionDigits: 1, maximumFractionDigits: 1 })} %
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span>{t('server.now')}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Spieler-Peak aus den 24h-Samples (Uptime kommt aus dem Balken oben) */
|
||||||
function historyStats(history) {
|
function historyStats(history) {
|
||||||
if (history.length === 0) return null;
|
if (history.length === 0) return null;
|
||||||
const online = history.filter((h) => h.online).length;
|
return { peak: Math.max(0, ...history.map((h) => h.players ?? 0)) };
|
||||||
return {
|
|
||||||
uptime: Math.round((online / history.length) * 100),
|
|
||||||
peak: Math.max(0, ...history.map((h) => h.players ?? 0)),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function ServerCard({ s }) {
|
function ServerCard({ s }) {
|
||||||
@@ -99,15 +158,14 @@ function ServerCard({ s }) {
|
|||||||
{stats && (
|
{stats && (
|
||||||
<div className="srv-stats">
|
<div className="srv-stats">
|
||||||
<span>{t('server.players24h')}</span>
|
<span>{t('server.players24h')}</span>
|
||||||
<span>
|
{/* Uptime steht jetzt am Balken darunter — zweimal dieselbe
|
||||||
{t('server.peak')} <b>{stats.peak}</b> · {t('server.uptime')}{' '}
|
Zahl in verschiedenen Zeiträumen verwirrt nur. */}
|
||||||
<b className={stats.uptime >= 99 ? 'up-good' : stats.uptime >= 90 ? '' : 'up-bad'}>
|
<span>{t('server.peak')} <b>{stats.peak}</b></span>
|
||||||
{stats.uptime}%
|
|
||||||
</b>
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<UptimeTrack buckets={s.uptime} />
|
||||||
|
|
||||||
{(s.connect_url || s.address) && (
|
{(s.connect_url || s.address) && (
|
||||||
<div className="srv-connect">
|
<div className="srv-connect">
|
||||||
{s.connect_url && (
|
{s.connect_url && (
|
||||||
|
|||||||
@@ -1736,6 +1736,40 @@ label.toggle {
|
|||||||
.srv-checked { font-family: var(--mono); font-size: .62rem; color: var(--muted2); margin-top: .8rem; }
|
.srv-checked { font-family: var(--mono); font-size: .62rem; color: var(--muted2); margin-top: .8rem; }
|
||||||
|
|
||||||
/* ── AUDIT-LOG ─────────────────────────────────────── */
|
/* ── 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 ──────────────────────────────── */
|
/* ── SPRACHUMSCHALTER ──────────────────────────────── */
|
||||||
.lang-switch {
|
.lang-switch {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
|||||||
@@ -695,9 +695,21 @@ const playerHistoryStmt = db.prepare(`
|
|||||||
WHERE server_id = ? AND ts >= datetime('now', ?) ORDER BY ts
|
WHERE server_id = ? AND ts >= datetime('now', ?) ORDER BY ts
|
||||||
`);
|
`);
|
||||||
const prunePlayerHistoryStmt = db.prepare(`DELETE FROM player_history WHERE ts < datetime('now', '-7 days')`);
|
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) =>
|
export const recordPlayerSample = (serverId, online, players) =>
|
||||||
insertPlayerSample.run(serverId, online ? 1 : 0, players);
|
insertPlayerSample.run(serverId, online ? 1 : 0, players);
|
||||||
export const playerHistory = (serverId, hours = 24) => playerHistoryStmt.all(serverId, `-${hours} hours`);
|
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();
|
export const prunePlayerHistory = () => prunePlayerHistoryStmt.run();
|
||||||
|
|
||||||
// Moderation: Verwarnungen + Sticky-Roles
|
// Moderation: Verwarnungen + Sticky-Roles
|
||||||
|
|||||||
+4
-1
@@ -15,7 +15,7 @@ import {
|
|||||||
saveWebAdmin, webAdminScopes, listWebAdmins, deleteWebAdmin,
|
saveWebAdmin, webAdminScopes, listWebAdmins, deleteWebAdmin,
|
||||||
saveTemplate, listTemplates, deleteTemplate,
|
saveTemplate, listTemplates, deleteTemplate,
|
||||||
getLevelRow, isPlaytester, saveWish, toggleWishVote, myWishVotes,
|
getLevelRow, isPlaytester, saveWish, toggleWishVote, myWishVotes,
|
||||||
logAudit, listAudit, playerHistory, deleteUserData,
|
logAudit, listAudit, playerHistory, uptimeBuckets, deleteUserData,
|
||||||
saveSsoApp, listSsoApps, deleteSsoApp, getSsoApp,
|
saveSsoApp, listSsoApps, deleteSsoApp, getSsoApp,
|
||||||
createService, updateService, listServices, deleteService,
|
createService, updateService, listServices, deleteService,
|
||||||
savePage, getPage, listPages, listPublishedPages, deletePage,
|
savePage, getPage, listPages, listPublishedPages, deletePage,
|
||||||
@@ -182,6 +182,9 @@ export function registerApiRoutes(app, client) {
|
|||||||
ping: r?.ping ?? null,
|
ping: r?.ping ?? null,
|
||||||
checkedAt: r?.checkedAt ?? null,
|
checkedAt: r?.checkedAt ?? null,
|
||||||
history: playerHistory(s.id, 24),
|
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),
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|||||||
Reference in New Issue
Block a user