feat(audit): admin audit log (Notification P3)

Append-only who/what/when trail for admin actions + scheduler run summaries,
generalizing QuotaEvent. Logged at ACTION / run-summary granularity — never
per-row (a scheduler run touching thousands of videos is ONE row).

Backend:
- migration 0054_audit_log + AuditLog model (actor_id FK SET NULL, action,
  target_type/id, summary, before/after JSON, created_at).
- app/audit.py: AuditAction constants (single source of truth, i18n'd on the
  client) + record() (adds to the caller's txn) + gc(retention_days).
- producers wired: role change / suspend / delete / invite approve-deny-add /
  demo add-remove-reset / discovery purge (admin.py); config set/reset — secret
  VALUES never logged, key only (config.py); scheduler interval + maintenance
  tune (scheduler routes); sync pause/resume (sync.py); and the scheduler _job()
  choke point writes ONE run-summary row per run — manual runs + errors always,
  automatic runs only when they changed something (no no-op-poll spam).
- retention: audit_gc scheduler job prunes rows older than audit_retention_days
  (sysconfig, default 180; 0 = keep forever).
- admin API routes/audit.py (/api/admin/audit): recent-window list (actor emails
  resolved, System for background) + clear (leaves one tamper-evidence row).

Frontend:
- new admin-only "Audit log" nav page (ScrollText) using the shared DataTable
  (first admin page to reuse it) — sort/filter/paginate/persist, action select
  filter, actor text filter, before→after detail, Clear-all with confirm.
- AuditLog.tsx + auditColumns.tsx + api.adminAudit/clearAudit + AuditRow type.
- trilingual audit + auditActions namespaces (HU/EN/DE) + nav + config-group +
  scheduler job labels; Audit config group (retention days).
This commit is contained in:
npeter83 2026-07-12 07:32:41 +02:00
parent c1d38eea21
commit 318fdc4812
35 changed files with 720 additions and 29 deletions

View file

@ -0,0 +1,18 @@
{
"title": "Audit log",
"intro": "An append-only record of admin actions and scheduler run summaries — who did what, and when. Logged at action granularity, never per item.",
"empty": "No audit entries yet.",
"system": "System",
"clear": "Clear log",
"clearTitle": "Clear the audit log?",
"clearConfirm": "Permanently delete all audit entries? This can't be undone. (One entry recording that you cleared it is kept.)",
"cleared": "Cleared {{count}} audit entries",
"clearFailed": "Couldn't clear the audit log.",
"truncated": "Showing the {{returned}} most recent of {{total}} entries.",
"cols": {
"when": "When",
"actor": "Who",
"action": "Action",
"details": "Details"
}
}

View file

@ -0,0 +1,21 @@
{
"user_role_change": "Role changed",
"user_suspend": "User suspended",
"user_unsuspend": "User reinstated",
"user_delete": "User deleted",
"invite_approve": "Access request approved",
"invite_deny": "Access request denied",
"invite_add": "Email whitelisted",
"demo_add": "Demo email added",
"demo_remove": "Demo email removed",
"demo_reset": "Demo reset",
"discovery_purge": "Discovery purged",
"config_set": "Setting changed",
"config_reset": "Setting reset",
"scheduler_interval": "Job interval changed",
"maintenance_tune": "Maintenance tuned",
"sync_pause": "Sync paused",
"sync_resume": "Sync resumed",
"job_run": "Scheduler job run",
"audit_clear": "Audit log cleared"
}

View file

@ -12,7 +12,8 @@
"batch": "Batch sizes",
"explore": "Channel explore",
"downloads": "Downloads",
"plex": "Plex"
"plex": "Plex",
"audit": "Audit log"
},
"fields": {
"smtp_host": {

View file

@ -29,7 +29,8 @@
"about": "About",
"signOut": "Sign out",
"addAccount": "Add another account",
"switchTo": "Switch to {{name}}"
"switchTo": "Switch to {{name}}",
"audit": "Audit log"
},
"sync": {
"yours": "yours",

View file

@ -71,7 +71,8 @@
"download_gc": "Deletes downloaded files past their retention window, reclaims space no one is holding anymore, and warns owners before a shared file expires. If it stops, old downloads pile up and disk space isn't freed.",
"plex_sync": "Mirrors the enabled Plex libraries into the app's catalogue — the metadata behind Plex browsing, search, filters and the info pages (genres, cast, ratings, artwork). If it stops, newly added or changed Plex titles don't appear until the next sync.",
"plex_watch_sync": "Pulls recent Plex watch changes (finished episodes, resume positions) into Siftlode and re-pushes any local watch changes that didn't reach Plex. The cheap two-way keep-in-sync between full reconciles. If it stops, watched/resume made in the Plex app takes longer to show here.",
"plex_watch_reconcile": "A full watch-state reconcile that rescans every library — catches what the incremental pass can't, notably an episode un-marked as watched on the Plex side. Heavier, so it runs about once a day. If it stops, un-watches done in Plex aren't mirrored back."
"plex_watch_reconcile": "A full watch-state reconcile that rescans every library — catches what the incremental pass can't, notably an episode un-marked as watched on the Plex side. Heavier, so it runs about once a day. If it stops, un-watches done in Plex aren't mirrored back.",
"audit_gc": "Prunes admin audit-log entries older than the configured retention window (Configuration → Audit log). If it stops, the audit log just keeps growing — harmless but unbounded."
},
"jobs": {
"rss_poll": "RSS poll (new uploads)",
@ -87,7 +88,8 @@
"download_gc": "Download cleanup",
"plex_sync": "Plex library sync",
"plex_watch_sync": "Plex watch sync (incremental)",
"plex_watch_reconcile": "Plex watch reconcile (full)"
"plex_watch_reconcile": "Plex watch reconcile (full)",
"audit_gc": "Audit-log cleanup"
},
"queue": {
"recentPending": "Channels to sync",