Maintenance-jobs admin panel and audit-log viewer #86

Closed
opened 2026-07-04 14:52:46 +02:00 by fable-5 · 1 comment
Collaborator

Context

Operators need one glance for instance health: job outcomes and the audit trail (operations.md).

Scope

Admin area 'System': maintenance job list (name, cadence, last run, outcome, duration; manual trigger button per job), backup status card (from status.json via API), audit-log viewer (filterable by actor/action/time, paginated — reading the structured audit entries written since M1/M5), storage overview (per-pond usage top list).

Acceptance criteria

  • all registered jobs appear with truthful last-run data; manual trigger works and is itself audit-logged
  • backup card mirrors status.json incl. degraded state
  • audit viewer finds a fixture grant-change by actor
  • panel is Site-Admin-only (permission test)

Technical notes

  • operations.md §Maintenance jobs, security.md (audit).

Dependencies

Depends on #31, #83, #52.

Size: ~1.5 days


Conventions: English code/comments, clear human-readable code, no hard-coded UI strings (ADR 0012, add de and en), permission checks only via the shared guard (docs/architecture/permissions.md). Read the referenced ADRs before starting.

## Context Operators need one glance for instance health: job outcomes and the audit trail (operations.md). ## Scope Admin area 'System': maintenance job list (name, cadence, last run, outcome, duration; manual trigger button per job), backup status card (from status.json via API), audit-log viewer (filterable by actor/action/time, paginated — reading the structured audit entries written since M1/M5), storage overview (per-pond usage top list). ## Acceptance criteria - [ ] all registered jobs appear with truthful last-run data; manual trigger works and is itself audit-logged - [ ] backup card mirrors status.json incl. degraded state - [ ] audit viewer finds a fixture grant-change by actor - [ ] panel is Site-Admin-only (permission test) ## Technical notes - operations.md §Maintenance jobs, security.md (audit). ## Dependencies Depends on #31, #83, #52. **Size**: ~1.5 days --- *Conventions: English code/comments, clear human-readable code, no hard-coded UI strings (ADR 0012, add `de` **and** `en`), permission checks only via the shared guard (docs/architecture/permissions.md). Read the referenced ADRs before starting.*
fable-5 added this to the M8 — Self-hosting & operations milestone 2026-07-04 14:52:46 +02:00
fable-5 added the
frontend
backend
labels 2026-07-04 14:52:46 +02:00
Author
Collaborator

Implemented in c8aac13 (pipeline green, 8/8 contexts; migration verified live on Test + Int).

New Site-Admin panel /admin/system (linked from the admin settings page), four sections:

  • Maintenance jobs — every job registered with the #31 scheduler (trash-purge, version-thinning, page-compaction, data-export-purge) merged with its jobs row: cadence, last run, duration (new Job.lastDurationMs, recorded by the scheduler on every completion), outcome badge with the error on hover, and a manual trigger per job. The trigger (POST /admin/system/jobs/:name/run) skips the due-check but never the run-mutex (already_running instead of doubling up) and is itself audit-logged (job.triggered). A leftover DB row whose job no longer registers in the build is flagged instead of hidden.
  • Backup card — mirrors the #83 sidecar's status.json (read through the api's ro-mount from #85) including the freshness verdict with the same 26 h bound as readyz: fresh/stale badge, last success with backup id and sizes, last run outcome with the error, retention. Missing status.json → explicit "not available" state.
  • Audit-log viewer — filterable by actor (username), action (localized select over all known ids), and time range; paginated (50/page), newest first, with target and details.
  • Storage overview — total usage plus the top-20 ponds by pond_usage.storageBytesUsed, linked.

Persistent audit trail (the substrate the viewer needed): audit events existed only as pino stdout lines — unqueryable. New audit_log table (migration 20260711180000; actor FK SET NULL, indexes on at/actor/action) + central AuditService.record() which writes the row and keeps emitting the established audit: … log line; persisting failures are swallowed so auditing can never break the audited operation. Converted call sites — the security.md audit trail: grants (created/deleted), members (added/role_changed/removed), user admin (disable, resend, delete, site-admin, pseudonymized), quota admin (set/cleared), plugins (installed/mode/uninstalled/pond-toggle — actor threaded through the controllers; dropzone installs record without an actor), instance settings (key only, values stay out of the trail), setup (4 steps), auth (signup, verified, login success/failure, password reset). Content activity (pages, files, exports, labels) deliberately stays log-only. Action ids are stable dot-namespaced strings; the UI localizes them (de+en, new system namespace).

