Analyse: Tageswerte nach 25 Monaten löschen, secure_delete, CSV-Export

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-01 18:50:26 +00:00
1 parent a73e0be952
commit 97890a8eee
5 files changed
+248 -3

No files matched your search

+1
View File
@@ -55,6 +55,7 @@ CREATE TABLE IF NOT EXISTS agg_state (
export function openDb(path) {
const db = new DatabaseSync(path);
if (path !== ':memory:') db.exec('PRAGMA journal_mode = WAL');
db.exec('PRAGMA secure_delete = ON');
db.exec(SCHEMA);
return db;
}
+32 -3
View File
@@ -8,6 +8,17 @@ const WALL_FMT = new Intl.DateTimeFormat('en-GB', {
const DAY_MS = 86400 * 1000;
export const RAW_DAYS = 30;
export const OFF_DAYS = 60;
export const DAILY_MONTHS = 25;
// Berliner Tag von `now` minus DAILY_MONTHS Kalendermonate (Monatsende auf den letzten Tag des Zielmonats begrenzt).
export function dailyCutoffDay(now) {
const [y, m, d] = DAY_FMT.format(now).split('-').map(Number);
const idx = y * 12 + (m - 1) - DAILY_MONTHS;
const ty = Math.floor(idx / 12);
const tm = idx % 12;
const last = new Date(Date.UTC(ty, tm + 1, 0)).getUTCDate();
return `${String(ty).padStart(4, '0')}-${String(tm + 1).padStart(2, '0')}-${String(Math.min(d, last)).padStart(2, '0')}`;
}
// Kalendertag (YYYY-MM-DD) in Europe/Berlin.
export const dayOf = (ts) => DAY_FMT.format(ts);
@@ -38,7 +49,7 @@ export function aggregateRows(rows) {
return [...m].filter(([, n]) => n > 0).map(([k, n]) => { const [dim, key] = k.split('\u0000'); return [dim, key, n]; });
}
export function runNightly(db, { now = Date.now() } = {}) {
export function runNightly(db, { now = Date.now(), log = console } = {}) {
const today = dayOf(now);
const rawCut = now - RAW_DAYS * DAY_MS;
const offCut = new Date(now - OFF_DAYS * DAY_MS).toISOString();
@@ -48,7 +59,7 @@ export function runNightly(db, { now = Date.now() } = {}) {
const insDaily = db.prepare(`INSERT INTO daily (site, day, dim, key, count) VALUES (?, ?, ?, ?, ?)
ON CONFLICT(site, day, dim, key) DO UPDATE SET count = count + excluded.count`);
const insDone = db.prepare('INSERT OR IGNORE INTO agg_state (site, day) VALUES (?, ?)');
const stats = { aggregated: 0, sitesDeleted: 0 };
const stats = { aggregated: 0, sitesDeleted: 0, dailyDeleted: 0, orphansMarked: 0, orphanRowsDeleted: 0 };
const tx = (fn) => {
db.exec('BEGIN');
@@ -85,6 +96,24 @@ export function runNightly(db, { now = Date.now() } = {}) {
stats.sitesDeleted++;
});
}
// Tageswerte älter als 25 Monate (alle Websites)
tx(() => {
const cut = dailyCutoffDay(now);
stats.dailyDeleted = Number(db.prepare('DELETE FROM daily WHERE day < ?').run(cut).changes);
db.prepare('DELETE FROM agg_state WHERE day < ?').run(cut);
});
// Ausgeschaltete Websites ohne offSince: Frist beginnt bei Entdeckung.
stats.orphansMarked = Number(db.prepare('UPDATE sites SET offSince = ? WHERE active = 0 AND offSince IS NULL').run(new Date(now).toISOString()).changes);
// Zeilen ohne Website-Eintrag
tx(() => {
for (const t of ['events_raw', 'clicks', 'daily', 'agg_state']) {
stats.orphanRowsDeleted += Number(db.prepare(`DELETE FROM ${t} WHERE site NOT IN (SELECT siteKey FROM sites)`).run().changes);
}
});
try { db.exec('PRAGMA wal_checkpoint(TRUNCATE)'); } catch (e) { log.error('Analytics-Nachtjob: wal_checkpoint:', e.message); }
return stats;
}
@@ -113,7 +142,7 @@ export function scheduleNightly(db, { startDelayMs = 60000, log = console, now =
const run = () => {
try {
const r = job(db, { now: now() });
log.log(`Analytics-Nachtjob: ${r.aggregated} Tage verdichtet, ${r.sitesDeleted} Websites gelöscht`);
log.log(`Analytics-Nachtjob: ${r.aggregated} Tage verdichtet, ${r.sitesDeleted} Websites gelöscht, ${r.dailyDeleted} Tageswerte > ${DAILY_MONTHS} Monate gelöscht, ${r.orphansMarked} Websites markiert, ${r.orphanRowsDeleted} verwaiste Zeilen gelöscht`);
} catch (e) { log.error('Analytics-Nachtjob:', e.message); }
};
const arm = (delay, fn) => { if (!stopped) timer = timers.setTimeout(fn, delay); };