feat(admin): what has been done on this server, across every project

The other half of the error log. That one is what broke; this is what was done,
and nobody could ask it: `ListActivity` takes one project id, and an
administrator is not necessarily a member of the project they need to look into.

Paged with a keyset cursor rather than an offset. The log is written to
constantly and read rarely, so a page fetched a minute after the one before it
would be shifted by everything written in between — one row shown twice and the
row behind it skipped, which is the single failure an audit log must not have.
Both halves of the cursor matter: the log is written in whole seconds, so a
timestamp alone cannot separate two rows written in the same one.

Migration 0009 makes `activity.user_id` SET NULL for the same reason 0008 did it
to `created_by`. A log whose rows vanish with the account they describe answers
"what has happened here" with everything except the part somebody had a reason to
remove. `project_id` keeps cascading on purpose: the trash is the undo, and past
it the deletion was deliberate.

The event names moved to `web/src/lib/events.js`, because two surfaces read the
same log now and two copies of nineteen strings is two places for them to drift.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Kristian Wind
2026-08-18 09:04:12 +02:00
co-authored by Claude Opus 5
parent ebdc8eacf2
commit 85f7b2488d
10 changed files with 996 additions and 56 deletions
+44
View File
@@ -0,0 +1,44 @@
/**
* What each recorded activity event is called.
*
* The keys are the server's and they are stable; this is the only place they
* become Danish. It lives in its own module because two surfaces read the same log
* now — a project's own history panel, and the administrator's instance-wide audit
* page — and two copies of nineteen strings is two places for them to drift apart.
*
* An unknown key falls through to the key itself rather than being hidden: a build
* that records something this table has not learned yet should still show that it
* happened.
*/
export const EVENTS = {
'project.created': 'oprettede projektet',
'project.updated': 'ændrede projektet',
'project.deleted': 'slettede projektet',
'project.imported': 'importerede projektet',
'section.created': 'oprettede en sektion',
'section.updated': 'ændrede en sektion',
'section.deleted': 'slettede en sektion',
'member.invited': 'inviterede',
'member.added': 'tilføjede',
'member.removed': 'fjernede',
'member.role_changed': 'ændrede rollen for',
'task.created': 'oprettede en opgave',
'task.updated': 'ændrede en opgave',
'task.completed': 'lukkede en opgave',
'task.reopened': 'genåbnede en opgave',
'task.moved': 'flyttede en opgave',
'task.deleted': 'slettede en opgave',
'task.split': 'delte en opgave op',
'comment.created': 'skrev en kommentar'
};
/** The name of an event, or the raw key when it is one this build does not know. */
export function eventName(event) {
return EVENTS[event] ?? event;
}
/** The bit of context an entry carries, when it has one worth reading. */
export function eventDetail(entry) {
const p = entry.payload ?? {};
return p.name ?? p.email ?? p.role ?? '';
}