Acceptance criteria

  • all registered jobs appear with truthful last-run data; manual trigger works and is itself audit-logged — DB test: trigger runs trash-purge → status IDLE, lastRunAt + lastDurationMs set, job.triggered entry attributed to the admin; e2e: button click in the panel, row shows date + duration, audit table finds the entry.
  • backup card mirrors status.json incl. degraded state — DB test writes a 40 h-old fixture → available: true, fresh: false; fresh fixture → fresh: true; e2e renders the card in both worlds.
  • audit viewer finds a fixture grant-change by actor — DB test and e2e: grant created → filter by the acting username + grant.created → entry with pond target and role; a different actor filter returns nothing.
  • panel is Site-Admin-only — DB test: all five endpoints 403 for a regular user; e2e: the route guard redirects non-admins.

Tests: system-admin.e2e.db.test.ts (7) in the checks job, Playwright system.spec.ts (4) as a new CI step; the full api suite stays green with the retrofits. Live: audit_log + jobs.last_duration_ms confirmed on Test and Int after the deploy (migrations auto-applied), endpoints up and guarded.

Implemented in `c8aac13` (pipeline green, 8/8 contexts; migration verified live on Test + Int). **New Site-Admin panel `/admin/system`** (linked from the admin settings page), four sections: - **Maintenance jobs** — every job registered with the #31 scheduler (trash-purge, version-thinning, page-compaction, data-export-purge) merged with its `jobs` row: cadence, last run, **duration** (new `Job.lastDurationMs`, recorded by the scheduler on every completion), outcome badge with the error on hover, and a **manual trigger** per job. The trigger (`POST /admin/system/jobs/:name/run`) skips the due-check but never the run-mutex (`already_running` instead of doubling up) and is **itself audit-logged** (`job.triggered`). A leftover DB row whose job no longer registers in the build is flagged instead of hidden. - **Backup card** — mirrors the #83 sidecar's `status.json` (read through the api's ro-mount from #85) including the **freshness verdict** with the same 26 h bound as readyz: fresh/stale badge, last success with backup id and sizes, last run outcome with the error, retention. Missing status.json → explicit "not available" state. - **Audit-log viewer** — filterable by **actor** (username), **action** (localized select over all known ids), and **time range**; paginated (50/page), newest first, with target and details. - **Storage overview** — total usage plus the top-20 ponds by `pond_usage.storageBytesUsed`, linked. **Persistent audit trail (the substrate the viewer needed):** audit events existed only as pino stdout lines — unqueryable. New `audit_log` table (migration `20260711180000`; actor FK `SET NULL`, indexes on at/actor/action) + central `AuditService.record()` which writes the row **and keeps emitting the established `audit: …` log line**; persisting failures are swallowed so auditing can never break the audited operation. Converted call sites — the security.md audit trail: **grants** (created/deleted), **members** (added/role_changed/removed), **user admin** (disable, resend, delete, site-admin, pseudonymized), **quota admin** (set/cleared), **plugins** (installed/mode/uninstalled/pond-toggle — actor threaded through the controllers; dropzone installs record without an actor), **instance settings** (key only, values stay out of the trail), **setup** (4 steps), **auth** (signup, verified, login success/failure, password reset). Content activity (pages, files, exports, labels) deliberately stays log-only. Action ids are stable dot-namespaced strings; the UI localizes them (de+en, new `system` namespace). **Acceptance criteria** - *all registered jobs appear with truthful last-run data; manual trigger works and is itself audit-logged* — DB test: trigger runs trash-purge → status IDLE, lastRunAt + lastDurationMs set, `job.triggered` entry attributed to the admin; e2e: button click in the panel, row shows date + duration, audit table finds the entry. - *backup card mirrors status.json incl. degraded state* — DB test writes a 40 h-old fixture → `available: true, fresh: false`; fresh fixture → `fresh: true`; e2e renders the card in both worlds. - *audit viewer finds a fixture grant-change by actor* — DB test and e2e: grant created → filter by the acting username + `grant.created` → entry with pond target and role; a different actor filter returns nothing. - *panel is Site-Admin-only* — DB test: all five endpoints 403 for a regular user; e2e: the route guard redirects non-admins. Tests: `system-admin.e2e.db.test.ts` (7) in the checks job, Playwright `system.spec.ts` (4) as a new CI step; the full api suite stays green with the retrofits. Live: `audit_log` + `jobs.last_duration_ms` confirmed on Test and Int after the deploy (migrations auto-applied), endpoints up and guarded.
Sign in to join this conversation.
No project
No Assignees
1 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: stwaidele/dorfteich#86
No description provided.