// Statistik für das Portal: Vergangenheit aus `daily`, noch nicht verdichtete Tage live aus den Rohdaten. import { dayOf, aggregateRows } from './jobs.js'; const DAY_MS = 86400 * 1000; const TOP = 20; // Liste der Kalendertage (YYYY-MM-DD), oldest first, endend am Berlin-Tag von `now`. function dayList(now, range) { const [y, m, d] = dayOf(now).split('-').map(Number); const days = []; for (let i = range - 1; i >= 0; i--) days.push(new Date(Date.UTC(y, m - 1, d - i)).toISOString().slice(0, 10)); return days; } const top = (map) => [...map].map(([key, count]) => ({ key, count })).filter((e) => e.count > 0) .sort((a, b) => b.count - a.count || (a.key < b.key ? -1 : 1)).slice(0, TOP); export function computeStats(db, site, range, now = Date.now()) { const days = dayList(now, range); const first = days[0]; const inRange = new Set(days); const pvByDay = new Map(days.map((d) => [d, 0])); const dims = {}; const bump = (dim, key, n) => { if (!dims[dim]) dims[dim] = new Map(); dims[dim].set(key, (dims[dim].get(key) || 0) + n); }; const apply = (day, dim, key, n) => { if (dim === 'pv_total') pvByDay.set(day, pvByDay.get(day) + n); bump(dim, key, n); }; const done = new Set(db.prepare('SELECT day FROM agg_state WHERE site = ? AND day >= ?').all(site, first).map((r) => r.day)); for (const r of db.prepare('SELECT day, dim, key, count FROM daily WHERE site = ? AND day >= ?').all(site, first)) { if (inRange.has(r.day)) apply(r.day, r.dim, r.key, r.count); } // Tage ohne agg_state (heute, ggf. gestern vor 02:30) live aus den Rohdaten const live = new Map(); const from = now - (range + 1) * DAY_MS; for (const r of db.prepare('SELECT ts, type, path, vp, ref, country, device, visitor, sec, scroll, kind FROM events_raw WHERE site = ? AND ts >= ? AND ts <= ? ORDER BY ts') .all(site, from, now)) { const d = dayOf(r.ts); if (!inRange.has(d) || done.has(d)) continue; if (!live.has(d)) live.set(d, []); live.get(d).push(r); } for (const [day, rows] of live) for (const [dim, key, n] of aggregateRows(rows)) apply(day, dim, key, n); const sum = (dim) => [...(dims[dim] || new Map()).values()].reduce((a, b) => a + b, 0); const banner = { shown: sum('bn_shown'), accept: sum('bn_accept'), reject: sum('bn_reject') }; const decided = banner.accept + banner.reject; const contacts = top(dims.ct_kind || new Map()); return { totals: { pv: sum('pv_total'), // Summe der Tages-Besucher (kein tagesübergreifender Abgleich möglich: Salz wechselt täglich) visitors: sum('visitors'), contacts: sum('ct_kind'), consentRate: decided ? banner.accept / decided : 0, }, series: days.map((day) => ({ day, pv: pvByDay.get(day) })), pages: top(dims.pv_path || new Map()), refs: top(dims.pv_ref || new Map()), countries: top(dims.pv_country || new Map()), devices: top(dims.pv_device || new Map()), contacts, banner, }; }