Feature: ORS-Stats im Admin-Panel — Tagesverbrauch/2000, 30-Tage-Sparkline, Top-Nutzer — SW by-v485, APP_VER 462

This commit is contained in:
rene 2026-04-29 10:10:59 +02:00
parent 69140a261e
commit 392359df45
6 changed files with 170 additions and 3 deletions

View file

@ -903,6 +903,7 @@ window.Page_admin = (() => {
${UI.icon('arrows-clockwise')} Aktualisieren
</button>
</div>
<div id="adm-ors-card"></div>
<div id="adm-sys-cards">Lade</div>
<div class="card" style="margin-top:var(--space-4);padding:var(--space-4)">
<div style="font-size:var(--text-sm);font-weight:var(--weight-semibold);
@ -1071,7 +1072,11 @@ window.Page_admin = (() => {
}
});
await _loadSystemCards(el.querySelector('#adm-sys-cards'));
const [, orsStats] = await Promise.all([
_loadSystemCards(el.querySelector('#adm-sys-cards')),
API.get('/admin/ors/stats').catch(() => null),
]);
_renderOrsCard(el.querySelector('#adm-ors-card'), orsStats);
await loadLogs();
}
@ -1109,6 +1114,101 @@ window.Page_admin = (() => {
`;
}
function _renderOrsCard(el, d) {
if (!el) return;
if (!d || d.today_count == null) { el.innerHTML = ''; return; }
const limit = d.today_limit ?? 2000;
const count = d.today_count ?? 0;
const pct = Math.min(Math.round(count / limit * 100), 100);
const barColor = pct < 50 ? '#4ade80' : pct <= 80 ? '#facc15' : '#f87171';
// Sparkline aus daily_history (letzte 30 Tage)
const hist = Array.isArray(d.daily_history) ? d.daily_history : [];
const W = 400, H = 60, padY = 5;
let sparkline = '';
if (hist.length >= 2) {
const maxC = Math.max(...hist.map(h => h.count), 1);
const pts = hist.map((h, i) => {
const x = (i / (hist.length - 1)) * W;
const y = H - padY - (h.count / maxC) * (H - 2 * padY);
return `${x.toFixed(1)},${y.toFixed(1)}`;
}).join(' ');
sparkline = `<polyline points="${pts}" fill="none" stroke="var(--c-primary)" stroke-width="1.5" stroke-linejoin="round" stroke-linecap="round"/>`;
} else {
sparkline = `<polyline points="0,55 ${W},55" fill="none" stroke="var(--c-primary)" stroke-width="1.5"/>`;
}
const lastDate = hist.length ? hist[hist.length - 1].date : '';
// Top-Nutzer-Tabelle
const topUsers = Array.isArray(d.top_users) ? d.top_users.slice(0, 10) : [];
const userRows = topUsers.map(u => {
const emailDisplay = (u.email || '').length > 20
? '@' + (u.email || '').split('@')[1]
: _esc(u.email || '');
return `<tr>
<td style="padding:5px 8px;font-weight:500">${_esc(u.name || '')}</td>
<td style="padding:5px 8px;color:var(--c-text-muted);font-size:var(--text-xs)">${emailDisplay}</td>
<td style="padding:5px 8px;text-align:right;font-weight:600">${u.total ?? 0}</td>
<td style="padding:5px 8px;text-align:right;color:var(--c-text-muted);font-size:var(--text-xs)">${u.last_week || ''}</td>
</tr>`;
}).join('');
el.innerHTML = `
<div class="card" style="margin-bottom:var(--space-4);padding:var(--space-4)">
<!-- Header -->
<div style="display:flex;align-items:center;gap:var(--space-3);margin-bottom:var(--space-3)">
<svg class="ph-icon" style="width:20px;height:20px;color:var(--c-primary);flex-shrink:0" aria-hidden="true">
<use href="/icons/phosphor.svg#path"></use>
</svg>
<span style="font-size:var(--text-sm);font-weight:var(--weight-semibold);color:var(--c-text);flex:1">
OpenRouteService
</span>
<span style="font-size:var(--text-xs);font-weight:700;padding:3px 10px;border-radius:999px;
background:${barColor}22;color:${barColor};border:1px solid ${barColor}44">
${count.toLocaleString('de')} / ${limit.toLocaleString('de')} heute
</span>
</div>
<!-- Fortschrittsbalken -->
<div style="height:6px;border-radius:3px;background:var(--c-border);overflow:hidden;margin-bottom:var(--space-4)">
<div style="height:100%;width:${pct}%;background:${barColor};border-radius:3px;transition:width 0.6s ease"></div>
</div>
<!-- Sparkline -->
<div style="margin-bottom:var(--space-1)">
<svg viewBox="0 0 ${W} ${H}" style="width:100%;height:60px;display:block">
${sparkline}
</svg>
</div>
<div style="display:flex;justify-content:space-between;font-size:var(--text-xs);
color:var(--c-text-muted);margin-bottom:var(--space-4)">
<span>30 Tage</span>
<span>${_esc(lastDate)}</span>
</div>
${topUsers.length ? `
<!-- Top-Nutzer -->
<div style="font-size:var(--text-xs);font-weight:700;text-transform:uppercase;letter-spacing:.05em;
color:var(--c-text-secondary);margin-bottom:var(--space-2)">Aktivste Nutzer</div>
<div style="overflow-x:auto">
<table style="width:100%;border-collapse:collapse;font-size:var(--text-xs)">
<thead>
<tr style="border-bottom:1px solid var(--c-border)">
<th style="text-align:left;padding:4px 8px;color:var(--c-text-muted);font-weight:600">Name</th>
<th style="text-align:left;padding:4px 8px;color:var(--c-text-muted);font-weight:600">E-Mail</th>
<th style="text-align:right;padding:4px 8px;color:var(--c-text-muted);font-weight:600">Gesamt</th>
<th style="text-align:right;padding:4px 8px;color:var(--c-text-muted);font-weight:600">Letzte Woche</th>
</tr>
</thead>
<tbody>${userRows}</tbody>
</table>
</div>` : ''}
</div>`;
}
function _formatUptime(secs) {
const d = Math.floor(secs / 86400);
const h = Math.floor((secs % 86400) / 3600);