Feature: KI-Nutzung im Admin mit 30-Tage-Sparkline + Top-Nutzer-Tabelle — SW by-v486, APP_VER 463

This commit is contained in:
rene 2026-04-29 10:42:00 +02:00
parent 392359df45
commit dc737d0c48
4 changed files with 109 additions and 32 deletions

View file

@ -3,7 +3,7 @@
Router, State-Management, Navigation, Initialisierung.
============================================================ */
const APP_VER = '462'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
const APP_VER = '463'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
const App = (() => {

View file

@ -311,9 +311,10 @@ window.Page_admin = (() => {
// TAB: ÜBERSICHT
// ------------------------------------------------------------------
async function _renderStats(el) {
const [s, ki] = await Promise.all([
const [s, ki, kiH] = await Promise.all([
API.get('/admin/stats'),
API.get('/admin/ki/status').catch(() => null),
API.get('/admin/ki/history').catch(() => null),
]);
const _kiStatusBadge = () => {
@ -364,37 +365,79 @@ window.Page_admin = (() => {
</div>
<div class="card" style="padding:var(--space-4)">
<p style="font-size:var(--text-sm);font-weight:600;margin:0 0 var(--space-3)">KI-Nutzung</p>
${_kiStatusBadge()}
<div style="display:flex;flex-direction:column;gap:var(--space-2)">
${[
['☁️ Claude (7 Tage)', s.ki_cloud_week, 'var(--c-primary)'],
['🖥️ LM Studio (7 Tage)', s.ki_local_week, 'var(--c-success)'],
['🌙 Luna (7 Tage)', s.ki_luna_week, 'var(--c-warning)'],
['Gesamt heute', s.ki_today, 'var(--c-text-secondary)'],
['Gesamt 7 Tage', s.ki_week, 'var(--c-text-secondary)'],
['Gesamt Monat', s.ki_month, 'var(--c-text-secondary)'],
['Aktive User heute', s.ki_users_today, 'var(--c-text-secondary)'],
].map(([label, val, color]) => `
<div style="display:flex;justify-content:space-between;font-size:var(--text-sm)">
<span style="color:var(--c-text-secondary)">${label}</span>
<span style="font-weight:600;color:${color}">${val ?? 0}</span>
</div>
`).join('')}
</div>
<div style="margin-top:var(--space-3);padding-top:var(--space-3);border-top:1px solid var(--c-border)">
<p style="font-size:var(--text-xs);color:var(--c-text-muted);margin:0 0 var(--space-2)">
User-Limit: <strong>${s.ki_cloud_weekly_limit ?? 20} Cloud-Anfragen / Woche</strong>
<!-- Header -->
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:var(--space-3)">
<p style="font-size:var(--text-sm);font-weight:600;margin:0;display:flex;align-items:center;gap:var(--space-2)">
<svg class="ph-icon" style="width:16px;height:16px;color:var(--c-primary)" aria-hidden="true"><use href="/icons/phosphor.svg#robot"></use></svg>
KI-Nutzung
</p>
${(s.ki_top_users || []).length ? `
<p style="font-size:var(--text-xs);font-weight:600;color:var(--c-text-secondary);margin:var(--space-2) 0 var(--space-1)">Top Cloud-User (7 Tage)</p>
${s.ki_top_users.map((u, i) => `
<div style="display:flex;justify-content:space-between;font-size:var(--text-xs)">
<span style="color:var(--c-text-secondary)">${i+1}. ${_esc(u.name)}</span>
<span style="font-weight:600;color:${u.cloud_calls >= (s.ki_cloud_weekly_limit ?? 20) ? 'var(--c-danger)' : 'var(--c-primary)'}">${u.cloud_calls}</span>
</div>
`).join('')}` : ''}
<span style="font-size:var(--text-xs);font-weight:700;padding:2px 10px;border-radius:100px;
background:var(--c-primary-subtle);color:var(--c-primary)">
${s.ki_today ?? 0} heute · ${s.ki_week ?? 0} (7 Tage)
</span>
</div>
<!-- KI-Status Badge -->
${_kiStatusBadge()}
<!-- Quellen-Zeile -->
<div style="display:flex;gap:var(--space-2);flex-wrap:wrap;margin-bottom:var(--space-3)">
${[
['☁️ Claude', s.ki_cloud_week, 'var(--c-primary)'],
['🖥️ Lokal', s.ki_local_week, 'var(--c-success)'],
['🌙 Luna', s.ki_luna_week, 'var(--c-warning)'],
['📅 Monat', s.ki_month, 'var(--c-text-secondary)'],
['👥 User heute',s.ki_users_today, 'var(--c-text-secondary)'],
].map(([label, val, color]) => `
<span style="font-size:var(--text-xs);padding:2px 8px;border-radius:100px;
background:var(--c-surface-2);border:1px solid var(--c-border);white-space:nowrap">
<span style="color:var(--c-text-muted)">${label}</span>
<strong style="color:${color};margin-left:3px">${val ?? 0}</strong>
</span>`).join('')}
</div>
<!-- Sparkline (30 Tage) -->
${(() => {
const hist = kiH?.daily_history || [];
if (!hist.length) return '<p style="font-size:var(--text-xs);color:var(--c-text-muted);margin:0 0 var(--space-3)">Noch keine Verlaufsdaten</p>';
const max = Math.max(...hist.map(d => d.total), 1);
const W = 400, H = 60, n = hist.length;
const pts = hist.map((d, i) => {
const x = n === 1 ? W/2 : (i / (n - 1)) * W;
const y = H - 5 - (d.total / max) * (H - 10);
return `${x.toFixed(1)},${y.toFixed(1)}`;
}).join(' ');
const first = hist[0]?.date?.slice(5) || '';
const last = hist[hist.length-1]?.date?.slice(5) || '';
return `<div style="margin-bottom:var(--space-3)">
<svg viewBox="0 0 ${W} ${H}" style="width:100%;height:60px;display:block" preserveAspectRatio="none">
<polyline points="${pts}" fill="none" stroke="var(--c-primary)" stroke-width="1.5" stroke-linejoin="round"/>
</svg>
<div style="display:flex;justify-content:space-between;font-size:10px;color:var(--c-text-muted);margin-top:2px">
<span>${first}</span><span>30 Tage</span><span>${last}</span>
</div>
</div>`;
})()}
<!-- Limit-Hinweis -->
<p style="font-size:var(--text-xs);color:var(--c-text-muted);margin:0 0 var(--space-3)">
Cloud-Limit: <strong>${s.ki_cloud_weekly_limit ?? 20} Anfragen / Woche</strong> pro User
</p>
<!-- Top-Nutzer -->
${(kiH?.top_users || []).length ? `
<p style="font-size:var(--text-xs);font-weight:600;color:var(--c-text-secondary);margin:0 0 var(--space-2);text-transform:uppercase;letter-spacing:.04em">Aktivste Nutzer</p>
<div class="adm-table-scroll">
<table class="adm-table">
<thead><tr>
<th>Name</th><th>E-Mail</th><th> Cloud</th><th>Gesamt</th><th>Zuletzt</th>
</tr></thead>
<tbody>
${(kiH.top_users).map(u => `<tr>
<td style="font-weight:600">${_esc(u.name)}</td>
<td style="color:var(--c-text-muted)">${_esc(u.email.length > 22 ? u.email.split('@')[1] : u.email)}</td>
<td style="color:var(--c-primary);font-weight:600">${u.cloud}</td>
<td>${u.total}</td>
<td style="color:var(--c-text-muted)">${u.last_date?.slice(5) || '—'}</td>
</tr>`).join('')}
</tbody>
</table>
</div>` : ''}
</div>
<div class="card" style="padding:var(--space-4)">