diff --git a/backend/routes/admin.py b/backend/routes/admin.py index b3bee12..4868a61 100644 --- a/backend/routes/admin.py +++ b/backend/routes/admin.py @@ -1,4 +1,5 @@ """BAN YARO — Admin / Moderator Backend""" +import asyncio import os import sys import time @@ -707,35 +708,44 @@ async def get_analytics(user=Depends(require_mod)): now = datetime.now(_TZ) today_start = now.replace(hour=0, minute=0, second=0, microsecond=0) week_start = today_start - timedelta(days=6) + month_start = today_start - timedelta(days=29) now_ms = int(now.timestamp() * 1000) today_ms = int(today_start.timestamp() * 1000) week_ms = int(week_start.timestamp() * 1000) + month_ms = int(month_start.timestamp() * 1000) - async with httpx.AsyncClient(timeout=10) as c: - r_today = await c.get(f"{url}/api/websites/{site_id}/stats", - params={"startAt": today_ms, "endAt": now_ms}, headers=headers) - r_week = await c.get(f"{url}/api/websites/{site_id}/stats", - params={"startAt": week_ms, "endAt": now_ms}, headers=headers) - r_pv = await c.get(f"{url}/api/websites/{site_id}/pageviews", - params={"startAt": week_ms, "endAt": now_ms, - "unit": "day", "timezone": "Europe/Berlin"}, headers=headers) - r_pages = await c.get(f"{url}/api/websites/{site_id}/metrics", - params={"startAt": week_ms, "endAt": now_ms, - "type": "url", "limit": 8}, headers=headers) + async with httpx.AsyncClient(timeout=15) as c: + r_today, r_week, r_month, r_pv_month, r_pages, r_refs = await asyncio.gather( + c.get(f"{url}/api/websites/{site_id}/stats", + params={"startAt": today_ms, "endAt": now_ms}, headers=headers), + c.get(f"{url}/api/websites/{site_id}/stats", + params={"startAt": week_ms, "endAt": now_ms}, headers=headers), + c.get(f"{url}/api/websites/{site_id}/stats", + params={"startAt": month_ms, "endAt": now_ms}, headers=headers), + c.get(f"{url}/api/websites/{site_id}/pageviews", + params={"startAt": month_ms, "endAt": now_ms, + "unit": "day", "timezone": "Europe/Berlin"}, headers=headers), + c.get(f"{url}/api/websites/{site_id}/metrics", + params={"startAt": month_ms, "endAt": now_ms, + "type": "url", "limit": 10}, headers=headers), + c.get(f"{url}/api/websites/{site_id}/metrics", + params={"startAt": month_ms, "endAt": now_ms, + "type": "referrer", "limit": 8}, headers=headers), + ) def _to_list(r): j = r.json() - if isinstance(j, list): - return j - if isinstance(j, dict): - return j.get("data", j.get("metrics", [])) + if isinstance(j, list): return j + if isinstance(j, dict): return j.get("data", j.get("metrics", [])) return [] return { - "today": r_today.json(), - "week": r_week.json(), - "pageviews": r_pv.json(), - "top_pages": _to_list(r_pages), + "today": r_today.json(), + "week": r_week.json(), + "month": r_month.json(), + "pageviews": r_pv_month.json(), + "top_pages": _to_list(r_pages), + "referrers": _to_list(r_refs), } diff --git a/backend/static/js/app.js b/backend/static/js/app.js index 48715ab..44bbf47 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 = '465'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '466'; // ← 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 24d651a..b1a0c3b 100644 --- a/backend/static/js/pages/admin.js +++ b/backend/static/js/pages/admin.js @@ -218,95 +218,149 @@ window.Page_admin = (() => { // ------------------------------------------------------------------ // TAB: ANALYTICS async function _renderAnalytics(el) { - const d = await API.get('/admin/analytics'); - - const pv = d.pageviews?.pageviews ?? []; - const ses = d.pageviews?.sessions ?? []; - - // Sparkline SVG (Seitenaufrufe 7 Tage) - function _sparkline(data, color) { - if (!data.length) return 'Keine Daten'; - const vals = data.map(p => p.y ?? 0); - const max = Math.max(...vals, 1); - const W = 200, H = 48, pad = 4; - const pts = vals.map((v, i) => { - const x = pad + i * ((W - 2*pad) / Math.max(vals.length - 1, 1)); - const y = H - pad - (v / max) * (H - 2*pad); - return `${x.toFixed(1)},${y.toFixed(1)}`; - }).join(' '); - return ``; + el.innerHTML = `
Keine Daten
`; + const W = 800, H = 120, padX = 0, padY = 8; + const pvVals = pvData.map(p => p.y ?? 0); + const sesVals = sesData.map(p => p.y ?? 0); + const maxVal = Math.max(...pvVals, ...sesVals, 1); + const n = pvData.length; + + function toXY(vals) { + return vals.map((v, i) => { + const x = n === 1 ? W/2 : padX + i * ((W - 2*padX) / (n - 1)); + const y = H - padY - (v / maxVal) * (H - 2*padY); + return [x.toFixed(1), y.toFixed(1)]; + }); + } + const pvPts = toXY(pvVals); + const sesPts = toXY(sesVals); + const pvLine = pvPts.map(p => p.join(',')).join(' '); + const sesLine = sesPts.map(p => p.join(',')).join(' '); + + // Filled area unter pageviews + const areaPath = `M ${pvPts[0][0]},${H} ` + + pvPts.map(p => `L ${p[0]},${p[1]}`).join(' ') + + ` L ${pvPts[pvPts.length-1][0]},${H} Z`; + + // X-Achsen-Labels: Anfang, Mitte, Ende + const labelIdx = [0, Math.floor(n/2), n-1].filter((v,i,a) => a.indexOf(v)===i); + const labels = labelIdx.map(i => { + const x = n === 1 ? W/2 : padX + i * ((W - 2*padX) / (n - 1)); + const date = pvData[i]?.x ? new Date(pvData[i].x).toLocaleDateString('de',{day:'2-digit',month:'2-digit'}) : ''; + return `Keine Daten
`; + const maxV = Math.max(...items.map(p => p[valKey] ?? 0), 1); + return `Noch keine Daten
` - : `