Review: Players row edit: form action UPDATE + audit log in same transaction

review-1093-2026-04-25 Review

review ready

Verdict: READY

Board item #1093, Forgejo issue forgejo_admin/westside-admin#5. Backlog -> todo review gate.

Template Completeness

  • [x] Type: Feature
  • [x] Lineage (depends on players list #4)
  • [x] Repo
  • [x] User Story (story-westside-admin-admin-row-crud)
  • [x] Context
  • [x] File Targets (Create / Modify / Do NOT create)
  • [x] Acceptance Criteria (9 items)
  • [x] Test Expectations (3 integration + 1 manual)
  • [x] Constraints (with monthly_fee=DOLLARS reminder)
  • [x] Checklist
  • [x] Related

Traceability

  • [x] story:admin-row-crud — Admin Row CRUD (the WRITE half)
  • [x] story note verified — story-westside-admin-admin-row-crud exists; listed in project-westside-admin user-stories table
  • [x] arch:page-server label present
  • [ ] arch note coverage — No standalone arch-page-server note, but the page-server pattern is documented as part of arch-dataflow-westside-admin Flow 2 (sequence diagram covers SvelteKit Server -> BEGIN -> UPDATE -> INSERT audit -> COMMIT). The label is acting as a component pointer into the dataflow note rather than a separate node. Acceptable for v1 since the Flow 2 diagram is the spec; flag below.
  • [x] Forgejo issue — https://forgejo.tail5b443a.ts.net/forgejo_admin/westside-admin/issues/5, open

File Targets

Repo not cloned locally (greenfield SvelteKit project — westside-admin not yet bootstrapped). All Create targets are NEW files in conventional SvelteKit locations:
  • [x] src/routes/players/[id]/+page.server.ts — standard SvelteKit server module path
  • [x] src/routes/players/[id]/+page.svelte — standard SvelteKit page path
  • [x] src/lib/components/inputs/EnumSelect.svelte, JsonbEditor.svelte, DatePicker.svelte — conventional $lib component locations
  • [x] src/lib/server/audit.ts — server-only helper module path is correct ($lib/server is the SvelteKit private convention)
  • [?] src/routes/players/+page.svelte — Modify. This file is created by dependency ticket #4 (players list view). Modify directive is correct but only valid AFTER #4 lands. Captured under Dependencies.

Repo Placement

OK. All file targets are inside forgejo_admin/westside-admin, which matches the Forgejo issue location. Single-repo ticket.

Dependencies

  • Blocked by #4 (players list view) — explicitly stated in Lineage. The Modify target src/routes/players/+page.svelte does not exist until #4 ships. Do not start #1093 until #4 is in done.
  • Likely needs #1 (scoped DB helper) — story group includes arch:scopeddb-helper tickets (#1, #3) that produce the tenant_id-scoped Drizzle wrapper. The AC "tenant_id mismatch returns 404" + "WHERE id=123 AND tenant_id=1" in Flow 2 imply the scoped helper is the canonical access path. If #1 is not landed first, this ticket will inline a one-off scope check, which contradicts the helper's purpose. Recommend ordering: #6 (sveltekit-server bootstrap) -> #1 (scopeddb) -> #4 (list) -> #1093 (edit).
  • Auth dependency on #301/#1096 (keycloak) and #6/#1087 (sveltekit-server) — locals.user.email referenced in audit AC requires hooks.server.ts populating locals.user from Keycloak JWT (covered by #2 hooks-server ticket). Must land before #1093 to satisfy AC.

Acceptance Criteria

All 9 AC are testable and mapped to concrete behavior. Strongest pieces:
  • Transaction atomicity AC ("UPDATE + audit log INSERT happen in the SAME transaction (both or neither)") is paired with a Test Expectation that injects deliberate failure — verifiable by an agent.
  • Audit row shape is fully specified (actor, table, row_id, old_state, new_state, timestamp).
  • Tenant scoping returns 404 not 403 — security-conscious, prevents enumeration.
  • Sensitive column exclusion (contract_token NOT editable AND NOT shown) is concrete.
  • Progressive enhancement constraint (works without JS) is testable by disabling JS.
Minor gaps (non-blocking, can be tightened by Dev during implementation):
  • "flash message Saved" — mechanism not specified (cookie? URL param? load() return?). SvelteKit idiom is the &saved=1 query param or a flash cookie. Dev judgment OK.
  • "validates client-side and server-side using Drizzle column metadata" — reuse path from #4 list view validation is implied but not explicit; assume shared util.

Blast Radius

Greenfield project — no sibling consumers to break. However:
  • contract_audit_log table schema must already exist in the westside Postgres database. The ticket assumes this (writes to it without referencing a migration). Per the never_alter_prod_directly + never_write_prod_db conventions, if the table is not yet present, a separate migration ticket is required. Recommend Dev verify schema exists in step 1; if missing, ticket BLOCKS pending migration scoping. (Flag below as [SCOPE] for Ava to confirm pre-flight.)
  • This is the first legitimized write surface for westside DB. Per feedback_never_write_prod_db, this ticket replaces ad-hoc psql UPDATEs. Bug here = audit gap = compliance regression. Justifies extra QA rigor.
  • Drizzle.transaction() semantics — must use the tx parameter for both UPDATE and audit INSERT; using the outer db handle inside the callback silently breaks atomicity. Worth calling out in a code review checklist.

Decomposition Assessment

File count: 6 create + 1 modify = 7 files in 1 repo. AC count: 9. Estimated agent work: borderline 5–7 minutes for a focused dev agent (form action + 3 input components + audit helper + transaction logic + 3 integration tests).
Per skill 5-minute rule (>3 file targets across >2 repos OR >5 AC): this hits the >5 AC trigger and is at the file-count threshold within a single repo. Decision: do NOT decompose. Reasoning:
  • Single repo, single transaction unit — splitting would break the atomicity story.
  • The 3 input components (EnumSelect, JsonbEditor, DatePicker) are small and tightly coupled to the form. Pulling them into a sub-ticket creates coordination cost without benefit.
  • Audit helper is a single function (writeAudit) — natural fit alongside the transaction it serves.
If the dev agent runs over 5 minutes, the right split is a follow-up ticket for the 3 input components as reusable primitives, NOT a pre-emptive decompose now.

Recommendation

  • [SCOPE] Ava to confirm contract_audit_log table exists in westside DB before this ticket leaves next_up. If missing, scope a migration ticket (must land before #1093). Capture in Dependencies note on the issue if not already covered by #1 (scopeddb-helper) ticket body.
  • [BODY] Optional: add a one-line note in Constraints clarifying that Drizzle's db.transaction(async (tx) => { ... }) requires using tx (not the outer db) for both statements. Common footgun, worth pre-empting.
  • [BODY] Optional: clarify the "Saved" flash message mechanism (query param vs cookie vs load() return) — Dev judgment otherwise.
Verdict READY because: traceability complete, file targets conventional, dependencies clearly enumerated, AC testable, transaction atomicity is the core spec and is rigorously specified. The two [BODY] items are polish, not gates. The [SCOPE] item is a pre-flight check Ava owns at next_up promotion — it does not block todo.