Feature: ORS-Stats im Admin-Panel — Tagesverbrauch/2000, 30-Tage-Sparkline, Top-Nutzer — SW by-v485, APP_VER 462
This commit is contained in:
parent
69140a261e
commit
392359df45
6 changed files with 170 additions and 3 deletions
|
|
@ -3,7 +3,7 @@
|
|||
Router, State-Management, Navigation, Initialisierung.
|
||||
============================================================ */
|
||||
|
||||
const APP_VER = '461'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
||||
const APP_VER = '462'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
||||
|
||||
const App = (() => {
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue