diff --git a/backend/routes/admin.py b/backend/routes/admin.py index ae379a8..b3bee12 100644 --- a/backend/routes/admin.py +++ b/backend/routes/admin.py @@ -534,6 +534,40 @@ async def scheduler_trigger(job_id: str, user=Depends(require_admin)): return {"ok": True, "job_id": job_id} +# ------------------------------------------------------------------ +# GET /api/admin/ki/history — 30-Tage-Verlauf + Top-User (all-time) +# ------------------------------------------------------------------ +@router.get("/ki/history") +async def ki_history(user=Depends(require_mod)): + with db() as conn: + daily = conn.execute(""" + SELECT date, + SUM(count) AS total, + SUM(CASE WHEN source='cloud' THEN count ELSE 0 END) AS cloud, + SUM(CASE WHEN source='local' THEN count ELSE 0 END) AS local, + SUM(CASE WHEN source='luna' THEN count ELSE 0 END) AS luna + FROM ki_daily_calls + WHERE date >= DATE('now', '-29 days') + GROUP BY date ORDER BY date ASC + """).fetchall() + top_users = conn.execute(""" + SELECT u.name, u.email, + SUM(k.count) AS total, + SUM(CASE WHEN k.source='cloud' THEN k.count ELSE 0 END) AS cloud, + MAX(k.date) AS last_date + FROM ki_daily_calls k JOIN users u ON u.id = k.user_id + GROUP BY k.user_id ORDER BY total DESC LIMIT 15 + """).fetchall() + return { + "daily_history": [{"date": r["date"], "total": r["total"], + "cloud": r["cloud"], "local": r["local"], "luna": r["luna"]} + for r in daily], + "top_users": [{"name": r["name"], "email": r["email"], + "total": r["total"], "cloud": r["cloud"], "last_date": r["last_date"]} + for r in top_users], + } + + # ------------------------------------------------------------------ # GET /api/admin/ki/status — lokale LLM-Erreichbarkeit prüfen # ------------------------------------------------------------------ diff --git a/backend/static/js/app.js b/backend/static/js/app.js index 210e83a..4294f1c 100644 --- a/backend/static/js/app.js +++ b/backend/static/js/app.js @@ -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 = (() => { diff --git a/backend/static/js/pages/admin.js b/backend/static/js/pages/admin.js index f54865d..24d651a 100644 --- a/backend/static/js/pages/admin.js +++ b/backend/static/js/pages/admin.js @@ -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 = (() => {
-

KI-Nutzung

- ${_kiStatusBadge()} -
- ${[ - ['☁️ 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]) => ` -
- ${label} - ${val ?? 0} -
- `).join('')} -
-
-

- User-Limit: ${s.ki_cloud_weekly_limit ?? 20} Cloud-Anfragen / Woche + +

+

+ + KI-Nutzung

- ${(s.ki_top_users || []).length ? ` -

Top Cloud-User (7 Tage)

- ${s.ki_top_users.map((u, i) => ` -
- ${i+1}. ${_esc(u.name)} - ${u.cloud_calls} -
- `).join('')}` : ''} + + ${s.ki_today ?? 0} heute · ${s.ki_week ?? 0} (7 Tage) +
+ + ${_kiStatusBadge()} + +
+ ${[ + ['☁️ 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]) => ` + + ${label} + ${val ?? 0} + `).join('')} +
+ + ${(() => { + const hist = kiH?.daily_history || []; + if (!hist.length) return '

Noch keine Verlaufsdaten

'; + 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 `
+ + + +
+ ${first}30 Tage${last} +
+
`; + })()} + +

+ Cloud-Limit: ${s.ki_cloud_weekly_limit ?? 20} Anfragen / Woche pro User +

+ + ${(kiH?.top_users || []).length ? ` +

Aktivste Nutzer

+
+ + + + + + ${(kiH.top_users).map(u => ` + + + + + + `).join('')} + +
NameE-Mail☁️ CloudGesamtZuletzt
${_esc(u.name)}${_esc(u.email.length > 22 ? u.email.split('@')[1] : u.email)}${u.cloud}${u.total}${u.last_date?.slice(5) || '—'}
+
` : ''}
diff --git a/backend/static/sw.js b/backend/static/sw.js index ec5fa6f..74e602c 100644 --- a/backend/static/sw.js +++ b/backend/static/sw.js @@ -3,7 +3,7 @@ Offline-Cache + Push Notifications + Tile-Cache ============================================================ */ -const CACHE_VERSION = 'by-v485'; +const CACHE_VERSION = 'by-v486'; const CACHE_STATIC = `${CACHE_VERSION}-static`; const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten