From 890271d4c890e0f5e46c1aa04063e224e78c3a91 Mon Sep 17 00:00:00 2001 From: Mitchell R Date: Sat, 23 May 2026 01:30:26 +0200 Subject: [PATCH] feat(store): event_log + audit_log rotation (30d/90d TTL + 100k row cap, 6h interval) --- server/src/plugins/service-store/index.ts | 26 +++++++++-------- .../src/plugins/service-store/repository.ts | 28 +++++++++++++++++++ 2 files changed, 43 insertions(+), 11 deletions(-) diff --git a/server/src/plugins/service-store/index.ts b/server/src/plugins/service-store/index.ts index 7fe0803..8a46e4a 100644 --- a/server/src/plugins/service-store/index.ts +++ b/server/src/plugins/service-store/index.ts @@ -175,22 +175,26 @@ export class Plugin extends BSBService, typeof Event registerRepo(this._repo); - const purged = this._repo.purgeOldKioskLogs(2); - if (purged > 0) { - obs.log.info("purged {count} kiosk logs older than 2h", { count: purged }); - } + // Startup purge + this.runPurge(obs); obs.log.info("store ready"); } + private runPurge(obs: Observable): void { + if (!this._repo) return; + const r = this._repo; + const kl = r.purgeKioskLogs(14); + const el = r.purgeEventLog(30, 100_000); + const al = r.purgeAuditLog(90); + if (kl + el + al > 0) { + obs.log.info("purge: {kl} kiosk_logs, {el} event_log, {al} audit_log", { kl, el, al }); + } + } + async run(obs: Observable): Promise { - this.purgeTimer = setInterval(() => { - if (!this._repo) return; - const purged = this._repo.purgeOldKioskLogs(2); - if (purged > 0) { - obs.log.info("purged {count} kiosk logs older than 2h", { count: purged }); - } - }, 10 * 60 * 1000); + // Purge every 6 hours. + this.purgeTimer = setInterval(() => this.runPurge(obs), 6 * 60 * 60 * 1000); } async dispose(): Promise { diff --git a/server/src/plugins/service-store/repository.ts b/server/src/plugins/service-store/repository.ts index c78f7df..00da114 100644 --- a/server/src/plugins/service-store/repository.ts +++ b/server/src/plugins/service-store/repository.ts @@ -1722,6 +1722,34 @@ export class Repository { this.prep("UPDATE event_log SET forwarded_to_nodered = 1 WHERE id = ?").run(eventId); } + /** + * Delete event_log rows older than `days` AND trim to `maxRows` total. + * Returns the number of rows deleted. + */ + purgeEventLog(days: number = 30, maxRows: number = 100_000): number { + const cutoff = new Date(Date.now() - days * 86_400_000).toISOString(); + const r1 = this.db.prepare("DELETE FROM event_log WHERE received_at < ?").run(cutoff); + // Trim to maxRows by deleting oldest beyond the cap. + const r2 = this.db.prepare( + `DELETE FROM event_log WHERE id NOT IN ( + SELECT id FROM event_log ORDER BY received_at DESC LIMIT ? + )`, + ).run(maxRows); + return Number(r1.changes) + Number(r2.changes); + } + + purgeAuditLog(days: number = 90): number { + const cutoff = new Date(Date.now() - days * 86_400_000).toISOString(); + const r = this.db.prepare("DELETE FROM audit_log WHERE ts < ?").run(cutoff); + return Number(r.changes); + } + + purgeKioskLogs(days: number = 14): number { + const cutoff = new Date(Date.now() - days * 86_400_000).toISOString(); + const r = this.db.prepare("DELETE FROM kiosk_logs WHERE received_at < ?").run(cutoff); + return Number(r.changes); + } + queryEvents(filters: EventQueryFilters): { events: EventLog[]; total: number } { const where: string[] = []; const params: (string | number)[] = [];