phase-6: the staff console, and a log that says who did what
Flow H, three screens behind a role check: find an account, work the ingestion error queue, read and export the audit log. Superadmins can change a role, never their own. The error queue merges ingest errors and dead jobs into one table with a cursor that pages both sources; only a job can be retried and only an ingest row resolved, with a note that migration 003 gives it somewhere to live. writeAudit no longer defaults a missing subject to the actor, which had been recording a user search as staff looking themselves up. Omitting the subject still means acting on yourself; null now means the action has no subject, which is what a search, a retry and an export are. Reading the log is not audited. Exporting it is: a copy leaving the building is a different act from looking. e2e/global-setup.ts asks for every screen once before the suite starts, so a dev server's first-request compile is paid before the first test rather than by it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
6620650e9e
commit
4c39926483
@@ -536,3 +536,88 @@ tab, since every one of them is a place the user needs to reach.
|
||||
- **Payment reminders after filing.** FLOWS.md D3 mentions scheduling one on "Ya lo
|
||||
presente". The deadline sweep already covers T-2 and T-0 for the period; a separate
|
||||
payment date is not in RULES.md and was not invented.
|
||||
|
||||
## Phase 6
|
||||
|
||||
### The console is a separate route group with its own shell
|
||||
FLOWS.md Flow H asks for plain and dense, desktop first. `(admin)` gets a wide layout, a
|
||||
text nav and no tab bar, no floating scan button and no playfulness. The audit reminder
|
||||
sits above every screen in the group rather than only the user search: staff reading the
|
||||
error queue are reading user data too.
|
||||
|
||||
### The role is checked in the layout and again in every handler
|
||||
The layout calls `GET /me/session` and renders "solo para el equipo" for anyone else, which
|
||||
is a convenience. Every admin handler calls `requireRole` itself, which is the rule. A
|
||||
plain user who guesses the URL gets a plain page from the layout and a 403 from the API.
|
||||
|
||||
### SPEC-GAP: GET /me/session
|
||||
CONTRACTS.md section 3 has no way to ask who you are signed in as, and a console that must
|
||||
know a viewer's role before it renders needs one. It returns id, email and role, and
|
||||
nothing else.
|
||||
|
||||
### A search has no subject, and the log now says so
|
||||
`writeAudit` used to default a missing `subjectUserId` to the actor, which made a user
|
||||
search read as staff looking themselves up. `undefined` still means "acting on yourself";
|
||||
`null` now means "no subject", which is what a search, a retry, an error resolution and an
|
||||
export are. The distinction is a fact about what happened, so the log keeps it.
|
||||
|
||||
### Reading the audit log is not audited, exporting it is
|
||||
A row for every scroll of the audit screen would bury the accesses that matter under the
|
||||
act of looking for them. Taking a copy out of the building is a different act, so
|
||||
`GET /admin/audit/export.csv` writes `admin.audit_export` with the filters that produced it.
|
||||
|
||||
### The error queue merges two sources into one table
|
||||
Ingest errors and dead jobs are one queue with a `source` on each row, filtered and paged
|
||||
together by a `createdAt|id` cursor applied to both sides before the merge. Only a job row
|
||||
can be retried and only an ingest row can be resolved, and a dead job never appears under
|
||||
the resolved filter: retrying it takes it out of the queue instead. A manual retry resets
|
||||
`attempts` to zero, so the retry gets the whole backoff schedule rather than dying on its
|
||||
first stumble.
|
||||
|
||||
### SPEC-GAP: ingest_errors.resolution_note
|
||||
Flow H resolves an error "with note" and SPEC.md section 5 gives the row nowhere to put
|
||||
one. Migration 003 adds a column, because the note is what the next person reads, not
|
||||
another key inside the payload the failure wrote.
|
||||
|
||||
### Role changes are superadmin only and never on yourself
|
||||
The one thing worse than an account with too much power is the last superadmin demoting
|
||||
themselves out of the console. Setting the role a user already has is a 409 rather than a
|
||||
silent success. No session shuffling is needed: `getSession` reads the role off the user
|
||||
row on every request, so a demotion takes effect on the demoted user's next call.
|
||||
|
||||
### The audit table shows the action code, not a translated phrase
|
||||
It is the same token the filter takes and the same one in the CSV. An operator matching a
|
||||
log wants to see what they can search for, and fourteen action names in two languages would
|
||||
be copy that has to stay in step with an enum.
|
||||
|
||||
### Timestamps in the console are ISO, not prose
|
||||
`2026-09-04 20:42` rather than "4 de septiembre". A console sorts, compares and copies
|
||||
timestamps; the year is part of the fact and the format has to be unambiguous. Everywhere
|
||||
the user sees a date, it is still formatted for the reader.
|
||||
|
||||
### CONTRACTS.md 5.5 is pinned against the API, not the browser
|
||||
"Exactly one `admin.user_lookup` row per overview" is asserted in `admin.test.ts`, where it
|
||||
is exact. It cannot be asserted through Playwright: React strict mode mounts a client
|
||||
component twice in development, so the browser asks for the overview twice and writes two
|
||||
rows. The e2e asserts the flow writes a lookup that then shows up on the audit screen.
|
||||
|
||||
### Seed: one dead job
|
||||
CONTRACTS.md section 4 asks for two open ingest errors, which `seedDocuments` has written
|
||||
since phase 3. The dead job is an addition: the queue merges two sources and the retry
|
||||
action has nothing to act on without one. It also cost a test its assumption that the jobs
|
||||
table starts empty, which was an assumption worth removing anyway.
|
||||
|
||||
### Two defects the screenshots caught
|
||||
**The user summary read as the wrong pairs.** Label and value side by side across a wide
|
||||
card put each value next to the following pair's label, so "4123456-1" and "Rol" read as
|
||||
one field. Label above value fixed it.
|
||||
|
||||
**A search recorded itself against the searcher.** See above: visible only once real rows
|
||||
were on screen next to each other.
|
||||
|
||||
### Deferred, deliberately
|
||||
- **Impersonation and account freezing.** Neither is in SPEC.md or FLOWS.md. better-auth's
|
||||
admin plugin ships both; leaving them off is the smaller surface.
|
||||
- **Filtering the error queue by account.** The overview lists a user's own open errors and
|
||||
links to the queue. CONTRACTS.md gives `/admin/errors` a stage and status filter and no
|
||||
user filter, and one was not invented.
|
||||
|
||||
Reference in New Issue
Block a user