From 977fbeb0fdbabff9d9bb622a150592ef10da38b6 Mon Sep 17 00:00:00 2001 From: rene Date: Wed, 29 Apr 2026 14:32:10 +0200 Subject: [PATCH] =?UTF-8?q?Feature:=20Analytics=20Jahres-Balkendiagramm=20?= =?UTF-8?q?=E2=80=94=20Seitenaufrufe=20+=20Neuanmeldungen=2012=20Monate=20?= =?UTF-8?q?=E2=80=94=20SW=20by-v502,=20APP=5FVER=20479?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/routes/admin.py | 31 ++++++++++++---- backend/static/js/app.js | 2 +- backend/static/js/pages/admin.js | 62 ++++++++++++++++++++++++++++++++ backend/static/sw.js | 2 +- 4 files changed, 88 insertions(+), 9 deletions(-) diff --git a/backend/routes/admin.py b/backend/routes/admin.py index 4868a61..8be436c 100644 --- a/backend/routes/admin.py +++ b/backend/routes/admin.py @@ -709,13 +709,15 @@ async def get_analytics(user=Depends(require_mod)): 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) + year_start = today_start - timedelta(days=364) 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) + year_ms = int(year_start.timestamp() * 1000) async with httpx.AsyncClient(timeout=15) as c: - r_today, r_week, r_month, r_pv_month, r_pages, r_refs = await asyncio.gather( + r_today, r_week, r_month, r_pv_month, r_pv_year, 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", @@ -725,6 +727,9 @@ async def get_analytics(user=Depends(require_mod)): 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}/pageviews", + params={"startAt": year_ms, "endAt": now_ms, + "unit": "month", "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), @@ -733,6 +738,16 @@ async def get_analytics(user=Depends(require_mod)): "type": "referrer", "limit": 8}, headers=headers), ) + # Monatliche Neuanmeldungen aus lokaler DB (letzte 12 Monate) + with db() as conn: + reg_rows = conn.execute(""" + SELECT strftime('%Y-%m', created_at) AS month, COUNT(*) AS count + FROM users + WHERE created_at >= date('now', '-12 months') + GROUP BY month ORDER BY month ASC + """).fetchall() + monthly_registrations = [{"month": r["month"], "count": r["count"]} for r in reg_rows] + def _to_list(r): j = r.json() if isinstance(j, list): return j @@ -740,12 +755,14 @@ async def get_analytics(user=Depends(require_mod)): return [] return { - "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), + "today": r_today.json(), + "week": r_week.json(), + "month": r_month.json(), + "pageviews": r_pv_month.json(), + "pageviews_year": r_pv_year.json(), + "monthly_registrations": monthly_registrations, + "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 9f26609..247d544 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 = '478'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '479'; // ← 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 8fab722..9452095 100644 --- a/backend/static/js/pages/admin.js +++ b/backend/static/js/pages/admin.js @@ -343,6 +343,68 @@ window.Page_admin = (() => { ${_dualChart(pv, ses)} + +
+
+ Jahresübersicht — letzte 12 Monate +
+ + Seitenaufrufe + + + Neuanmeldungen + +
+
+ ${(() => { + const pvYear = d.pageviews_year?.pageviews ?? []; + const regs = d.monthly_registrations ?? []; + + // Alle Monate der letzten 12 sammeln + const months = []; + const now = new Date(); + for (let i = 11; i >= 0; i--) { + const d2 = new Date(now.getFullYear(), now.getMonth() - i, 1); + months.push(d2.getFullYear() + '-' + String(d2.getMonth()+1).padStart(2,'0')); + } + + // Umami liefert x als ISO-Datum-String + const pvByMonth = {}; + pvYear.forEach(p => { + const key = (p.x || '').slice(0, 7); + pvByMonth[key] = (pvByMonth[key] || 0) + (p.y ?? 0); + }); + const regByMonth = {}; + regs.forEach(r => { regByMonth[r.month] = r.count; }); + + const pvVals = months.map(m => pvByMonth[m] || 0); + const regVals = months.map(m => regByMonth[m] || 0); + const maxPv = Math.max(...pvVals, 1); + const maxReg = Math.max(...regVals, 1); + + const W = 800, H = 120, n = months.length; + const barW = Math.floor((W - (n-1)*4) / n); + + const bars = months.map((m, i) => { + const x = i * (barW + 4); + const pvH = Math.round((pvVals[i] / maxPv) * (H - 20)); + const regH = Math.round((regVals[i] / maxReg) * (H - 20)); + const label = m.slice(5); // MM + return ` + + + ${label} + `; + }).join(''); + + return `${bars}`; + })()} +
+
diff --git a/backend/static/sw.js b/backend/static/sw.js index 63836f7..8a0622e 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-v501'; +const CACHE_VERSION = 'by-v502'; const CACHE_STATIC = `${CACHE_VERSION}-static`; const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten