Feature: Analytics Jahres-Balkendiagramm — Seitenaufrufe + Neuanmeldungen 12 Monate — SW by-v502, APP_VER 479
This commit is contained in:
parent
b2e7f1409a
commit
977fbeb0fd
4 changed files with 88 additions and 9 deletions
|
|
@ -709,13 +709,15 @@ async def get_analytics(user=Depends(require_mod)):
|
||||||
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||||
week_start = today_start - timedelta(days=6)
|
week_start = today_start - timedelta(days=6)
|
||||||
month_start = today_start - timedelta(days=29)
|
month_start = today_start - timedelta(days=29)
|
||||||
|
year_start = today_start - timedelta(days=364)
|
||||||
now_ms = int(now.timestamp() * 1000)
|
now_ms = int(now.timestamp() * 1000)
|
||||||
today_ms = int(today_start.timestamp() * 1000)
|
today_ms = int(today_start.timestamp() * 1000)
|
||||||
week_ms = int(week_start.timestamp() * 1000)
|
week_ms = int(week_start.timestamp() * 1000)
|
||||||
month_ms = int(month_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:
|
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",
|
c.get(f"{url}/api/websites/{site_id}/stats",
|
||||||
params={"startAt": today_ms, "endAt": now_ms}, headers=headers),
|
params={"startAt": today_ms, "endAt": now_ms}, headers=headers),
|
||||||
c.get(f"{url}/api/websites/{site_id}/stats",
|
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",
|
c.get(f"{url}/api/websites/{site_id}/pageviews",
|
||||||
params={"startAt": month_ms, "endAt": now_ms,
|
params={"startAt": month_ms, "endAt": now_ms,
|
||||||
"unit": "day", "timezone": "Europe/Berlin"}, headers=headers),
|
"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",
|
c.get(f"{url}/api/websites/{site_id}/metrics",
|
||||||
params={"startAt": month_ms, "endAt": now_ms,
|
params={"startAt": month_ms, "endAt": now_ms,
|
||||||
"type": "url", "limit": 10}, headers=headers),
|
"type": "url", "limit": 10}, headers=headers),
|
||||||
|
|
@ -733,6 +738,16 @@ async def get_analytics(user=Depends(require_mod)):
|
||||||
"type": "referrer", "limit": 8}, headers=headers),
|
"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):
|
def _to_list(r):
|
||||||
j = r.json()
|
j = r.json()
|
||||||
if isinstance(j, list): return j
|
if isinstance(j, list): return j
|
||||||
|
|
@ -740,12 +755,14 @@ async def get_analytics(user=Depends(require_mod)):
|
||||||
return []
|
return []
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"today": r_today.json(),
|
"today": r_today.json(),
|
||||||
"week": r_week.json(),
|
"week": r_week.json(),
|
||||||
"month": r_month.json(),
|
"month": r_month.json(),
|
||||||
"pageviews": r_pv_month.json(),
|
"pageviews": r_pv_month.json(),
|
||||||
"top_pages": _to_list(r_pages),
|
"pageviews_year": r_pv_year.json(),
|
||||||
"referrers": _to_list(r_refs),
|
"monthly_registrations": monthly_registrations,
|
||||||
|
"top_pages": _to_list(r_pages),
|
||||||
|
"referrers": _to_list(r_refs),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
Router, State-Management, Navigation, Initialisierung.
|
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 = (() => {
|
const App = (() => {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -343,6 +343,68 @@ window.Page_admin = (() => {
|
||||||
${_dualChart(pv, ses)}
|
${_dualChart(pv, ses)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Jahres-Übersicht -->
|
||||||
|
<div class="card" style="padding:var(--space-4)">
|
||||||
|
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:var(--space-4)">
|
||||||
|
<span style="font-size:var(--text-sm);font-weight:var(--weight-semibold)">Jahresübersicht — letzte 12 Monate</span>
|
||||||
|
<div style="display:flex;gap:var(--space-3);font-size:10px;color:var(--c-text-muted)">
|
||||||
|
<span style="display:flex;align-items:center;gap:4px">
|
||||||
|
<span style="width:12px;height:10px;background:var(--c-primary);opacity:0.7;display:inline-block;border-radius:2px"></span> Seitenaufrufe
|
||||||
|
</span>
|
||||||
|
<span style="display:flex;align-items:center;gap:4px">
|
||||||
|
<span style="width:12px;height:10px;background:var(--c-success);opacity:0.7;display:inline-block;border-radius:2px"></span> Neuanmeldungen
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
${(() => {
|
||||||
|
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 `
|
||||||
|
<rect x="${x}" y="${H - 20 - pvH}" width="${barW}" height="${pvH}"
|
||||||
|
fill="var(--c-primary)" opacity="0.65" rx="2"/>
|
||||||
|
<rect x="${x + Math.floor(barW*0.55)}" y="${H - 20 - regH}" width="${Math.floor(barW*0.4)}" height="${regH}"
|
||||||
|
fill="var(--c-success)" opacity="0.8" rx="2"/>
|
||||||
|
<text x="${x + barW/2}" y="${H - 4}" text-anchor="middle"
|
||||||
|
font-size="9" fill="currentColor" style="color:var(--c-text-muted)">${label}</text>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
return `<svg viewBox="0 0 ${W} ${H}" style="width:100%;height:120px;display:block;overflow:visible"
|
||||||
|
preserveAspectRatio="none">${bars}</svg>`;
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Top Seiten + Referrers nebeneinander -->
|
<!-- Top Seiten + Referrers nebeneinander -->
|
||||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-4)">
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-4)">
|
||||||
<div class="card" style="padding:var(--space-4)">
|
<div class="card" style="padding:var(--space-4)">
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
Offline-Cache + Push Notifications + Tile-Cache
|
Offline-Cache + Push Notifications + Tile-Cache
|
||||||
============================================================ */
|
============================================================ */
|
||||||
|
|
||||||
const CACHE_VERSION = 'by-v501';
|
const CACHE_VERSION = 'by-v502';
|
||||||
const CACHE_STATIC = `${CACHE_VERSION}-static`;
|
const CACHE_STATIC = `${CACHE_VERSION}-static`;
|
||||||
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten
|
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue