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 <[email protected]>
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
+4 -3
View File
@@ -2,7 +2,7 @@
/**
* The settings surface.
*
* Six pages rather than one long scroll: the sections have nothing to do with
* Several pages rather than one long scroll: the sections have nothing to do with
* each other, and a person opening "API-tokens" should not have to walk past
* their own password to get there.
*
@@ -28,6 +28,7 @@
...(app.user?.is_admin
? [
{ href: '/indstillinger/brugere', label: 'Brugere' },
{ href: '/indstillinger/historik', label: 'Historik' },
{ href: '/indstillinger/fejl', label: 'Fejl' }
]
: []),
@@ -65,8 +66,8 @@
margin-bottom: var(--s5);
}
/* A scrolling strip rather than a wrapping row: six labels wrap to two lines on
a phone, and a two-line tab bar looks like a mistake. */
/* A scrolling strip rather than a wrapping row: this many labels wrap to two
lines on a phone, and a two-line tab bar looks like a mistake. */
nav {
display: flex;
gap: var(--s1);