westside-admin

westside-admin forgejo

Notes

Project Page 1
  • westside-admin project-westside-admin

    westside-admin

    Vision

    westside-admin is the data administration tool for the basketball-api Postgres database. It uses Drizzle ORM with live-introspected schemas to provide type-safe CRUD over visible tables and columns, gated by Keycloak admin role. Built as a SvelteKit adapter-node app so Drizzle queries run server-side and rows flow into components as typed data. Replaces direct psql access (banned per feedback_never_write_prod_db) and reduces dependency on basketball-api for admin-route work. Public URL: https://westside-admin.tail5b443a.ts.net.

    User Stories

    Key Story Note Role Success Metric
    admin-row-crud story-westside-admin-admin-row-crud Admin (Lucas, Marcus) Zero psql UPDATEs by Lucas + zero "fix X" texts from Marcus over 30 days

    One story for v1. Earlier draft had 5 stories; 3 of them (keycloak-admin-gate, tenant-scoping, schema-introspection) were re-classified as Safety Constraints (guarantees on the core story, not user-visible value). 2 of them (table-browser, row-edit) were two halves of the same story and collapsed into admin-row-crud.

    Safety Constraints

    These are guarantees enforced on the user story, not separate user stories. Every ticket on the board carries story:admin-row-crud; constraints are tracked here as project-level invariants.

    1. Keycloak admin gate. Cookie SSR auth via westside-basketball realm. JWKS validation per request. HttpOnly + Secure + SameSite=Lax token cookie — no Bearer in browser. Public Tailscale funnel permitted only because gate is airtight per feedback_funnel_requires_auth.
    2. Tenant scoping. Query helper wraps Drizzle db, auto-injects WHERE tenant_id = ? for tenant-scoped tables. v1: hardcoded TENANT_ID = 1. Direct unscoped db not exported. Lint or CI rule prevents bypass.
    3. Schema introspection. Drizzle schema generated from live Postgres via drizzle-kit pull; checked into git; CI fails on drift between checked-in schema and live DB.

    Architecture

    1. arch-domain-westside-admin — Domain Model (22 entities, relationships, sensitive columns)
    2. arch-dataflow-westside-admin — Data Flow (auth + page load + mutation sequences)
    3. arch-deployment-westside-admin — Deployment (k3s + Tailscale funnel + Keycloak + cross-ns Postgres)

    Key technical decisions inlined:

    • SvelteKit adapter-node (forced by Drizzle server-side requirement)
    • Cookie SSR auth (HttpOnly cookies, no keycloak-js in browser)
    • Public Tailscale funnel (matched to westside-app's accessibility model; admin gate satisfies funnel-auth rule)
    • Image-build pipeline: kaniko → Harbor → kustomize-tag bump → ArgoCD (matches every other westside-* and pal-e-* service in this cluster)
    • First feature: hand-rolled players list+edit; introspection-driven generic browser is the second iteration after the players seam ships

    Board

    Primary kanban: board-westside-admin

    Columns: Backlog → Todo (review gate) → Next Up → In Progress → QA → Done. Every ticket carries story:admin-row-crud + an arch: label + a type: label per template-ticket. Tickets span 4 repos (see Repos below); board is the single coordination point.

    Status

    2026-04-25: Project bootstrapped end-to-end at the docs layer. Project entity, project page, board, repo registration, 1 user story, 3 architecture notes — all created. Forgejo repo forgejo_admin/westside-admin created (empty, README only). 12 backlog tickets being filed across 4 repos. Local clone, scaffolding, deployment overlay, Keycloak client, k8s namespace, secrets — all not yet started.

    2026-05-03 (morning): M1 ACHIEVED. Three-layer bootstrap arc complete. Layer 1 (CI smoke check, westside-admin#13) → Layer 2 (PSA securityContext, pal-e-deployments#141) → Layer 3 (harbor-creds source-of-truth migration from SOPS-overlay to terraform, pal-e-deployments#145 + targeted tofu apply). Deployment 1/1 ready. ArgoCD Synced + Healthy. External funnel https://westside-admin.tail5b443a.ts.net returns HTTP 200 (was 502 since 2026-04-28). Validation notes: validation-12-2026-04-30, validation-137-2026-05-01, validation-143-2026-05-03. Five follow-on tickets filed during arc (#54, #139, #140, #142, #144) capturing every piece of discovered scope.

    2026-05-03 (afternoon): Auth foundation landed. Keycloak westside-admin confidential OIDC client created in westside-basketball realm via Playwright-driven admin console flow per sop-keycloak-client-creation (S256 PKCE, exact /auth/callback, front-channel logout ON, no realm-level side effects). westside-admin#2 (cookie SSR auth) decomposed into 4 sub-tickets per review-1132-2026-05-03 (5-min rule violation): #14 keycloak.ts lib, #15 hooks.server.ts, #16 auth endpoints, #17 403 page. #14 merged via PR #18 — 25/25 vitest cases. pal-e-deployments#147 (KEYCLOAK_CLIENT_SECRET) merged with kustomize-build evidence; ArgoCD reconciled the new secret into the cluster.

    2026-05-03 (evening): #2 CLOSED — auth foundation 4/4 shipped. All 4 decomposed sub-tasks landed on main: #15/PR #20 (handle hook with `/health` + `/auth/*` + `/__unauthorized` exclusions, layered cookie refresh, missing-admin internal rewrite), #16/PR #21 (PKCE-S256 OIDC code grant; state-validated-before-token-exchange CSRF protection; POST-only logout with Origin-header CSRF check; 502-with-no-upstream-body-relay on Keycloak failures), #17/PR #23 (`(unauthorized)` route group: `+layout@.svelte` reset + `+layout.server.ts` data persistence + `+page.server.ts` `error(403)` throw → `+error.svelte` rendered UI with sign-out form). End-to-end auth flow now testable against the live realm/secret stack: admin → cookie set → admin app; non-admin → 403 with sign-out. Two follow-ups in backlog: #19 (CI vitest gate — tests don't run on Woodpecker yet), #22 (extend KeycloakTokens with refresh_exp so cookie Max-Age tracks refresh window instead of access window). Standing scope: arch-keycloak + arch-westside-admin + arch-dev-overlay notes still missing — backfill is a deferred docs-layer task. Wave 4 feature work (Drizzle setup #1, tenant-scoped query helper #3, players list #4, players edit #5) now fully unblocked. Lucas to manually validate end-to-end SSO before items move from validation to done per feedback_validate_before_done.

    2026-05-04 (overnight): Dev overlay arc shipped to main. Marcus needed visibility into his data without waiting for image rebuilds, so a hostPath dev overlay was scoped on board-westside-admin. Two sequential tickets: pal-e-services#73 (Keycloak client) + pal-e-deployments#148 (kustomize overlay) — both MERGED. pal-e-services#73/PR #74 brought the live westside-admin client under tofu management (was created out-of-band in the realm) and added https://westside-admin-dev.tail5b443a.ts.net as a second redirect URI + web origin; required schema extension use_refresh_tokens (optional, default false, additive — verified zero-drift on 4 sibling clients) because the live client carries the Keycloak default true while the existing schema hardcoded false; targeted tofu apply ran post-merge — admin-API GET confirms both prod + dev URIs/origins live. pal-e-deployments#148/PR #149 added overlays/westside-admin/dev/ mirroring pal-e-dictionary/dev/ with three intentional deviations (port 5179, no funnel, PSA-restricted securityContext); 3 review passes caught a port collision with pal-e-projects-dev, the PSA admission requirement, and the ArgoCD path mismatch (the existing Application sources prod/ only; dev is intentionally manual-apply). Pending: manual kubectl apply -k overlays/westside-admin/dev/ from archbox + Lucas e2e validation as draneylucas@gmail.com.

    2026-05-05: #1 CLOSED — Drizzle ORM foundation merged (PR #30). Drizzle ORM setup complete: schema definitions, db client, drizzle.config.ts, migration infrastructure. Issue westside-admin#1 closed. M1 (deploy + auth + DB foundation) fully achieved. Board item moved to validation pending deployment verification. Next target: M2 (players list visible) — tenant-scoped query helper (#3), players list route (#4), players edit (#5).

    2026-05-05: #19 CLOSED — CI vitest gate merged (PR #32). Woodpecker test step now runs npm ci && npm test before kaniko build on every push. All future PRs are test-gated.

    2026-05-05: #3 CLOSED — Tenant-scoped Drizzle query helper merged (PR #34). scopedDb auto-injects WHERE tenant_id=1 on all tenant tables. 12 new unit tests. Grep-based lint prevents direct db imports.

    2026-05-05: #4 CLOSED — Players list view merged (PR #36). First user-visible feature: SSR table at /players with 10-column allowlist, pagination, name filter, EnumBadge/DateCell components. 70 tests, pure CSS. M2 milestone pending deployment validation.

    Milestones

    • M1: First admin login. App scaffolded + deployed + Keycloak gate working + blank dashboard renders. Validates auth + deploy seam end-to-end.
    • M2: Players list visible. Drizzle pulls schema, players table renders SSR, tenant scoping enforced. Validates DB seam.
    • M3: First row edit committed. Marcus successfully edits a player record without my involvement. Validates the value proposition.

    Repos

    Repo Platform Role Modified for westside-admin? Status
    forgejo_admin/westside-admin forgejo The SvelteKit app: code, Drizzle schema, hooks.server.ts, Dockerfile, .woodpecker.yaml Yes — 8 issues active (empty)
    forgejo_admin/pal-e-deployments forgejo Kustomize overlay overlays/westside-admin/prod/ (deployment, ingress with funnel, harbor-creds, namespace) Yes — 1 issue active
    forgejo_admin/pal-e-platform forgejo Terraform: Keycloak client westside-admin, Postgres admin_app user + grants on basketball db Yes — 2 issues active
    forgejo_admin/pal-e-services forgejo Service onboarding registration (if pattern requires it; TBD) Maybe — 1 issue deferred until needed active
    forgejo_admin/basketball-api forgejo Owns the Postgres DB schema westside-admin reads/writes (source of truth for drizzle-kit pull) No — read dependency only active
Doc 24
  • Scope Review (v2): westside-admin#29 (board item 1146) — Players edit route + transactional audit

    Verdict: APPROVED

    Re-review of body v2 (edited 2026-05-04, in response to v1 verdict NEEDS_REFINEMENT (minor)). All three minor refinements cleanly applied; no new gaps introduced. Greenlit by Lucas for backlog → todo.

    v2 changes verified

    1. GET-side leak check. Body now contains two matching artifacts:
      • AC line: GET /players/:id response body grep — contract_token value NEVER present in HTML
      • Test Expectations: Integration: GET response body grep — contract_token value never present in HTML
    2. old_state / new_state shape. Pinned in Constraints: "FULL ROW snapshots, not diffs. jsonb of every column on players (excluding contract_token) at the moment of UPDATE — both before and after." Removes ambiguity for the dev agent.
    3. arch-page-server backing note gap. Documented in Related as KNOWN PLATFORM GAP (not blocking #29); shared with #4; tracked as a separate Ava task. Correctly classified as non-blocker.

    Template Completeness

    • [x] Type, Lineage, Repo, User Story, Context, File Targets, AC, Test Expectations, Constraints, Checklist, Related, Edit Log — all 12 sections present.
    • [x] Edit Log section properly maintained (v1 + v2 entries).

    Traceability

    • [x] story:admin-row-crud label — story note story-westside-admin-admin-row-crud verified on project-westside-admin#user-stories table.
    • [x] arch:page-server label — backing note arch-page-server still missing in pal-e-docs (shared gap with #4, board item 1092). Behavior IS documented in arch-dataflow-westside-admin Flow 2, which IS listed on the project page architecture section. Non-blocker, tracked separately by Ava.
    • [x] Forgejo issue forgejo_admin/westside-admin#29 — open, body v2 last updated 2026-05-05T01:25:08Z.
    • [x] type:feature + split-from:5 labels present.

    File Targets

    • [x] ~/westside-admin/src/routes/players/[id]/+page.server.ts — verified greenfield (no players/ route exists yet).
    • [x] ~/westside-admin/src/routes/players/[id]/+page.svelte — verified greenfield.
    • [x] ~/westside-admin/src/lib/server/audit.ts — verified greenfield (src/lib/server/ currently contains only keycloak.ts + keycloak.test.ts).
    • [x] Modify ~/westside-admin/src/routes/players/+page.svelte — file ships in #4 (board item 1092, currently in todo); modification only valid after #4 merges. Already declared as a hard-dep in body Lineage. Correct.

    Repo Placement

    OK. All targets in forgejo_admin/westside-admin; matches the issue's ### Repo declaration and the board item's labels.

    Dependencies

    Hard deps #1, #3, #4, #28 — all open and correctly enumerated in body Lineage. Board state confirmed:

    • #1 (1089) — todo (scopedDb / Drizzle base)
    • #3 (1091) — todo (tenant-scoped scopedDb)
    • #4 (1092) — todo (players list — supplies the row in +page.svelte we mutate to make rows clickable)
    • #28 (1145) — backlog (input components — sibling, parallel)

    Cannot advance past todo until at least #1, #3, #4 clear (and ideally #28 to import inputs). Body is explicit about this.

    Acceptance Criteria

    11 ACs, all individually verifiable by an agent. The new contract_token grep AC is testable via integration test (response body assertion) — both AC and test are stated. monthly_fee dollars-not-cents constraint preserved from v1. Tenant-scoping 404-not-403 (existence-leak avoidance) preserved. Atomicity-by-failure-injection in checklist preserved.

    Blast Radius

    WRITE surface for players table. Audit log target contract_audit_log is the canonical mutation ledger. Replaces banned ad-hoc psql UPDATEs (feedback_never_write_prod_db). No sibling services consume this route — internal admin only. Helper signature writeAudit({ table, rowId, actor, sub, oldState, newState }, tx) is generic enough that future tables (#4 list rows, future row-edit routes) reuse it without re-design — good seam.

    Decomposition Assessment

    4 file targets, 11 AC. Already a decomposition (#5 split into #28 + #29 on 2026-05-04). Further splitting would create useless intermediate states (a +page.server.ts with no +page.svelte is not testable). Hold at this granularity. Estimated agent work: ~5 min — at the limit but justified by atomicity/leak/tenant requirements that must land together.

    Recommendation

    No action needed. APPROVED for backlog → todo move. Cannot advance to next_up / in_progress until #1, #3, #4 (and ideally #28) clear — that's a downstream concern, not a refinement gap.

    • v1 review: review-1146-2026-05-04 (NEEDS_REFINEMENT minor)
    • Forgejo v1 verdict comment: issue #29 comment 16229
    • Forgejo issue: forgejo_admin/westside-admin#29
    • Board item: 1146 on board-westside-admin
    • Sibling: #28 (board 1145) — input components, also backlog
    • Parent (closed): #5
  • Verdict: APPROVED

    Re-review of forgejo_admin/westside-admin#4 (board item 1092) after v2 body edits. The three v1 schema contradictions are resolved; new 10-column allowlist verified end-to-end against the live public.players table. No further refinements required — clear to advance from todo to next_up.

    Headline

    v2 reconciliation is complete. All ten allowlist column names exist on the live schema with the data types the ticket claims; monthly_fee rendering is corrected to whole-dollar; the explicit "Columns NOT in v1" callout closes the loop on the v1 mistakes.

    Live DB Verification (2026-05-04, post-NP fix)

    Queried information_schema.columns via kubectl -n basketball-api exec postgres-9b5b87b5-5nccx -- psql -U basketball -d basketball. Result: public.players has 37 columns; SELECT COUNT(*) = 58 rows. The 10 v2 allowlist columns matched as follows:

    # Column Live data_type Live is_nullable Ticket claim Match
    1 id integer NO integer, NOT NULL OK
    2 name character varying NO varchar, NOT NULL OK
    3 division USER-DEFINED YES USER-DEFINED enum, nullable OK
    4 position character varying YES varchar, nullable OK
    5 jersey_number character varying YES varchar, nullable OK
    6 jersey_size USER-DEFINED YES USER-DEFINED enum, nullable OK
    7 contract_status USER-DEFINED NO USER-DEFINED enum, NOT NULL OK
    8 subscription_status USER-DEFINED NO USER-DEFINED enum, NOT NULL OK
    9 monthly_fee integer YES integer DOLLARS, nullable OK
    10 created_at timestamp without time zone NO timestamp without time zone, NOT NULL OK

    Every name, type, and nullability matches the live schema. The three previously-broken columns (email, team_name, updated_at) are confirmed absent from public.players and correctly relocated to the "Columns NOT in v1" section with the right rationale (parents.email, player_teams M2M, only created_at exists).

    Template Completeness (Feature)

    • [x] Type — Feature
    • [x] Lineage — hard deps documented (#6, #1, #2, #3) plus indirect platform deps (#301, #302)
    • [x] Repo — forgejo_admin/westside-admin
    • [x] User Story — story-westside-admin-admin-row-crud
    • [x] Context — first end-to-end seam, justifies players-first scope
    • [x] v1 Column Allowlist — 10 cols, all schema-verified, with explicit NOT-in-v1 callout
    • [x] File Targets — Create + Modify + Do NOT Create lists
    • [x] Acceptance Criteria — 11 items, each verifiable
    • [x] Test Expectations — integration + unit + manual
    • [x] Constraints — scopedDb only, SSR only, pure CSS, allowlist inline, 10 cols, no JOINs
    • [x] Checklist — PR / screenshot / Lighthouse
    • [x] Related — project page, arch notes, dependency / blocks links
    • [x] Edit Log — v1 → v2 transition documented in body

    Traceability

    • [x] story:admin-row-crud label — present on board item 1092
    • [x] Story note verified — story-westside-admin-admin-row-crud exists, AC #1 ("Index page lists all visible tables...") and AC #4 ("paginated, type-aware grid... 50/page, filter inputs, enums as badges, dates formatted") are exactly the surface this ticket implements (READ half of the story)
    • [x] Story listed on project-westside-admin user-stories section (table-4000)
    • [x] arch:page-server label — present on board item 1092
    • [~] Arch note — no standalone arch-page-server note exists, but this is consistent with the rest of board-westside-admin (no per-component arch-X notes for scoped-db, hooks-server, keycloak, harbor either). The arch:page-server label references the page-server component within arch-dataflow-westside-admin Flow 1 (cited in the issue body's Related section). Inherited acceptable from v1 review per project convention.
    • [x] Forgejo issue — forgejo_admin/westside-admin#4, state OPEN, valid URL

    File Targets

    • [x] Create src/routes/players/+page.server.ts — parent src/routes/ exists; players/ subdir does not yet, will be created by the agent. OK.
    • [x] Create src/routes/players/+page.svelte — same, OK.
    • [x] Create src/lib/components/EnumBadge.svelte — parent src/lib/components/ exists (currently empty). OK.
    • [x] Create src/lib/components/DateCell.svelte — same, OK.
    • [x] Create src/lib/components/JsonbCell.svelte — same, OK. (Ticket explicitly notes JsonbCell is shipped now for parity with #29 even though v1 allowlist doesn't include a jsonb column.)
    • [x] Modify src/routes/+layout.svelte — file exists at expected path. OK.

    Repo Placement

    Issue is filed on forgejo_admin/westside-admin; all file targets are inside that repo. No cross-repo work. OK.

    Dependencies

    Hard deps: #6 (qa), #1 (todo), #2 (qa), #3 (todo). Of these, #1 and #3 are still in todo — both belong to arch:scoped-db, which is the helper this ticket consumes (scopedDb.select(allowlistedColumns).from(players)). The dev agent for #4 cannot start until #1 and #3 land. This blocker chain is correctly documented in the body's Lineage section ("Hard depends on #6, #1, #2, #3"); it does not invalidate scope but does mean #4 cannot advance to in_progress until #1 + #3 reach at minimum qa. Indirect platform deps (#301 done, #302 qa) are also tracked.

    Blocks: #29 (edit page) — correctly noted.

    Acceptance Criteria

    11 AC. Each has an objective verification path: AC1 (50 rows, id ASC) is a count + ordering assert; AC2 (10 columns) is a snapshot or column-count test; AC3 (pagination) is a URL test; AC4 (name filter) is a query-param test; AC5 (4 enum badges) is a render assert; AC6 (DateCell) is a render assert; AC7 ($NN) is a string-format test; AC8 (no contract_token in HTML) is a body-grep assert; AC9–10 (auth) inherit from #2's already-tested handlers; AC11 (58 rows live) is a manual visual confirm against the production DB. All testable.

    Blast Radius

    This is a NEW route (/players) on a NEW app (westside-admin) with no existing UI users beyond Lucas + Marcus. Risk is contained. Sensitive-column exclusion (contract_token) is enforced both at the allowlist level AND by integration test grep (AC8) — defense in depth. The ticket explicitly forbids JOINs to parents or teams in v1, which keeps the auth/scoping seam single-table and matches the scoped-db design (#1, #3). No similar routes exist elsewhere to drift from.

    Decomposition Assessment

    6 file targets, 11 AC, ~15-25 min of agent work. Slightly over the 5-minute rule, but: (a) already split — #4 list / #28 inputs / #29 edit; (b) further splitting fragments the load→render seam; (c) v1 review already accepted this. No further decomposition needed.

    Recommendation

    APPROVED — no action needed. v2 body resolves every [BODY] recommendation from the v1 review. Ticket is ready to advance from todo to next_up once its hard-dep chain (#1, #3) clears.

    • Prior review: review-1092-2026-05-04 (NEEDS_REFINEMENT, v1)
    • Forgejo issue: forgejo_admin/westside-admin#4 (state: open)
    • Board item: 1092 on board-westside-admin (column: todo)
    • Story: story-westside-admin-admin-row-crud
    • Project: project-westside-admin
  • Scope Review v2: westside-admin#28 (board item 1145) — Players edit form: input components

    Verdict

    APPROVED. v2 body addresses all three blockers from review-1145-2026-05-04 (v1, NEEDS_REFINEMENT). Greenlit by Lucas for backlog→todo on this review.

    What changed in v2 (verified)

    1. Test approach pivoted to pure helpers. Body now extracts parseJsonOnBlur, isoToDateInput, dateInputToIso into src/lib/components/inputs/helpers.ts and tests them in helpers.test.ts. Verified achievable: vitest.config.ts at ~/westside-admin/vitest.config.ts sets environment: 'node'; package.json has neither @testing-library/svelte nor jsdom/happy-dom. The existing src/lib/server/keycloak.test.ts is precedent for pure-logic vitest tests in node env. Component-render visual sanity is correctly scoped to a temp scratch page in PR description (not committed).
    2. #1 (Drizzle) downgraded hard-dep → soft-dep. Leaf input components don't import Drizzle types; the metadata-to-component dispatcher lives in #29's +page.svelte. #28 can ship before or independently of #1. Confirmed by reading File Targets (no schema.ts or db imports).
    3. Contradictory /dev/inputs checklist item dropped. Body and Checklist now consistent: no committed dev route; visual sanity = temp scratch page screenshot in PR description.

    Template Completeness (template-issue-feature)

    • [x] Type — Feature
    • [x] Lineage — split from #5; soft-dep #1; blocks #29; depends-on #6 (closed)
    • [x] Repo — forgejo_admin/westside-admin
    • [x] User Story — story-westside-admin-admin-row-crud
    • [x] Context — Svelte 5 runes, pure CSS, vitest node-env, helper-based test approach explained
    • [x] File Targets — 3 components + 1 helpers.ts + 1 helpers.test.ts; explicit Do-NOT list
    • [x] Acceptance Criteria — 10 testable criteria
    • [x] Test Expectations — 9+ vitest cases on helpers; no DOM tests
    • [x] Constraints — Svelte 5 runes, pure CSS, no Tailwind, no DOM env additions
    • [x] Checklist — PR opened, vitest passes, manual visual sanity in PR desc
    • [x] Edit Log — v1 and v2 entries present

    Traceability

    • [x] story:admin-row-crud — verified on project-westside-admin user-stories table (block 35117)
    • [x] story note — story-westside-admin-admin-row-crud entry present
    • [ ] arch:svelte-components — label present, but arch-svelte-components note still MISSING in pal-e-docs (shared gap with #29; carried over as soft note from v1, not a blocker since user explicitly greenlit the backlog→todo move)
    • [x] Forgejo issue — open at forgejo_admin/westside-admin#28

    File Targets (against ~/westside-admin)

    • [x] src/lib/components/inputs/EnumSelect.svelte — directory does not yet exist; Create-only path is correct (verified via ls ~/westside-admin/src/lib/components/)
    • [x] src/lib/components/inputs/JsonbEditor.svelte — same, clean greenfield
    • [x] src/lib/components/inputs/DatePicker.svelte — same, clean greenfield
    • [x] src/lib/components/inputs/helpers.ts — Create
    • [x] src/lib/components/inputs/helpers.test.ts — Create. Pattern matches src/lib/server/keycloak.test.ts (vitest, node env)
    • [x] Convention checks: +layout.svelte uses let { children } = $props(); — Svelte 5 runes confirmed as repo convention

    Repo Placement

    OK. Single-repo change in forgejo_admin/westside-admin. No cross-repo coordination required.

    Dependencies

    • Hard depends on #6 (closed, scaffolding done) — satisfied
    • Soft on #1 (Drizzle, todo column) — leaf components don't use Drizzle types; not blocking
    • Blocks #29 (Players edit route) — sibling backlog item 1146
    • No in-progress board items conflict with this scope

    Acceptance Criteria

    10 ACs, all testable. Helper-coverage criterion ("9+ vitest cases in helpers.test.ts: 3 per helper including edge cases") is concretely verifiable. Component-render ACs (EnumSelect renders all options, etc.) are visually verifiable from the temp scratch page screenshot in PR description. CustomEvent dispatch ACs on JsonbEditor are testable indirectly via helper unit tests on parseJsonOnBlur (the helper returns {valid, error?}; the component just relays).

    Blast Radius

    Greenfield directory src/lib/components/inputs/. No existing imports to break. Downstream consumer is #29 only.

    Decomposition Assessment

    5-min envelope passes. 5 files in one repo, all under src/lib/components/inputs/, ~9-12 unit tests. Three small Svelte components + one helper module + one test file. No cross-cutting concerns. No decomposition needed.

    Recommendation

    No action needed. APPROVED for backlog→todo (per Lucas's standing greenlight on this review).

    Soft note carried forward (not blocking): [SCOPE] Create arch-svelte-components architecture note in pal-e-docs as a separate ticket — shared gap with #29.

    • Prior verdict: review-1145-2026-05-04 (v1, NEEDS_REFINEMENT) — Forgejo comment 16227
    • Forgejo issue: forgejo_admin/westside-admin#28
    • Board item: 1145 on board-westside-admin
    • Sibling: forgejo_admin/westside-admin#29 (board 1146) — consumes these components
  • Scope Review: westside-admin#29 (board item 1146) — Players edit route + transactional audit

    Verdict

    NEEDS_REFINEMENT (minor) (refinements applied in body v2 on 2026-05-04; re-review pending). Forgejo comment issue #29 comment 16229 is the canonical record.

    Three minor refinements

    1. [BODY] GET-side leak check. Added explicit AC + integration test that contract_token value never appears in /players/:id HTML body. Existing WRITE-side guard (form excludes the column) was correct but didn't enforce READ-side.
    2. [BODY] old_state / new_state shape. Pinned as full-row snapshots, not diffs, in Constraints. Removes ambiguity for the dev agent.
    3. [SCOPE — non-blocker] arch-page-server backing note doesn't exist in pal-e-docs. Shared gap with #4 (board item 1092). Filed as a separate Ava task; thin pointer to arch-dataflow-westside-admin Flow 2 should suffice.

    Other findings

    • Template completeness: all 11 template-issue-feature sections present.
    • Traceability: story:admin-row-crud backing note exists. arch:page-server backing absent (gap above). Behavior IS documented in arch-dataflow-westside-admin Flow 2.
    • File targets verified: all 4 Create paths greenfield in ~/westside-admin. Modify target (src/routes/players/+page.svelte) ships in #4 — modification only valid after #4 merges (already declared as hard-dep).
    • Dependencies: #1, #3, #4, #28 all open. Documented correctly in body. Cannot advance past todo until those clear.
    • Decomposition: 4 files, 11 AC. Already a decomposition (split-from #5 → #28 + #29). Hold; further splitting creates useless intermediate states.

    Outage Note

    Filed retroactively after 7-min pal-e-docs outage; Forgejo comment 16229 is the real-time PM record.

    Re-review

    Body v2 edits are minor refinements (3 small additions). Will dispatch a fresh review-ticket agent for confirmation; expect quick turnaround to APPROVED. Once APPROVED, file review-1146-2026-05-04-v2 note.

    • Forgejo issue: forgejo_admin/westside-admin#29
    • Board item: 1146 on board-westside-admin
  • Scope Review: westside-admin#28 (board item 1145) — Players edit form input components

    Verdict

    NEEDS_REFINEMENT (refinements applied in body v2 on 2026-05-04; re-review pending). Forgejo comment issue #28 comment 16227 is the canonical record.

    Three blocking refinements

    1. Test infrastructure gap (the dominant scope risk). Repo's vitest stack today is node env only (vitest.config.ts: environment: 'node'); no @testing-library/svelte, no jsdom/happy-dom in package.json. The component-render ACs (EnumSelect renders all options, JsonbEditor parses on blur, DatePicker accepts ISO) all require component-mounting tests, which the current setup cannot run.
      Resolution applied: refactored AC to extract pure helpers (parseJsonOnBlur, isoToDateInput, dateInputToIso) and test those logic-only via vitest in node env. Visual rendering verified manually when #29 wires components into the edit page.
    2. Lineage overstates dep on #1. The leaf input components reference no Drizzle types. Drizzle-metadata-to-component dispatcher lives in #29's +page.svelte, not here.
      Resolution applied: downgraded #1 from hard-dep to soft-dep. #28 can ship before or independently of #1.
    3. Self-contradicting checklist. Body said "Do NOT create routes" but Checklist asked for a /dev/inputs dev route.
      Resolution applied: dropped the dev route; visual sanity moves to a temp scratch page in PR description (not committed).

    Soft notes

    • Story/arch backing notes verified after pal-e-docs recovery (story present in project-westside-admin; arch-svelte-components not present — shared gap, see #29 review).
    • Label-vs-slug nit (story:admin-row-crud vs story-westside-admin-admin-row-crud) — not a refinement, both forms valid per feedback_traceability_triangle.

    Decomposition

    5-min envelope passes with the helper-extraction approach: 3 components + 1 helpers.ts + 1 helpers.test.ts = 5 files in one repo, ~9-12 unit tests, all under src/lib/components/inputs/.

    Outage Note

    Original review agent flagged pal-e-docs outage in real-time; that's where the postgres NP gap was first identified.

    Re-review

    Body v2 edits are material; will dispatch a fresh review-ticket agent. Once APPROVED, file review-1145-2026-05-04-v2 note.

    • Forgejo issue: forgejo_admin/westside-admin#28
    • Board item: 1145 on board-westside-admin
  • Scope Review: westside-admin#4 (board item 1092) — Players list view at /players

    Verdict

    NEEDS_REFINEMENT (refinements applied in body v2 on 2026-05-04; re-review pending). Forgejo comment issue #4 comment 16214 is the canonical record.

    Headline

    The 2026-05-04 v1 PM-locked column allowlist was internally coherent on paper but contradicted the live players schema in three places. Three of the ten allowlisted columns (email, team_name, updated_at) do not exist as columns on the players table.

    Three columns broken in v1 allowlist

    1. email — lives on parents.email, not players. Rendering requires JOIN, contradicts the constraint scopedDb.select(allowlistedColumns).from(players).
    2. team_name — does not exist on players. Teams link via player_teams M2M to teams.name. Needs JOIN + semantic decision for multi-team players.
    3. updated_at — does not exist on players. Only created_at.

    Other findings

    • Live DB verified: 37 columns confirmed (37th is jersey_number_preferred), 58 rows, contract_token present (sensitive), division + contract_status are USER-DEFINED enums, monthly_fee is integer DOLLARS.
    • Repo layout: all File Target Create paths valid; nav modify target (src/routes/+layout.svelte) exists.
    • Dependencies: #6, #2, #301, #302 done; #1 + #3 in todo (hard-dep chain holds).
    • Decomposition: 6 files, 11 AC, ~15-25 min. Slightly over 5-min rule but splitting further fragments the load→render seam. Hold as-is (already split: #4 list / #28 inputs / #29 edit).

    Required body edits (applied in v2)

    • [BODY] Reconcile each broken column. Applied: dropped email/team_name, swapped updated_atcreated_at, added position + subscription_status.
    • [BODY] monthly_fee precision: AC said $NN.NN but column is integer DOLLARS — applied: changed to $NN (whole dollars).
    • [BODY] Edit Log section now records v1→v2 transition.

    Outage Note

    Filed after 7-min pal-e-docs outage. Review agent flagged the postgres NetworkPolicy gap as the outage root cause; that's tracked separately as a retroactive Forgejo issue.

    Re-review

    Body v2 edits are material; will dispatch a fresh review-ticket agent. Once APPROVED, file review-1092-2026-05-04-v2 note.

    • Forgejo issue: forgejo_admin/westside-admin#4
    • Board item: 1092 on board-westside-admin
  • Scope Review: westside-admin#3 (board item 1091) — Tenant-scoped Drizzle query helper

    Verdict

    APPROVED. Forgejo comment issue #3 comment 16208 is the canonical record.

    Findings (summary)

    • Template completeness: all template-issue-feature sections present.
    • File targets verified: tenant.ts, scopedDb.ts not yet in ~/westside-admin/src/lib/server/. Repo scaffolding (#6) closed.
    • Traceability: story:admin-row-crud + arch:scoped-db labels consistent across siblings. Backing notes verified after pal-e-docs recovery.
    • Decomposition: not needed. 4 files, 6 contained ACs, single repo. Within 5-minute rule.
    • Dependencies: hard-deps on #1 (in todo) and #6 (closed). Blocks #4 and #29.

    Non-blocking recommendations

    1. [BODY] Anchor AC #3 (TypeScript distinguishes tenant vs. system tables) to a concrete expectTypeOf test against alembicVersion — prevents the yak-shaving risk PM flagged.
    2. [BODY] Add eslint.config.js to File Targets > Modify alongside package.json — canonical enforcement for the AC #4 lint check is no-restricted-imports, not just an npm grep script.

    Sequencing note

    Hard-dep #1 must merge before #3 starts. Recommend serializing #1 → #3 rather than parallel dispatch.

    Outage Note

    Filed retroactively after 7-min pal-e-docs outage; Forgejo comment 16208 is the real-time record.

    • Forgejo issue: forgejo_admin/westside-admin#3
    • Board item: 1091 on board-westside-admin
  • Scope Review: westside-admin#1 (board item 1089) — Drizzle setup + drizzle-kit pull schema.ts

    Verdict

    READY (equivalent to APPROVED for the todo → next_up gate). Forgejo comment issue #1 comment 16201 is the canonical record.

    Findings (summary)

    • Hard dependencies all closed: westside-admin#6 (scaffold, 0b7b313), westside-admin#2 (auth, 982df5b..63e708d), pal-e-platform#302 (admin_app PG user) — closed; verified DB privileges hold.
    • File targets verified greenfield in ~/westside-admin: drizzle.config.ts, src/lib/server/db.ts, src/lib/server/schema.ts all absent; src/lib/server/ contains only keycloak.ts + keycloak.test.ts (per #6); scripts/ exists with .mjs helpers.
    • Live DB shape matches ticket claims: 22 tables, 18 enums, 1 row in tenants, 58 rows in players, 37 columns on players, contract_token present, tenant_id on tenant tables.
    • Decomposition: 5 files / 6 ACs / 15–25 min — exceeds 5-min rule but irreducible (every file consumes the others). Single ticket; agent commits incrementally.
    • Downstream chain (#3, #4, #28, #29) all correctly declare hard-dep on #1.

    Non-blocking nits

    • [BODY] Constraints reference "match the connection pool pattern westside-app uses" — westside-app is adapter-static with no server DB. The actual sibling pg.Pool pattern lives in westside-contracts/src/lib/db.ts. Reference-only fix; doesn't change implementation.

    Outage Note

    This review note was filed retroactively at 12:38 UTC after a 7-minute pal-e-docs outage (postgres NetworkPolicy missing pal-e-docs allow rule, fallout from commit c6a138d). The review agent ran during the outage and held the note for backfill; Forgejo comment 16201 was posted in real-time and is the dispositive record.

    • Forgejo issue: forgejo_admin/westside-admin#1
    • Board item: 1089 on board-westside-admin
    • Skill: skill-review-ticket
    • Template: template-review
  • Validation: pal-e-platform#301 — Keycloak client + SOP

    Ticket

    • Forgejo: forgejo_admin/pal-e-platform#301 — Create westside-admin Keycloak client + document SOP
    • Board item: board-westside-admin id=1096
    • Shipped: OIDC client westside-admin exists in realm westside-basketball; SOP sop-keycloak-client-creation exists in pal-e-docs.

    Environment

    • Keycloak: https://keycloak.tail5b443a.ts.net, realm westside-basketball
    • Consuming app: https://westside-admin.tail5b443a.ts.net (running off image harbor.tail5b443a.ts.net/westside-admin/app:63e708d)
    • pal-e-docs MCP: get_note_toc(slug="sop-keycloak-client-creation")

    Checks

    # Criterion (from #301) How to Verify Result Evidence
    1 OIDC client westside-admin exists in realm westside-basketball Hit Keycloak admin API GET /admin/realms/westside-basketball/clients?clientId=westside-admin PASS API returned 1 client with id 19bfe0df-7fbc-463c-97df-59d77901421e
    2 Standard Flow enabled, Direct Access Grants disabled, PKCE S256 enforced Live OIDC redirect: navigate to https://westside-admin.tail5b443a.ts.net/ PASS 302 chain redirected to code_challenge_method=S256 + response_type=code + scope=openid profile email; password grant rejected with unauthorized_client | Client not allowed for direct access grants
    3 Valid Redirect URI https://westside-admin.tail5b443a.ts.net/auth/callback Inspect login redirect URL; complete login round-trip via Playwright PASS Redirect URI in OIDC params matches; full login flow completed at 12:09 UTC, cookie set, page rendered
    4 Confidential client with KEYCLOAK_CLIENT_SECRET wired into deployment kubectl -n westside-admin get secret westside-admin-secrets -o json PASS Secret has 5 keys including KEYCLOAK_CLIENT_SECRET; pod env consumes it; live login proves cluster-side credential is valid
    5 SOP note sop-keycloak-client-creation exists in pal-e-docs get_note_toc(slug="sop-keycloak-client-creation") PASS TOC returned with sections: Purpose, Background, Prerequisites, Steps, OIDC Client Config Summary, State Parameter / CSRF Requirement, Secret Handling, Rules, Related
    6 SOP tagged sop,active SOP injected at session start lists slug among active SOPs PASS Session-start board sync injection includes SOP: Keycloak Client Creation (Admin Console) (slug: sop-keycloak-client-creation)

    Verdict

    PASS — both deliverables (Keycloak client + SOP) are live and exercised. The client is the same one currently serving production logins for westside-admin; the SOP is queryable via MCP and correctly documents the manual admin-console procedure.

    Discovered Issues

    None. One observation worth noting (not a blocker): the original ticket discussed both public and confidential client modes; the implemented client is confidential (matching the existing westside-app pattern). The SOP correctly documents both options.

    Notes

    This validation note was filed retroactively on 2026-05-04 during PM review of board-westside-admin TODO column. The work shipped earlier without the gate, but the deliverables stand independent verification today. Future moves to done on this board will go through the validation column live to avoid retroactive filings.

  • Ticket

    forgejo_admin/westside-admin#24 (PR #25, merged). P1 BUG: session cookie value (4719 B with id_token) exceeded browser per-cookie limit, causing infinite redirect loop. Fix: drop id_token from session payload.

    Environment

    • URL: https://westside-admin.tail5b443a.ts.net
    • Cluster image SHA at validation: 63e708dc (post-#25 plus #27 azp fix)
    • Test user: draneylucas@gmail.com

    Checks

    # Criterion How to Verify Result Evidence
    1 session cookie value < 4096 bytes curl auth flow → capture Set-Cookie len PASS Live measurement: 3151 bytes (was 4719 B pre-fix). 945-byte headroom.
    2 browser keeps the cookie through the redirect chain Playwright headless Chromium SSO round-trip PASS Browser landed at /, page rendered <h1>westside-admin</h1>. No redirect loop.
    3 logout still works without id_token_hint POST /auth/logout, observe redirect to Keycloak SLO PASS Per validation-16-2026-05-03 check #4 — Keycloak /logout redirect still fires; id_token_hint param omitted (acceptable per OIDC spec)
    4 no regression on /auth/callback failure paths Static QA review of PR #25 PASS State validation, code exchange, error mapping unchanged

    Verdict

    PASS — bug no longer reproduces. SSO round-trip terminates cleanly.

    Discovered Issues

    Bug #26 (JWT aud=account vs expected westside-admin) was unmasked by this fix — the cookie-size loop hid it. Bug #26 was filed, fixed in PR #27, and validated in validation-26-2026-05-03 (or via the integrated validation in validation-15).

  • Ticket

    forgejo_admin/westside-admin#17 (PR #23, merged). The (unauthorized) route group renders a 403 page when an authenticated user lacks the admin realm role.

    Environment

    • URL: https://westside-admin.tail5b443a.ts.net
    • Cluster image SHA at validation: 63e708dc

    Checks

    # Criterion How to Verify Result Evidence
    1 Route files exist on the deployed image Forgejo source at SHA 63e708dc PASS +layout@.svelte, +layout.server.ts, +page.server.ts, +error.svelte, +page.svelte all present per PR #23 diff
    2 Build succeeds; route is wired npm run build at PR #23 PASS entries/pages/(unauthorized)/__unauthorized/_error.svelte.js present in build output (per PR #23 dev report)
    3 Hook rewrite to /__unauthorized matches the page's path hooks.server.ts UNAUTHORIZED_PATH constant + (unauthorized)/__unauthorized/+page.server.ts route PASS Static review verified path coordination across PR #20 (hook) and PR #23 (page)
    4 Live missing-admin user sees the 403 page Sign in with a non-admin Keycloak user, observe URL + page DEFERRED No non-admin Keycloak user exists in the realm yet (only Lucas+admin set). Creating a separate non-admin test user is out of scope for tonight's M1 close-out — file follow-up.

    Verdict

    PASS — wiring + build + static analysis confirm the route is in place. The dynamic missing-admin user-experience flow is the only AC not exercised; deferred via a separate validation task once a non-admin test user is provisioned.

    Discovered Issues

    Need a non-admin Keycloak realm user for full e2e validation of the missing-admin path. File a follow-up: "create a non-admin test user in westside-basketball realm; re-validate westside-admin#17 missing-admin path." Low priority — the static + hook coordination evidence already proves the wiring.

  • Ticket

    forgejo_admin/westside-admin#16 (PR #21, merged). OIDC code-grant + PKCE auth endpoints + RP-initiated logout.

    Environment

    • URL: https://westside-admin.tail5b443a.ts.net
    • Cluster image SHA at validation: 63e708dc
    • Test user: draneylucas@gmail.com

    Checks

    # Criterion How to Verify Result Evidence
    1 /auth/login generates fresh state + PKCE, sets transient cookie, 302 to Keycloak curl -i /auth/login PASS 302 to Keycloak /authorize with client_id=westside-admin, redirect_uri (exact match), response_type=code, scope=openid+profile+email, state, code_challenge_method=S256, code_challenge. Set-Cookie: westside_admin_state=...; Path=/auth/callback; HttpOnly; Secure; SameSite=Lax; Max-Age=600.
    2 /auth/callback validates state BEFORE token exchange Code review at deployed SHA + behavioral test (state mismatch returns 400 without /token POST) PASS QA review of PR #21 verified ordering. Full SSO round-trip with valid state succeeded.
    3 /auth/callback exchanges code, sets session cookie, clears transient cookie, 302 to redirect Live SSO round-trip; observe Set-Cookie + Location PASS Set-Cookie: westside_admin_session (3151 B, path=/, HttpOnly, Secure, SameSite=Lax) + westside_admin_state cleared (Max-Age=0). Location: /
    4 /auth/logout POST clears cookie + 302 to Keycloak SLO POST /auth/logout with Origin header PASS Live test 2026-05-03 21:43Z: status 302, Location: https://keycloak.tail5b443a.ts.net/realms/westside-basketball/protocol/openid-connect/logout?post_logout_redirect_uri=https%3A%2F%2Fwestside-admin.tail5b443a.ts.net%2F. Set-Cookie: westside_admin_session=; Max-Age=0.
    5 /auth/logout CSRF protection: no-Origin POST returns 403 POST /auth/logout without Origin header PASS Live test: status 403
    6 redirect URI matches Keycloak client config exactly Compare /auth/login's redirect_uri param to Keycloak client's "Valid redirect URIs" PASS Both: https://westside-admin.tail5b443a.ts.net/auth/callback
    7 token POST failure → 502 with no upstream body relay QA review verified static path; not triggered live PASS (static) QA verdict + code review at PR #21

    Verdict

    PASS — all three endpoints validated end-to-end (login + callback + logout) plus CSRF defense.

    Discovered Issues

    None new during this validation. Bug #26 (jwt aud) was discovered in the same broader sweep but is technically a verifyKeycloakJwt issue (lib level), not an endpoint issue.

  • Ticket

    forgejo_admin/westside-admin#15 (PR #20, merged). Implements the per-request `handle` hook that reads the encrypted session cookie, validates the JWT against the JWKS, populates event.locals.user, and enforces the admin realm role.

    Environment

    • URL: https://westside-admin.tail5b443a.ts.net
    • Cluster image SHA at validation: 63e708dc (after #25 cookie-size fix and #27 azp-acceptance fix)
    • Test user: draneylucas@gmail.com (realm-roles include admin)

    Checks

    # Criterion How to Verify Result Evidence
    1 Anonymous request to / 302s to /auth/login?redirect=/ curl unauthenticated GET / PASS Live curl: status 302, Location: /auth/login?redirect=%2F
    2 Valid session cookie + admin role passes through Full SSO round-trip → GET / with cookies PASS GET / returned 200 with <h1>westside-admin</h1>; Playwright headless Chromium also confirmed (no redirect loop)
    3 /health bypasses auth gate (k8s probes) Pod readinessProbe hits /health unauthenticated PASS Pod has been Ready for 76s+ post-roll; readinessProbe hits /health and got 200
    4 /auth/* bypasses auth gate GET /auth/login (no cookie) returns 302 to Keycloak (not to itself) PASS Live trace shows /auth/login → Keycloak /authorize, no recursive /auth/login
    5 tampered/expired/wrong-key cookie treated as anonymous Implicit: previously-broken state had this exact behavior — wrong-aud rejection routed to anonymous redirect path (logged in #26's repro) PASS Bug #26 reproduced this code path: hook caught JwtVerificationError, deleted cookie, 302 to /auth/login. No 5xx ever observed.
    6 cookie attributes correct (HttpOnly+Secure+SameSite=Lax+Path=/) Inspect Set-Cookie on /auth/callback response PASS Live curl captured: HttpOnly; Secure; SameSite=Lax; Path=/ on the session cookie

    Verdict

    PASS — gate works for the validated user (admin role). The missing-admin path renders the (unauthorized) 403 group, validated separately under validation-17-2026-05-03.

    Discovered Issues

    Two surfaced during this validation pass: (1) cookie size exceeded browser limit — fixed in #24/PR#25; (2) JWT aud claim mismatch — fixed in #26/PR#27. Both were latent defects in code that statically passed QA but failed under live runtime conditions. Both now resolved.

  • Verdict: PASS

    Milestone significance: This is the FINAL gate of the 5-day westside-admin bootstrap arc. With this PASS, milestone M1 (first admin login operationally possible) is officially achieved — the only remaining step is the manual Keycloak admin-console click. The deploy chain is healthy end-to-end for the first time since 2026-04-28.

    Ticket

    forgejo_admin/pal-e-deployments#143 — Migrate westside-admin harbor-creds from SOPS-overlay to terraform-managed (Option A, first migration).

    Shipped via PR #145 (squash-merged 2026-05-03, commit a6a99136597d044e619b26b6850bb7be74b24979) plus a coordinated targeted tofu apply against pal-e-services/terraform.

    Environment

    • Cluster: prod k3s (single-node, archbox)
    • Namespace: westside-admin
    • External URL: https://westside-admin.tail5b443a.ts.net (Tailscale funnel)
    • ArgoCD app: westside-admin (synced to commit a6a9913)
    • Harbor robot: robot$westside-admin+westside-admin-pull (id=384, project-scoped to westside-admin id=38)
    • Validation run: 2026-05-03 by Dottie

    Tier

    Per sop-validation, ticket has labels arch:harbor + touches kustomize + coordinated terraform apply. Executed Tier 3 (prod) end-to-end — the live cluster IS the validation surface for an infra-fix bug. Tier 1 (local terraform plan) was rolled in as the no-drift check.

    Checks

    # Acceptance Criterion How Verified Result Evidence
    1 overlays/westside-admin/prod/kustomization.yaml no longer references harbor-creds.enc.yaml in resources: Forgejo API GET on kustomization.yaml@main PASS resources: now lists only namespace.yaml, ../../../bases/standard, ingress.yaml, westside-admin-secrets.enc.yaml — no harbor-creds reference
    2 overlays/westside-admin/prod/harbor-creds.enc.yaml deleted from repo Forgejo API GET on the file path at ref=main PASS HTTP 404 (file does not exist on main)
    3 Harbor pull robot exists for westside-admin curl -sk -u admin:$HARBOR_ADMIN_PASSWORD https://harbor.tail5b443a.ts.net/api/v2.0/robots/384 PASS Returns name=robot$westside-admin+westside-admin-pull, kind=project, namespace=westside-admin, permissions read artifact + pull repository, disable=false, no expiry
    4 kubectl get secret harbor-creds -n westside-admin dockerconfigjson parses cleanly with valid creds kubectl get secret harbor-creds -o jsonpath='{.data.\.dockerconfigjson}' | base64 -d | python3 -c json.tool PASS Parses as valid JSON. auths."harbor.tail5b443a.ts.net" contains username=robot$westside-admin+westside-admin-pull, password length=32 (real, not placeholder), auth length=100 (base64-encoded username:password)
    5 kubectl get deployment westside-admin -n westside-admin shows 1/1 ready kubectl get deployment westside-admin -n westside-admin -o wide PASS READY=1/1, UP-TO-DATE=1, AVAILABLE=1, image=harbor.tail5b443a.ts.net/westside-admin/app:803e9439...
    6 Pod Running, no CreateContainerError or ImagePullBackOff on current pod kubectl get pods + kubectl get events --sort-by=.lastTimestamp PASS Pod westside-admin-5c96ffff7f-rhz84 Status=Running, Ready=true, Restarts=0. Latest events show Pulled successfully in 1.522s — the pre-fix events showing illegal base64/FailedToRetrieveImagePullSecret are on superseded ReplicaSets, not current pod
    7 /health endpoint responds 200 inside cluster kubectl exec ... -- wget -qO- http://10.42.0.237:3000/health PASS Returns {"status":"ok"}. Note: Alpine wget on localhost hits IPv6 (::1) where node binds IPv4-only — probe via Pod IP works. Kubelet readiness probe uses Pod IP and is passing every 10s (Ready=true confirms)
    8 External funnel https://westside-admin.tail5b443a.ts.net returns app (not 502) curl -skI https://westside-admin.tail5b443a.ts.net/ PASS HTTP 200 in 11.7ms (was 502 since 2026-04-28)
    9 No-drift: targeted tofu plan after apply returns 0 changes for the harbor resources cd ~/pal-e-services/terraform && tofu plan -var-file=k3s.tfvars -target='harbor_robot_account.service_pull["westside-admin"]' -target='kubernetes_secret_v1.harbor_creds["westside-admin"]' -lock=false PASS Plan: 0 to add, 1 to change, 0 to destroy. The 1 change is kubernetes_namespace_v1.service["westside-admin"] (transitive target dep, removing a stray ArgoCD label — pre-existing namespace drift unrelated to harbor migration). The two TARGETED resources (harbor_robot_account + kubernetes_secret_v1.harbor_creds) plan as zero-change — AC condition met
    10 ArgoCD sync state Synced (not OutOfSync, not Drift) kubectl get application westside-admin -n argocd -o jsonpath='{.status.sync.status}{" "}{.status.health.status}{" "}{.status.sync.revision}' PASS sync=Synced health=Healthy revision=a6a99136597d044e619b26b6850bb7be74b24979 (matches PR #145 squash-merge commit)

    Verdict

    PASS — all 10 acceptance criteria green with command-output evidence. Move ticket #143 to done.

    Regression Check

    • Targeted tofu plan shows the harbor migration introduced no drift on adjacent service harbor_robot/secret resources. Other 13 services still on SOPS-overlay placeholder pattern remain unchanged (per the narrow Option A scope of #143 — their migration is parent ticket #144 follow-up work).
    • ArgoCD westside-admin app health=Healthy confirms no other sync issues introduced by the overlay deletion (kustomize still builds, ingress + namespace + secrets still applied).
    • External funnel responsive (HTTP 200, ~12ms) — ingress + Tailscale funnel still wired correctly. SvelteKit adapter-node serving the app.
    • Pod restart count = 0 on the new pod, no crash-loop. Image pull worked first try with the new credentials.

    Discovered Issues

    Two incidental observations — neither blocks the verdict:

    • notion-mcp-remote scope creep in same tofu apply. The targeted apply incidentally created harbor_project.service["notion-mcp-remote"] + kubernetes_namespace_v1.service["notion-mcp-remote"] because the service was in var.services but never previously applied. This is benign completion of state drift, not damage. Already tracked context-only (no follow-up ticket needed unless notion-mcp-remote owner objects).
    • Pre-existing namespace label drift on westside-admin namespace. The targeted plan still shows kubernetes_namespace_v1.service["westside-admin"] wants to remove a stray argocd.argoproj.io/instance label. This is a separate pre-existing condition (ArgoCD added the label out-of-band when it adopted the namespace). Cosmetic, not functional. Suggest tracking under a small "argocd-managed-by-label cleanup" hygiene ticket if desired, but NOT a blocker for #143.

    Implication for Board Item 1117

    Board item 1117 (westside-admin#12 — "Validate step fails: build/index.html doesn't exist for adapter-node") was held in qa pending end-to-end deploy-chain proof. With this PASS, the deploy chain is provably healthy: CI built the image (803e9439...) — that exact tag is now running 1/1 in cluster — the validate-step fix is implicitly verified. Recommend moving #1117 to done alongside #1120 based on this same evidence.

    Bootstrap Arc Recap

    Three layered blockers, fixed in sequence over 5 days, each unmasking the next:

    1. Layer 1 — CI validate step couldn't find build/index.html (adapter-node, not adapter-static). Fixed by westside-admin#13. Surfaced 40-zeros image tag.
    2. Layer 2 — PSA admission rejected pod (no securityContext). Fixed by pal-e-deployments#141.
    3. Layer 3 — harbor-creds Secret was a SOPS-managed PLACEHOLDER fighting terraform writes. Fixed by THIS ticket (#143/#145) — SOPS overlay deleted, terraform owns it.

    The arc demonstrates the value of strict validation gates: each layer was only visible once the previous layer was fixed. Skipping validation would have left this Secret in placeholder state indefinitely.

    Parent pal-e-deployments#144 (platform-wide harbor-creds SOPS→terraform migration) now has a working template — the remaining 13 service migrations can follow the same pattern.

  • Verdict: PASS

    This ticket's scope is fully delivered. The PSA-restricted securityContext patch is rendered correctly in cluster, the new ReplicaSet is admitted with zero PSA violations, and the wrong-container-name failure mode (caught in the third PR review) is not present. M1 (first admin login) is not reachable yet, but the remaining blocker is a separate, pre-existing infra gap (placeholder Harbor pull credentials) — it is not a regression from this change. See Discovered Issues for the new ticket needed.

    Ticket

    forgejo_admin/pal-e-deployments#137 — Add PSA-restricted securityContext to overlays/westside-admin/prod/deployment-patch.yaml so the deployment is admitted under pod-security.kubernetes.io/enforce: restricted.
    Board item: 1118 on board-westside-admin (column qa).
    Merged PR: pal-e-deployments#141 — squash-merged 2026-05-01, merge commit 2e10af964e473d342366ac1a76454613bb04dfc4.

    Environment

    Cluster: archbox (k3s, prod).
    Namespace: westside-admin.
    ArgoCD app: westside-admin (sync revision 2e10af96, status Synced, health Progressing).
    Image: harbor.tail5b443a.ts.net/westside-admin/app:803e9439806cb644cc824fa8fbd87cc5d0f567e2.
    External funnel: https://westside-admin.tail5b443a.ts.net (Tailscale).
    Active ReplicaSet (post-merge): westside-admin-8786bffd6.
    Validation run: 2026-05-01.

    Checks

    # Criterion How Verified Result Evidence
    1 ArgoCD app has synced to a revision newer than the pre-merge head 576eacac kubectl get application westside-admin -n argocd -o jsonpath='{.status.sync.revision}' PASS Returned 2e10af964e473d342366ac1a76454613bb04dfc4, the merge commit of PR #141. operationState.message: successfully synced (all tasks run) at 2026-05-01T14:36:05Z.
    2 Rendered Deployment in cluster carries all 4 PSA-required fields at the pod level (runAsNonRoot, runAsUser, runAsGroup, fsGroup) plus seccompProfile.type=RuntimeDefault kubectl get deployment westside-admin -n westside-admin -o json | jq '.spec.template.spec.securityContext' PASS
    {
      "fsGroup": 1000,
      "runAsGroup": 1000,
      "runAsNonRoot": true,
      "runAsUser": 1000,
      "seccompProfile": { "type": "RuntimeDefault" }
    }
    3 Container-level securityContext sets all 4 PSA-restricted fields (allowPrivilegeEscalation=false, capabilities.drop=[ALL], runAsNonRoot=true, seccompProfile.type=RuntimeDefault) kubectl get deployment ... | jq '.spec.template.spec.containers[0].securityContext' PASS
    {
      "allowPrivilegeEscalation": false,
      "capabilities": { "drop": ["ALL"] },
      "runAsNonRoot": true,
      "seccompProfile": { "type": "RuntimeDefault" }
    }
    4 Container count is exactly 1, named westside-admin (the wrong-name failure mode caught in PR review #3 — patch referenced app while base used westside-admin) kubectl get deployment westside-admin -n westside-admin -o jsonpath='{.spec.template.spec.containers[*].name}' PASS Returned westside-admin (single container). The patch's strategic merge correctly targeted the named container in the base — securityContext is on the right container, not on a phantom app sidecar.
    5 Post-merge ReplicaSet has zero FailedCreate events from PSA admission kubectl describe rs westside-admin-8786bffd6 -n westside-admin PASS Events on the new RS contain only SuccessfulCreate. All historical FailedCreate events from PSA (on stale RS 566d4cc96d, bcb595854, 6fb5db9c6) predate the merge — the new RS is admitted cleanly. PSA is no longer the failure mode.
    6 Deployment reaches 1/1 ready, Available=True, /health returns 200 kubectl get deployment; curl https://westside-admin.tail5b443a.ts.net/health FAIL (out-of-scope) Deployment is 0/1, funnel returns HTTP 502. Pod is stuck in Waiting with reason illegal base64 data at input byte 11 — kubelet can't decode the auth field in the harbor-creds dockerconfigjson because it's the literal placeholder PLACEHOLDER_REPLACE_AFTER_PAL_E_SERVICES_HARBOR_PROJECT_PROVISIONED (byte 11 = _, not a valid base64 char). This blocker is pre-existing and is not caused by PR #141 — it would block the pod regardless of the PSA fix. Tracked as new discovered issue (see below). Not counted against this ticket's verdict — PR #137 was scoped to the PSA admission gate, which is now passed.

    Why Verdict is PASS Despite Pod Not Ready

    The acceptance criteria of issue #137 are: "the deployment manifest carries the four PSA-required securityContext fields and the namespace's restricted enforcement no longer blocks pod creation." Both are met:

    • The four required fields are present at both pod and container level (checks 2 and 3).
    • The new ReplicaSet's pods are admitted by PodSecurity (check 5 — zero FailedCreate from PSA).

    Pod liveness, image pull, and runtime startup are downstream of admission. The pod-startup failure is governed by a separate prerequisite (Harbor robot credentials) that pal-e-services#64 was supposed to wire up but didn't replace the placeholder values in cluster. PR #141 cannot make a pod start that has unusable image-pull creds — and it doesn't claim to. The PSA gate is closed; the next gate (Harbor auth) is open and is now visible because PSA stopped masking it.

    Regression Check

    • ArgoCD sync: app remains Synced after PR; operationState.message: successfully synced (all tasks run). No drift introduced.
    • Other pal-e-deployments apps: PR was scoped to overlays/westside-admin/prod/deployment-patch.yaml only. No other app is affected.
    • kustomize build: ArgoCD's server-side render of the overlay produced a valid Deployment spec with the patched fields landing on the correct container — confirms strategic merge by container name worked.
    • Pre-existing PSA failures elsewhere: spike #139 (westside-streamlit) and audit #140 (system-wide) remain open and are unaffected.

    Discovered Issues

    (NEW — needs Forgejo issue + board item per convention-discovered-scope)

    D-1: harbor-creds placeholder injection — westside-admin namespace's harbor-creds Secret still contains the literal placeholder string from pal-e-deployments bootstrap.

    • Symptom: kubelet rejects pod creation with illegal base64 data at input byte 11 when attempting to construct the docker auth header for image pull.
    • Root cause: The Secret's .dockerconfigjson contains "auth":"PLACEHOLDER_REPLACE_AFTER_PAL_E_SERVICES_HARBOR_PROJECT_PROVISIONED". The auth field of a dockerconfigjson must be base64-encoded username:password. The placeholder fails base64 validation at byte 11 (_ is not in the base64 alphabet).
    • Bootstrap intent: pal-e-services#64 (closed 2026-04-26) onboarded westside-admin into terraform/k3s.tfvars but did not replace the placeholder Secret in the running namespace. The terraform onboarding provisions the Harbor robot account and (per existing services pattern) generates the dockerconfigjson — but the rendered Secret was never applied into the westside-admin namespace.
    • Verification of placeholder location:
      kubectl get secret harbor-creds -n westside-admin -o jsonpath="{.data\.dockerconfigjson}" | base64 -d
      # {"auths":{"harbor.tail5b443a.ts.net":{"auth":"PLACEHOLDER_REPLACE_AFTER_PAL_E_SERVICES_HARBOR_PROJECT_PROVISIONED","username":"robot$westside-admin+westside-admin-pull","password":"PLACEHOLDER"}}}
    • Impact: blocks M1. Until the real auth token is injected, no pod can pull the image, regardless of any other deployment fix. This is now the sole remaining blocker for first admin login.
    • Suggested remediation path: per the existing services pattern (pal-e-app, basketball-api), Harbor robot creds for new namespaces are bootstrapped via the pal-e-services terraform module + a one-shot kubectl apply of the rendered Secret. Likely fix is one of: (a) re-run pal-e-services apply with output verification, (b) add a missing kubectl apply step that pal-e-services#64 was supposed to include, or (c) document a manual kubectl create secret docker-registry harbor-creds step for service onboarding. Lucas/Ava to scope.
    • Recommended labels: type:bug, arch:harbor, blocks:deploy-chain, blocks:M1, story:admin-row-crud.
    • Related: pal-e-services#64 (closed but did not deliver complete bootstrap), spike #139 / #140 (PSA — different domain), board item #1088/#1098 (already in qa, but they validated terraform apply not secret injection).

    Implications for Bootstrap Arc / M1

    Both items #1117 (validate-step fix) and #1118 (PSA fix) had their merged code reach cluster: image 803e9439… was successfully built/pushed by westside-admin pipeline #3, ArgoCD picked up #141's manifest, the new RS was admitted, and the pod was scheduled. All deploy-chain work this validation was scoped to has succeeded. But M1 (first admin login achievable) is still blocked by the harbor-creds placeholder — a third, previously-hidden blocker that PSA was masking. The bootstrap arc is not complete: it has one more step. Recommend creating the discovered issue (D-1) and routing it through the standard backlog → review → todo pipeline before declaring M1 unblocked.

    Recommendation

    • Move board item #1118 (this ticket) to done.
    • Move board item #1117 (validate-step fix in westside-admin#12) to done — its image successfully reached cluster, proving the validate-step fix worked end-to-end.
    • Create a new Forgejo issue + board item in backlog for D-1 (harbor-creds placeholder injection). Do not reopen pal-e-services#64 — fresh ticket with clean repro and fix scope.
    • Hold off on declaring M1 reached until D-1 is shipped, pod becomes 1/1 Running, /health returns 200, and Lucas verifies first admin login at https://westside-admin.tail5b443a.ts.net.
  • Verdict: APPROVED

    Third review of board item #1118 after the second body refinement landed. Lucas approved Option A on 2026-05-01. The previous reviewer (review-1118-2026-05-01) flagged a one-word body fix (container name westside-adminapp in the snippet) plus a recommended new AC. Both have landed cleanly. All caller-specified verification points pass. All previously-passing checks remain clean. Ticket is ready to advance to next_up.

    Caller-Specified Verifications (Second Refinement)

    • [x] 1. Container name in snippet is - name: app — CONFIRMED. Body's "Expected Behavior" snippet at the strategic-merge stage uses - name: app. Matches base container name; will merge correctly into the single existing container.
    • [x] 2. Inline comment explaining post-merge JSON-patch rename — CONFIRMED. Three-line inline comment in the snippet: "IMPORTANT: targets BASE container name; the overlay's JSON patch renames it to 'westside-admin' AFTER strategic merge applies. Using 'westside-admin' here would create a second container instead of patching the existing one." Plus a dedicated "Why container name app not westside-admin" paragraph below the snippet that walks through the kustomization patch order. Both verbose and self-documenting.
    • [x] 3. New AC: kustomize build ... | yq '.spec.template.spec.containers | length' returns 1 — CONFIRMED. Listed as AC #5: "kustomize build overlays/westside-admin/prod/ | yq '.spec.template.spec.containers | length' returns 1 (catches the wrong-container-name failure mode pre-cluster)". Catches the strategic-merge container-append regression before sync.
    • [x] 4. Pod-level superset matches streamlit pattern — CONFIRMED. Snippet has runAsUser: 1000, runAsGroup: 1000, fsGroup: 1000 at the pod-level securityContext. Cross-checked against overlays/westside-streamlit/prod/deployment-patch.yaml: identical superset (runAsNonRoot: true, runAsUser: 1000, runAsGroup: 1000, fsGroup: 1000, seccompProfile.type: RuntimeDefault). Cross-overlay consistency achieved.
    • [x] 5. Pre-merge Dockerfile USER verification updated to "(CONFIRMED)" — CONFIRMED. Body now reads: "Pre-merge Dockerfile verification (CONFIRMED): westside-admin's Dockerfile@main line 31 has USER node per PR #10 — the image actually runs as non-root, so the manifest assertion will hold at runtime." Independently verified: fetched forgejo_admin/westside-admin@main Dockerfile via API, line 31 reads USER node. Hidden runtime prerequisite is satisfied.

    Regression Checks (Previously Passing)

    • [x] 6. Per-overlay scope (no base change) — CONFIRMED. Snippet targets overlays/westside-admin/prod/deployment-patch.yaml exclusively. "Why per-overlay rather than base" paragraph explains the 9-of-14-services Dockerfile-USER prerequisite. Base untouched.
    • [x] 7. Out-of-scope items explicit and pointing to filed spike tickets — CONFIRMED. "Scope Boundary (narrow)" section names both deferrals. Verified via Forgejo API:
      • forgejo_admin/pal-e-deployments#139 — "Spike: westside-streamlit pods fail PSA admission despite existing pod+container securityContext" — OPEN, type Spike, lineage cites review-1118-2026-04-30.
      • forgejo_admin/pal-e-deployments#140 — "Spike: System-wide PSA-restricted readiness audit (Dockerfile USER + securityContext per service)" — OPEN, type Spike, lineage cites review-1118-2026-04-30.
    • [x] 8. AC list does NOT include streamlit-recovery — CONFIRMED. All 6 ACs (the original 5 + the newly-added kustomize-build check) are scoped to westside-admin only. Zero references to streamlit, basketball-api, or any sibling overlay.
    • [x] 9. Misleading "valid in any namespace" sentence still removed — CONFIRMED. Full-text scan of refined body returns zero matches for "valid in any namespace" or analogous PSA-misframings.

    Independent Verifications

    • [x] File path existsoverlays/westside-admin/prod/deployment-patch.yaml verified at exact path on forgejo_admin/pal-e-deployments@main via API. Returns 200, valid YAML, container name app, no securityContext block currently. Confirms the fix is needed and the file is the right target.
    • [x] Kustomize patch orderoverlays/westside-admin/prod/kustomization.yaml verified: patches: list applies deployment-patch.yaml (strategic merge) FIRST, then JSON patches that replace /spec/template/spec/containers/0/name from appwestside-admin. Inline comment line 9 confirms: "targets base name 'app' before rename". Snippet's - name: app will merge correctly.
    • [x] Streamlit reference patternoverlays/westside-streamlit/prod/deployment-patch.yaml uses identical pod-level superset. Snippet's pod-level fields match exactly. Container-level fields match the security-relevant subset (snippet additionally adds runAsNonRoot: true at container-level, which is harmless redundancy with the pod-level setting).
    • [x] Base unchangedbases/standard/deployment.yaml remains free of any securityContext block (verified locally). Option A scope honored.

    Template Completeness

    • [x] Type (Bug)
    • [x] Lineage (full audit trail: validation note → first review → second review → this re-review)
    • [x] Repo (forgejo_admin/pal-e-deployments)
    • [x] What Broke
    • [x] Scope Boundary (narrow) — explicit OUT-OF-SCOPE list with both deferrals
    • [x] Repro Steps (3 steps, all kubectl commands)
    • [x] Expected Behavior (with corrected snippet + three "Why" justification paragraphs + pre-merge Dockerfile note)
    • [x] Environment
    • [x] Acceptance Criteria (6 ACs — manifest fields, deployment ready, FailedCreate=0, CreateContainerConfigError=0, kustomize-build container count, no-regression)
    • [x] Related (cross-references to parent issue, validation note, both reviews)

    Traceability

    • [x] story:admin-row-crud label — story note story-westside-admin-admin-row-crud verified on project-westside-admin user-stories table (carried from prior reviews).
    • [x] arch:k8s-deploy label — arch-k8s-deploy note still missing platform-wide. Per established precedent (review-933, review-935, review-972, review-283, review-1118-2026-04-30, review-1118-2026-05-01) treated as known waiver, not per-ticket blocker. Tracked debt.
    • [x] type:bug — correctly classified.
    • [x] blocks:deploy-chain — correct.
    • [x] Forgejo issue #137 — open, accessible, refined body confirmed.

    File Targets

    • [x] overlays/westside-admin/prod/deployment-patch.yaml — verified exists at exact path, container name app matches snippet target.
    • [x] bases/standard/deployment.yaml — verified untouched scope (Option A).
    • [x] overlays/westside-admin/prod/kustomization.yaml — verified patch ordering compatible with snippet.

    Repo Placement

    Correct — forgejo_admin/pal-e-deployments. Single repo, single overlay file, single targeted change. No cross-repo dependencies for this ticket.

    Dependencies

    No upstream board blockers. Westside-admin Dockerfile (PR #10) is merged on forgejo_admin/westside-admin@main, satisfying the runtime non-root prerequisite. ArgoCD will pick up the change on next sync after merge. Two follow-up tickets (#139 streamlit spike, #140 system-wide PSA audit) are tracked separately and explicitly out of scope.

    Acceptance Criteria

    6 ACs, all verifiable:

    • AC1 — manifest field check via inspection of merged YAML.
    • AC2 — kubectl get deployment 1/1 ready after ArgoCD sync.
    • AC3 — zero FailedCreate from PSA admission via kubectl describe rs.
    • AC4 — zero CreateContainerConfigError events (catches Dockerfile-runs-as-root failure mode).
    • AC5 — kustomize build | yq '.spec.template.spec.containers | length' returns 1 (catches container-name strategic-merge bug pre-cluster). Newly added in this refinement.
    • AC6 — no regression to other services (trivially true, no base change).

    AC list is appropriately scoped and cleanly testable.

    Blast Radius

    Per refinement: zero impact on the 13 sibling overlays. Westside-admin only. Streamlit and base-hardening explicitly carved out into spike #139 and #140 respectively. Confirmed clean.

    Decomposition Assessment

    One file, one overlay, 6 ACs (5 substantive + 1 trivial), single agent pass under the 5-minute rule. No decomposition needed.

    Recommendation

    No action needed. Approved — advance to next_up.

    Optional polish (non-blocking, dev agent can ignore):

    • [BODY] OPTIONAL — In the "Scope Boundary (narrow)" section, append explicit issue numbers (e.g., "… tracked as separate spike tickets in this repo: #139 (streamlit), #140 (system-wide PSA audit)") for one-click navigation. The current prose is unambiguous; this is a navigability nicety only.
    • [SCOPE] arch:k8s-deploy backing note still missing platform-wide. Carries forward as platform-wide tracked debt, not a per-ticket blocker per established precedent.

    Summary

    Two-line fix from the second review (container name app + new kustomize-build AC) landed cleanly, with a robust three-line inline comment and a dedicated justification paragraph for posterity. Pod-level superset now matches streamlit canonical. Pre-merge Dockerfile note upgraded to "(CONFIRMED)" with line-number citation. Both out-of-scope spikes are filed and verified open. All 9 caller-specified verification points pass. Ticket is ready for a dev agent — the snippet can be copy-pasted verbatim and will produce a correct kustomize merge with one container, PSA-restricted, matching streamlit's pattern.

  • Verdict: NEEDS_REFINEMENT

    Re-review of refined issue body (Lucas approved Option A on 2026-05-01). The refinement resolves all three original blockers cleanly and the scope is now appropriately narrow. However, the verification matrix the caller asked me to run also surfaced a new blocker in the canonical YAML snippet under "Expected Behavior" — the container name in the example does not match the base. A dev agent that copy-pastes the snippet verbatim will produce a broken kustomize merge. One-line fix; re-review will be near-instant.

    Original Blocker Resolution (re-verification)

    Each of the three blockers from review-1118-2026-04-30, plus the six caller-specified verification items:

    • [x] Original Blocker 1 (root crash on 9 sibling services) — RESOLVED. New "Expected Behavior" targets only overlays/westside-admin/prod/deployment-patch.yaml. Base untouched. Zero impact on the 9 root-running consumers. (Caller verification 1 PASS.)
    • [x] Original Blocker 2 (streamlit's existing securityContext insufficient) — RESOLVED via OUT-OF-SCOPE deferral. AC list no longer contains any westside-streamlit recovery condition. Streamlit spike explicitly called out as a separate ticket in the new "Scope Boundary (narrow)" section. (Caller verifications 2 + 5 PASS.)
    • [x] Original Blocker 3 (basketball-api initContainer) — RESOLVED via OUT-OF-SCOPE deferral. The system-wide PSA hardening initiative explicitly carries this forward as separate scope. (Caller verification 5 PASS.)
    • [x] Caller verification 3 — AC list includes CreateContainerConfigError zero-events check. Verified at AC #4: "Zero CreateContainerConfigError events on westside-admin pods (proves the Dockerfile actually runs as non-root, not just the manifest)." PASS.
    • [x] Caller verification 4 — Pre-merge Dockerfile USER verification note IS present, located in the "Expected Behavior" section: "Pre-merge verification of westside-admin's Dockerfile: confirm the image actually runs as non-root before merging the manifest…" PASS.
    • [x] Caller verification 6 — Misleading "fields are valid in any namespace" sentence has been removed from the body. Confirmed by full-text scan of refined body. PASS.

    All six caller-specified verifications pass. The refinement work is solid.

    Fresh Verifications (caller-specified)

    • [x] File path existsoverlays/westside-admin/prod/deployment-patch.yaml verified at that exact path on forgejo_admin/pal-e-deployments@main via curl /api/v1/repos/.../raw/.... Returns 200, valid YAML, 56 lines. Container name in current file: app.
    • [x] Dockerfile USER directive — verified forgejo_admin/westside-admin@main Dockerfile contains USER node (line 31). PR #10 is merged (state=closed, merged=true, title "docker: multi-stage Dockerfile for adapter-node runtime"). Hidden prerequisite is satisfied — no flag needed.
    • [ ] Kustomize merge correctnessFAILS. See "New Blocker" below. The base has a containers array with one container named app. Strategic merge keys on container name. The issue body's snippet uses - name: westside-admin, which will NOT merge with - name: app in the base — it will append a SECOND container, leaving the original one without securityContext.

    New Blocker: Container Name Mismatch in Snippet

    The snippet under "Expected Behavior" specifies:

    containers:
      - name: westside-admin            # <-- WRONG for strategic-merge stage
        securityContext:
          ...

    Why this breaks:

    1. Kustomize processes patches: in order. The overlays/westside-admin/prod/kustomization.yaml applies deployment-patch.yaml (strategic merge) FIRST, then JSON patches that rename containers/0/name from appwestside-admin.
    2. The existing deployment-patch.yaml correctly uses - name: app at the strategic-merge stage. The kustomization comment even calls this out: "targets base name 'app' before rename."
    3. If the dev agent copy-pastes the issue snippet verbatim, the patch will contain - name: westside-admin. Strategic merge keys on container name. With no container named westside-admin in the base, kustomize appends a second container. The pod manifest will then have:
      • Container 0: name app (later renamed to westside-admin by JSON patch), no securityContext, with all the env/probes/resources from the existing patch.
      • Container 1: name westside-admin, with securityContext but no image, no env, no probes — invalid pod spec, will fail admission AND/OR fail to schedule.
    4. Even if kustomize doesn't error, the resulting manifest is broken and PSA admission will reject the second container for being incomplete (no image), or the pod will hit InvalidImageName.

    Reference pattern (correct): overlays/westside-streamlit/prod/deployment-patch.yaml — which DOES work in PSA admission terms (its failure is a separate kustomize-rename issue, deferred to the spike) — uses - name: app at the strategic-merge stage. Snippet from streamlit:

    spec:
      template:
        spec:
          securityContext:
            runAsNonRoot: true
            runAsUser: 1000
            runAsGroup: 1000
            fsGroup: 1000
            seccompProfile:
              type: RuntimeDefault
          containers:
            - name: app                  # <-- correct: matches base before JSON-patch rename
              securityContext:
                allowPrivilegeEscalation: false
                readOnlyRootFilesystem: false
                capabilities:
                  drop:
                    - ALL

    Secondary Finding: Snippet vs. Streamlit Canonical

    The issue snippet's pod-level securityContext omits runAsUser: 1000, runAsGroup: 1000, and fsGroup: 1000. Streamlit (the only other working PSA-restricted-targeting overlay in this repo) sets all three. Two views:

    • Strict PSA-restricted requirement: only runAsNonRoot: true, seccompProfile.type: RuntimeDefault, allowPrivilegeEscalation: false, capabilities.drop: ["ALL"] are required. The snippet is sufficient for admission.
    • Project canonical pattern: matches streamlit's superset (adds the explicit UID/GID + fsGroup). Lower drift risk if other PSA-restricted overlays follow.

    Not a blocker — the snippet IS valid PSA-restricted shape — but flagging because westside-admin's Dockerfile sets USER node (UID 1000 on node:alpine) and an explicit runAsUser: 1000 would document that contract in the manifest. Recommend matching streamlit's canonical superset for consistency, or explicitly annotate why this overlay diverges. Not strictly required for AC pass.

    Template Completeness

    • [x] Type (Bug)
    • [x] Lineage (validation-12-2026-04-30, with refinement audit trail)
    • [x] Repo (forgejo_admin/pal-e-deployments)
    • [x] What Broke
    • [x] Scope Boundary (narrow) — new section, explicit OUT OF SCOPE list. Excellent addition.
    • [x] Repro Steps
    • [x] Expected Behavior (with snippet — see container-name issue)
    • [x] Environment
    • [x] Acceptance Criteria (5 ACs, all testable)
    • [x] Related

    Traceability

    • [x] story:admin-row-crud label — story note story-westside-admin-admin-row-crud verified on project-westside-admin user-stories table (carried from prior review).
    • [x] arch:k8s-deploy label — arch note still MISSING: search_notes(query="arch-k8s-deploy") returns zero results. Per platform-wide precedent (review-933, review-935, review-972, review-283, review-1118-2026-04-30) treated as known waiver, not per-ticket blocker. Tracked debt.
    • [x] type:bug — correctly classified.
    • [x] blocks:deploy-chain — correct.
    • [x] Forgejo issue #137 — open, accessible, refined body confirmed.

    File Targets

    • [x] overlays/westside-admin/prod/deployment-patch.yaml — verified exists at exact path. Currently has zero securityContext. Existing container name: app (matches base). Existing strategic merge correctly targets pre-rename container name.
    • [x] bases/standard/deployment.yaml — verified, container named app, no securityContext. Untouched by this fix per Option A.
    • [x] overlays/westside-admin/prod/kustomization.yaml — verified patch order: strategic-merge FIRST, JSON-patch container rename SECOND. Comment at line 9 confirms: "targets base name 'app' before rename."

    Repo Placement

    Correct — forgejo_admin/pal-e-deployments. Single repo, single file, single overlay. Refinement collapsed multi-service ambition into a clean single-overlay change.

    Dependencies

    Board context: #1117 (validation surface) is in qa. Two follow-up tickets explicitly named in the body (streamlit PSA spike + system-wide PSA hardening) — both intentionally out of scope for #1118 and to be filed separately. No upstream board blockers. Westside-admin Dockerfile (PR #10) is merged, satisfying the runtime-USER prerequisite.

    Acceptance Criteria

    5 ACs, all verifiable:

    • AC1 — manifest field check (visual inspection of merged YAML or kubectl get deploy -o yaml | yq).
    • AC2 — kubectl get deployment 1/1 ready after ArgoCD sync. Standard rollout check.
    • AC3 — zero FailedCreate from PSA admission via kubectl describe rs. Standard PSA failure-mode check.
    • AC4 — zero CreateContainerConfigError events. Catches the Dockerfile-runs-as-root failure mode. Smart, narrowly scoped check.
    • AC5 — no regression elsewhere. Trivially true (no base change). Could even be omitted.

    AC list is appropriately scoped. One implicit AC missing: verify the kustomize build before merge — kustomize build overlays/westside-admin/prod/ should produce exactly ONE container in the output. This catches the container-name mismatch blocker before it reaches the cluster. Recommend adding as AC #6.

    Blast Radius

    Per refinement: zero impact on the 13 sibling overlays. Westside-admin only. Streamlit and base-hardening explicitly carved out. Confirmed clean.

    Decomposition Assessment

    One file, one overlay, 5 ACs (4 substantive + 1 trivial), single agent pass under the 5-minute rule. No decomposition needed.

    Recommendation

    Refinement is 95% there. Two body fixes get this to APPROVED:

    • [BODY] BLOCKER — In the "Expected Behavior" snippet, change - name: westside-admin to - name: app. The strategic-merge patch must target the base container name (app); the kustomization's JSON patches will rename it to westside-admin AFTER the merge. Add a one-line comment in the snippet: # container name 'app' matches base (renamed to 'westside-admin' by kustomization JSON patch).
    • [BODY] RECOMMENDED — Add AC #6: kustomize build overlays/westside-admin/prod/ | yq '.spec.template.spec.containers | length' returns 1. Verifies the strategic merge produced one container, not two — catches the container-name mistake before cluster sync.
    • [BODY] OPTIONAL — Consider matching streamlit's canonical pod-level superset (runAsUser: 1000, runAsGroup: 1000, fsGroup: 1000) for cross-overlay consistency. Not required for PSA admission. If diverging intentionally, add a one-line "why" comment.
    • [SCOPE] arch:k8s-deploy backing note still missing platform-wide. Not blocking per precedent.

    Summary

    Refinement work resolved all three original blockers cleanly: per-overlay scope, streamlit deferred, AC-list narrowed, misleading sentence removed, Dockerfile pre-merge note added. All six caller-specified verifications pass. New blocker introduced by the canonical YAML snippet's container-name mismatch — fix is one word change (westside-adminapp) plus a clarifying comment. Re-review on the corrected body will be a fast pass.

  • Verdict: NEEDS_REFINEMENT

    The bug is real and the validation note correctly identifies the PSA admission failure. However, the proposed fix as written has three concrete blast-radius problems that will break sister services or fail to actually resolve the streamlit case. Issue body needs revision before dispatching a dev agent.

    Template Completeness

    • [x] Type (Bug)
    • [x] Lineage (validation-12-2026-04-30)
    • [x] Repo (forgejo_admin/pal-e-deployments)
    • [x] What Broke
    • [x] Repro Steps
    • [x] Expected Behavior
    • [x] Environment
    • [x] Acceptance Criteria
    • [x] Related

    Traceability

    • [x] story:admin-row-crud label — story note story-westside-admin-admin-row-crud verified on project-westside-admin user-stories table
    • [x] arch:k8s-deploy label — arch note MISSING: search_notes(query="arch-k8s-deploy") returns zero results. Per platform-wide precedent (review-933-2026-04-10, review-935-2026-04-11, review-972-2026-04-11, review-283-2026-03-27) this is treated as a known waiver, not a per-ticket blocker. Tracked debt for a future arch-k8s-deploy note.
    • [x] type:bug — correctly classified
    • [x] blocks:deploy-chain — correct
    • [x] Forgejo issue #137 — open, accessible

    File Targets

    • [x] bases/standard/deployment.yaml — verified exists at the path. Currently has zero securityContext blocks. Container is named app, port 8000, image app-image.
    • [x] overlays/westside-admin/prod/deployment-patch.yaml — verified, no securityContext.
    • [ ] overlays/westside-streamlit/prod/deployment-patch.yamlISSUE: contains the issue body's claim is wrong. Streamlit overlay already defines BOTH pod-level securityContext (runAsNonRoot: true, runAsUser: 1000, runAsGroup: 1000, fsGroup: 1000, seccompProfile.type: RuntimeDefault) AND container-level (allowPrivilegeEscalation: false, capabilities.drop: [ALL]). Yet validation note shows streamlit pods are also blocked. The issue body must explain why streamlit's existing securityContext is insufficient (likely root cause: kustomize strategic merge replaces vs. merges arrays at containers, OR a kustomize patch ordering issue, OR streamlit-streamlit's PSA never actually rejected streamlit and validation note conflated the two namespaces). This must be diagnosed before the fix.
    • [x] Base path is correct: 14 overlays consume ../../../bases/standard (basketball-api, gcal-scheduler, mcd-tracker-app, mcd-tracker, pal-e-app, pal-e-docs, pal-e-mail, pal-e-production, platform-validation, twitch-2k-wager, westside-admin, westside-ai-assistant, westside-streamlit, westsidekingsandqueens). pal-e-docs-api and the three playgrounds/email do not.

    Repo Placement

    Correct — the fix is in pal-e-deployments. No multi-repo concern.

    Dependencies

    Board context: #1117 (westside-admin#12 validation bug) is in qa — already merged. #1118 was surfaced by that validation. No upstream board blockers.

    The fix DOES affect 14 sibling services because they all consume the same base. This is the central blast-radius concern below.

    Acceptance Criteria

    AC1, AC4, AC5 are testable. AC2/AC3 (deployment 0/1 -> 1/1) depend on AC4 being true (no regression elsewhere). AC4 specifically calls out basketball-api, westsidekingsandqueens, pal-e-app, pal-e-docs — this is the right list to test, but see Blast Radius for what the fix needs to do to satisfy AC4.

    Blast Radius — THREE BLOCKERS

    Blocker 1: runAsNonRoot: true at base will crash 9 of 14 sibling services at pod start. Audit of Dockerfiles in each consuming service:

    • Has USER directive (safe under runAsNonRoot: true): westside-admin (USER node, UID 1000), westside-streamlit (USER streamlit, UID 1000), twitch-2k-wager (USER appuser).
    • NO USER directive (runs as root, will fail with CreateContainerConfigError when runAsNonRoot: true is enforced): basketball-api, gcal-scheduler, mcd-tracker, mcd-tracker-app, pal-e-app, pal-e-docs, pal-e-mail, westside-ai-assistant, westsidekingsandqueens.
    • Not audited (no Dockerfile pulled in this review): pal-e-production, platform-validation (platform-validation Dockerfile shows no USER — runs as root).

    Issue body claims "the fields are valid in any namespace; they only become required under restricted." This is true for the other three fields but FALSE for runAsNonRoot: true: that field actively rejects root containers at runtime regardless of PSA labels. AC4 will fail for at least 9 services.

    Blocker 2: basketball-api init container. basketball-api/prod/deployment-patch.yaml defines an initContainer (copy-gmail-oauth using busybox). PSA-restricted requires every container in the pod to satisfy securityContext, including initContainers. A base-level container[0] securityContext does not propagate to initContainers, and basketball-api lives in a PSA-relaxed namespace today — so this is not a regression for basketball-api specifically, but if any future base consumer adopts PSA-restricted (M1 path is exactly this), the init container will fail. Must be addressed in scope or explicitly deferred.

    Blocker 3: Streamlit's existing patch already defines securityContext but pods still fail. Without diagnosing why, adding the same fields at the base will not fix streamlit. Two plausible root causes the dev agent must investigate:

    • Kustomize strategic-merge container override: Streamlit's deployment-patch.yaml uses containers: [{name: app, securityContext: {...}, ...}] with no JSON patch op — strategic merge by name should merge with the base, but if the base now also defines a container[0] securityContext under a different field shape, the merge result may drop fields silently.
    • Patch ordering / target mismatch: The streamlit overlay's JSON patches rename container[0]/name from app to streamlit's container name AFTER the strategic merge — verify the rename does not drop securityContext.

    Decomposition Assessment

    One file target, one repo. Under the 5-minute / 3-file / 5-AC threshold. No decomposition needed if the scope is corrected. If the agent must also patch each consuming overlay's image to add a USER directive or per-overlay securityContext, scope balloons to 9+ repos — at that point, decomposition required.

    Recommendation

    • [BODY] Diagnose why streamlit pods fail despite an existing securityContext. Add the diagnosis to the "What Broke" section. Without this, the fix will not unblock streamlit and AC3 cannot pass.
    • [BODY] Replace the proposed "add the four fields to the base" approach with a scoped alternative. Two options:
      • Option A — Per-overlay patch (LOW RISK): Add the 4 securityContext fields ONLY to overlays/westside-admin/prod/deployment-patch.yaml. Streamlit's existing patch is debugged separately. Zero impact on other services. Smallest diff, smallest blast radius. Recommended.
      • Option B — Base + per-service USER audit (HIGH RISK): Add fields to base, but FIRST audit + fix every consuming Dockerfile to add a USER directive (9 separate repos, 9 separate PRs). Multi-week effort. Defeat the "single PR fix" framing.
    • [BODY] If Option B is chosen, decompose into a parent ticket + 9 child Dockerfile PRs via skill-decompose-ticket. Block the base change until all child PRs land.
    • [BODY] Pod-level vs. container-level: PSA-restricted treats runAsNonRoot and seccompProfile.type as satisfied if set at EITHER level (per upstream k8s PSA docs — admission checks pod-level fallback). allowPrivilegeEscalation and capabilities.drop are container-level only. The streamlit overlay's split (pod-level for runAsNonRoot+seccomp, container-level for the rest) is the canonical pattern and should be the recommended shape, not the issue's all-container-level proposal.
    • [BODY] Remove the line "PSA-relaxed namespaces are unaffected (the fields are valid in any namespace; they only become required under restricted)" — runAsNonRoot: true rejects root containers regardless of PSA labels. This sentence misleads the dev agent into thinking AC4 is automatically satisfied.
    • [BODY] Add an explicit AC: "kubectl get pods -A after merge shows zero CreateContainerConfigError events across all 14 base consumers."
    • [SCOPE] arch:k8s-deploy backing note still missing platform-wide. Not blocking per precedent.

    Summary

    The bug is real, the validation note is sound, the proposed file target is correct. The fix shape proposed in the issue body has three concrete blast-radius problems: (1) breaks 9 services that run as root, (2) doesn't address basketball-api's initContainer, (3) doesn't actually unblock streamlit because streamlit already has the securityContext yet still fails. Reviewer recommends the per-overlay patch alternative (Option A) as the smallest fix that satisfies AC2; AC3 needs a separate streamlit-specific diagnosis ticket. Refine issue body, then re-review.

  • Verdict: PARTIAL

    Ticket

    forgejo_admin/westside-admin#12PR #13 (squash-merged 2026-04-30 12:26:39 UTC, merge SHA 803e9439). Replaced test -f build/index.html with test -f build/index.js + test -f build/handler.js in the .woodpecker.yaml validate step so it asserts on artifacts @sveltejs/adapter-node actually produces.

    Environment

    • Cluster: archbox k3s, namespace westside-admin
    • Funnel: https://westside-admin.tail5b443a.ts.net
    • Image: harbor.tail5b443a.ts.net/westside-admin/app:803e9439806cb644cc824fa8fbd87cc5d0f567e2
    • ArgoCD app: westside-admin (auto-sync) — reconciled at 2026-04-30T12:30:05Z
    • Pipeline: forgejo_admin/westside-admin#3 (event=push, branch=main, all 4 steps success)

    Checks

    # Criterion How Verified Result Evidence
    1 AC1: .woodpecker.yaml validate asserts on adapter-node artifacts (build/index.js, build/handler.js) Pipeline #3 + #2 logs grep validate step output PASS Pipeline #3 validate logs end with + test -f build/index.js then + test -f build/handler.js exit 0. Pipeline #2 (PR) shows the same.
    2 AC2: a fresh pipeline against main reaches build-and-push Woodpecker MCP get_pipeline_status for #3 PASS Pipeline #3 (event=push) all 4 steps success: clone, validate, build-and-push, update-kustomize-tag.
    3 AC3: no regression in pull_request runs (validate still gates Kaniko) Pipeline #2 (event=pull_request) status PASS Pipeline #2 ran 2 steps (clone, validate) then stopped — Kaniko correctly skipped on PR per when: branch: main. Validate gate intact.
    4 Image exists in Harbor under merge commit SHA Harbor API /projects/westside-admin/repositories/app/artifacts PASS Tag 803e9439806cb644cc824fa8fbd87cc5d0f567e2, digest sha256:d350d5c5c05fea, pushed 2026-04-30T12:27:11Z (28 sec after merge).
    5 pal-e-deployments has auto-commit bumping overlays/westside-admin/prod newTag Forgejo commits API for overlays/westside-admin path PASS Commit 576eacacde: [skip ci] westside-admin: update image tag to 803e9439806cb644cc824fa8fbd87cc5d0f567e2. Replaced the 40-zeros placeholder. Visible in kustomization.yaml newTag.
    6 ArgoCD app westside-admin Synced + Healthy kubectl get application -n argocd westside-admin -o jsonpath PARTIAL Sync status: Synced at revision 576eacac. Health status: Progressing (waiting on rollout). Operation phase: Succeeded. ArgoCD did its job; pod creation is what's failing downstream.
    7 Deployment westside-admin 1/1 ready kubectl get deploy -n westside-admin FAIL 0/1 ready, 2 unavailable replicas. Deployment image is correct (...:803e9439...) but pods cannot be created.
    8 Pod boots cleanly; logs show no startup errors kubectl get pods -n westside-admin and events FAIL Zero pods exist. Three replicasets (566d4cc96d, 6fb5db9c6, bcb595854) all show 0/1 created. Events: FailedCreatepods "..." is forbidden: violates PodSecurity "restricted:latest" (allowPrivilegeEscalation, capabilities.drop, runAsNonRoot, seccompProfile not set).
    9 /health endpoint returns 200 curl -s -o /dev/null -w "%{http_code}" https://westside-admin.tail5b443a.ts.net/health FAIL Returns 000 (connection drops). Root URL returns 502 from the Tailscale funnel because no backend pod exists to serve traffic.

    Regression Check

    The PR itself introduces no regression. Pipeline #2 (PR event) ran clean — validate now passes against adapter-node artifacts and Kaniko correctly skips on PR per the existing when: branch: main gate. Pipeline #3 (push event) reached every step. The downstream pod failure is not caused by PR #13: it is a pre-existing namespace configuration gap that was previously masked by the placeholder image tag (0000...0000) keeping the deployment in ImagePullBackOff rather than reaching the PSA admission controller.

    Discovered Issues

    Pod creation blocked by PodSecurity admission "restricted:latest". The westside-admin namespace is labeled pod-security.kubernetes.io/enforce: restricted + enforce-version: latest in overlays/westside-admin/prod/namespace.yaml, but neither the base manifest at bases/standard/deployment.yaml nor the overlay's deployment-patch.yaml sets the four fields PSA-restricted requires:

    • spec.template.spec.containers[].securityContext.allowPrivilegeEscalation: false
    • spec.template.spec.containers[].securityContext.capabilities.drop: ["ALL"]
    • spec.template.spec.containers[].securityContext.runAsNonRoot: true
    • spec.template.spec.containers[].securityContext.seccompProfile.type: RuntimeDefault

    Sister namespace westside-streamlit has the identical PSA enforcement and is also at 0 pods (its deployment was also never running). Other prod namespaces (basketball-api, westsidekingsandqueens, pal-e-app, pal-e-docs) have no PSA label and so are unaffected. Two coupled gaps: (a) the PSA label in the overlay was added without coordinating securityContext on the workload; (b) the base manifest bases/standard/deployment.yaml doesn't set securityContext, so any overlay that adopts PSA-restricted will hit this. Recommend a follow-up Forgejo issue against pal-e-deployments to either add securityContext to bases/standard/deployment.yaml (preferred — benefits every consumer) or patch it in the overlays/westside-admin/prod overlay specifically. The same fix unblocks westside-streamlit.

    Summary

    The merge of PR #13 successfully fixed the bug it set out to fix — all three acceptance criteria from issue #12 are met, and the deploy chain (build → push → tag bump → ArgoCD sync) now works end-to-end for the first time in westside-admin's history. The image is in Harbor under the correct SHA; pal-e-deployments has the auto-commit; ArgoCD has applied the new manifest. However, the broader bootstrap goal — westside-admin actually serving traffic — is blocked by a separate PSA-restricted namespace policy that no workload manifest in the deploy chain satisfies. This is an infrastructure gap, not a regression. Verdict PARTIAL: ticket-level ACs all PASS; production readiness PARTIAL pending the PSA fix.

  • Scope

    Ticket pal-e-deployments#135 — surface the admin-app-db-url Secret produced in the basketball-api namespace (PR pal-e-platform#304) into the westside-admin namespace so the westside-admin pod (PR pal-e-deployments#134) can mount DATABASE_URL via secretKeyRef.

    Producer: pal-e-platform terraform module/database creates kubernetes_secret_v1.admin_app_db_url in basketball-api.
    Consumer: westside-admin Deployment in westside-admin namespace expects a Secret named westside-admin-secrets with key DATABASE_URL.

    Verdict

    REQUEST_CHANGES — pre-decision required before dev dispatch.

    The dev agent should not be asked to "decide during scoping" between four mechanisms (mittwald/kubernetes-replicator, External Secrets Operator, kustomize secretGenerator with duplicated SOPS payload, terraform-emitted second Secret). Each option has materially different blast radius and operational burden, and at least one (replicator / ESO) introduces a brand-new platform dependency that belongs to pal-e-platform, not to a deployments-overlay PR.

    Verification

    • Read PR pal-e-deployments#134 body + diff — consumer expects Secret westside-admin-secrets in the westside-admin ns with DATABASE_URL. Currently the SOPS-encrypted westside-admin-secrets.enc.yaml in the overlay carries a placeholder DATABASE_URL.
    • Read PR pal-e-platform#304 body + diff — producer creates kubernetes_secret_v1.admin_app_db_url in basketball-api ns. The PR body explicitly defers cross-namespace surfacing to "Track B (#133/#135)" and even names the two leading candidates ("kustomize secretGenerator referencing the same value or a small replicator").
    • Issue pal-e-deployments#135 body could not be retrieved directly (no get_issue MCP tool, Tailscale-internal Forgejo not reachable from WebFetch). Review proceeds from the producer/consumer PR bodies, both of which are authoritative on the gap and the candidate set.
    • Repo precedent check: bases/standard/kustomization.yaml has no replicator hook. No kubernetes-replicator / external-secrets Helm release exists in the visible pal-e-platform/terraform/main.tf module wiring (storage, keycloak, database, ops, observability, etc.). No precedent for cross-namespace secret replication exists in the platform. This ticket is therefore introducing a new pattern, which raises the bar for pre-decision.
    • Story story:admin-row-crud and arch arch:k8s-deploy are the correct labels (consistent with #134 / #304).

    Findings

    1. Four-option deferral is the wrong shape for a brand-new pattern

    Per feedback_review_before_dispatch and feedback_tickets_not_solution_specs, tickets carry user story + context + AC; dev agents pick implementation. That holds when there is precedent. Here there is none — picking between mittwald-replicator (cluster-wide controller, RBAC, ArgoCD app), ESO (CRDs, secret store, controller), kustomize secretGenerator + duplicated SOPS file, and a terraform-side second kubernetes_secret_v1 in the consumer ns is a platform architecture decision, not an implementation detail. The blast radii differ by orders of magnitude.

    2. Three of the four options shouldn't even be on the table for this ticket

    • mittwald/kubernetes-replicator — adds a new cluster-wide controller. Belongs to pal-e-platform as its own ticket + tofu module, not to a deployments-overlay PR. Out of scope.
    • External Secrets Operator — same: new CRDs, controller, SecretStore. Out of scope for a single overlay.
    • kustomize secretGenerator with duplicated SOPS ciphertext — violates the spirit of feedback_never_alter_prod_directly's "single source of truth" thinking. Two encrypted blobs to rotate, two failure modes. Hard to keep in sync with the terraform-emitted producer.
    • Terraform emits a second kubernetes_secret_v1 in the westside-admin namespace — adds ~10 lines to module/database, no new dependency, no new controller, idempotent, single source of truth (the Salt pillar value). Already inside the producer module that owns the password lifecycle.

    Recommend pre-deciding option 4 (terraform mirrors the Secret into westside-admin ns), and downscoping the ticket to either (a) a westside-admin-overlay-only fix that simply switches the env var secretKeyRef.name from westside-admin-secretsadmin-app-db-url and removes the placeholder DATABASE_URL from the SOPS file, or (b) a tiny pal-e-platform PR that adds the mirror Secret. The honest answer is the work largely belongs to pal-e-platform, not pal-e-deployments — that should be settled before dispatch.

    3. File Targets ambiguity

    The ticket reportedly lists services/westside-admin/overlays/prod/ + possibly services/_shared/. The actual repo layout is overlays/westside-admin/prod/ (no services/ prefix, no _shared/ dir). Per feedback_verify_repo_layout_before_ticketing, File Targets must match the live repo or the dev agent burns cycles. Fix before promoting to todo.

    4. AC must include rotation propagation E2E

    Producer Job already rotates via Salt-pillar bump → terraform re-apply. The consumer must observe the new password without manual pod-roll, OR the AC must explicitly state "rotation requires kubectl rollout restart of westside-admin Deployment." Either is fine; the unstated case is a foot-gun.

    5. Constraints that must be in the ticket

    • No manual SOPS duplication of the encrypted DATABASE_URL.
    • No coupling of pal-e-platform module/database to westside-admin app internals (mirror Secret is fine; importing westside-admin config is not).
    • No new cluster-wide controllers added in this ticket — if a replicator is the right answer long-term, that is its own pal-e-platform ticket gated separately.

    Recommendation

    1. Pre-decide the mechanism with Lucas/Ava before dispatch. Recommended: terraform mirrors the Secret into westside-admin ns from the same module/database resource. Lowest blast radius, no new dependency, single source of truth.
    2. Move the actual implementation work onto pal-e-platform as a small follow-up PR to #304 (or amend #304 if not yet merged), since terraform owns the password and the producer Secret. Then pal-e-deployments#135 becomes a tiny overlay-only change: rewire deployment-patch.yaml envs to source DATABASE_URL from the mirrored Secret name, drop DATABASE_URL from the SOPS-encrypted westside-admin-secrets.enc.yaml.
    3. Fix File Targets to match real repo paths (overlays/westside-admin/prod/deployment-patch.yaml, overlays/westside-admin/prod/westside-admin-secrets.enc.yaml).
    4. Add explicit rotation-propagation step to AC.
    5. Add the three constraints in finding #5.
    6. Once the above are in the ticket body, ticket can move backlogtodo per feedback_todo_means_reviewed.
  • Verdict: READY (with one [SCOPE] carryover)

    Re-review of board item #1097 — Forgejo forgejo_admin/pal-e-platform#302. Issue body fully rewritten since review-1097-2026-04-25: 3-option fork removed, Path A (idempotent k8s Job in pal-e-deployments) selected. Type: Task.

    Changes since v1

    • Title rewritten: "k8s Job: provision admin_app Postgres user + grants on basketball db" (was generic "create admin_app user")
    • 3-option fork removed; Path A explicitly chosen with rationale block citing v1 review findings
    • SQL spelled out in body (CREATE ROLE DO-block, GRANT, ALTER DEFAULT PRIVILEGES)
    • Three concrete deliverables enumerated (password gen, Job manifest + kustomization, DATABASE_URL secret mirror)
    • 10 AC including idempotency + blocked-write test + forward-grant test
    • Constraints section explicitly forbids cyrilgdn provider, forbids modifying basketball-api Deployment, forbids CREATE/ALTER/DROP grants
    • Label drift: arch:postgres-grantsarch:postgres

    Template Completeness

    • [x] Type — Task
    • [x] Lineage (with scope-revision lineage block)
    • [x] Repo
    • [x] Scope (3 deliverables, SQL inline)
    • [x] Acceptance Criteria — 10 items
    • [x] Test Expectations
    • [x] Constraints
    • [x] Checklist
    • [x] Related (links v1 review for provenance)

    Traceability

    • [x] story:admin-row-crud label — present
    • [x] story note verified — story-westside-admin-admin-row-crud exists in project-westside-admin user-stories table
    • [x] arch:postgres label — present (changed from arch:postgres-grants)
    • [ ] arch note MISSING — neither arch-postgres nor arch-deployment-westside-admin exist as backing notes. [SCOPE] Carryover from v1: create arch-deployment-westside-admin and a stub arch-postgres (or fold grants into the parent). Should not block ticket execution.
    • [x] Forgejo issue — open, valid (#302)
    • [x] type:infra label — present

    SQL Idempotency Audit

    • CREATE ROLE — wrapped in DO $$ ... IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname='admin_app') ... CREATE ROLE ... END $$. Idempotent for creation. Caveat: if the role already exists, the Job will NOT update the password — there's no ALTER ROLE branch. For initial provisioning + re-apply with same password, this is correct (no-op). For password rotation, the Job won't propagate a new password unless ALTER ROLE is added. Acceptable for v1 since rotation is deferred to the companion SOP.
    • GRANT USAGE / GRANT SELECT,INSERT,UPDATE,DELETE / GRANT USAGE ON SEQUENCES — Postgres GRANT is idempotent by design (re-grant is a no-op). Correct.
    • ALTER DEFAULT PRIVILEGES — idempotent. Correct pattern for "future tables get same grants automatically." This is the right tool for the AC "all future tables" requirement.
    • Conclusion: idempotency holds for the scope as written. Re-running the Job is safe.

    Grants Minimality

    • USAGE on schema public — required for any table access. ✓
    • SELECT, INSERT, UPDATE, DELETE on tables — exactly the row-CRUD scope. ✓
    • USAGE on sequences — needed for nextval (SERIAL/identity inserts). ✓
    • NO CREATE, ALTER, DROP, TRUNCATE, REFERENCES, TRIGGER. ✓
    • NO superuser, NO CREATEDB, NO CREATEROLE. ✓ (default for non-superuser CREATE ROLE)
    • Minor: if the app ever needs currval() or lastval(), sequences also need SELECT. USAGE alone covers nextval() which is what Drizzle uses for inserts. Not a blocker — flag for later if reads of sequence state are added.
    • Blocked-write AC (CREATE TABLE → permission denied) is the correct fail-test signal that minimality holds.

    DATABASE_URL Handoff to #1095/#133

    • Path: pal-e-deployments/overlays/westside-admin/prod/westside-admin-secrets.enc.yaml — explicit.
    • Key: DATABASE_URL — explicit.
    • Connection string: postgresql://admin_app:<password>@postgres.basketball-api.svc.cluster.local:5432/basketball — verified against postgres.yaml: Service name is postgres in namespace basketball-api, port 5432, DB name basketball. Correct.
    • Cross-namespace DNS works in k3s — overlay-side NetworkPolicy must allow westside-admin → basketball-api:5432 (separate concern, likely covered by #133 or a follow-up).
    • Companion ticket #1095 (pal-e-deployments#133) is the consumer; contract is unambiguous.
    • Verdict: handoff is well-defined.

    File Targets / Path Verification

    • [x] pal-e-deployments/overlays/basketball-api/prod/ — exists, contains kustomization.yaml + postgres.yaml. New admin-app-user-job.yaml goes here.
    • [x] pal-e-deployments/overlays/basketball-api/prod/postgres.yaml — verified: plain postgres:16-alpine Deployment, Service named postgres, namespace basketball-api, DB basketball, user basketball, password from Secret basketball-api-secrets key postgres-password.
    • [ ] pal-e-deployments/overlays/westside-admin/prod/westside-admin-secrets.enc.yaml — directory does NOT exist yet. Created by companion #1095/#133. Acceptable: ordering allows this Job (#1097) to land first; secret mirror coordinates with the consumer overlay.
    • Minor [BODY] nit (non-blocking): body says "Mounts the basketball-api postgres superuser credentials (existing Secret) for connecting as superuser." The role basketball is the DB owner (created via POSTGRES_USER env), not formally SUPERUSER. As DB owner it CAN create roles in its database and grant on objects it owns, so the SQL works — but the wording "superuser" is technically inaccurate. Consider "owner credentials" instead.

    Repo Placement

    OK. Tracking issue lives on pal-e-platform (correct — bootstrap repo). Manifest deliverable lives in pal-e-deployments (correct — overlay home). Secret deliverable also in pal-e-deployments via #133. Three repos involved, two PRs expected (one per repo as needed); body acknowledges this.

    Dependencies

    Foundational. Blocks: forgejo_admin/westside-admin#1 (Drizzle integration), forgejo_admin/pal-e-deployments#133 (overlay secret consumer = board item #1095). No upstream blockers. Can start immediately.

    Acceptance Criteria

    10 AC, all testable: role exists check, grants check, negative-grant check, password storage check, secret mirror check, idempotency check, three psql connection tests (read/write/blocked), SOP note creation. Each is verifiable by an agent with explicit psql commands. The blocked-write test (CREATE TABLE foo → permission denied) is the strongest minimality signal. Test Expectations adds a forward-grant test (create table as superuser, verify admin_app immediately has CRUD) that proves ALTER DEFAULT PRIVILEGES works. AC are excellent.

    Blast Radius

    Low. Plain-Postgres Deployment pattern is also used by westsidekingsandqueens and westside-streamlit — same admin_app pattern would extend cleanly later if needed, but only basketball is in scope here. Job touches only the basketball DB. Do NOT reuse basketball-api-secrets for the new credential — keep it in a fresh Secret to keep blast radius isolated. Body should call this out explicitly but it's implied by "Mirror the password Secret" pointing to a separate file.

    Decomposition Assessment

    3 deliverables, ~3-4 files (Job manifest + kustomization edit + secret mirror + SOP note), 10 AC but all run in seconds via psql. Estimated agent work: ~10 min. Fits a single agent pass at the upper edge of the 5-minute rule. No decomposition needed, but agent should be briefed tightly to avoid scope drift.

    Recommendation

    READY to advance backlog → todo. v1 [BODY] recommendations all addressed (concrete approach chosen, secret handoff explicit, cyrilgdn provider option removed). Two non-blocking carryovers:

    • [SCOPE] Carryover from v1: create arch-deployment-westside-admin and stub arch-postgres backing notes. Foundational refinement, does not block ticket execution but story:arch traceability is incomplete without it. Recommend Ava queue this as a separate ticket on board-westside-admin.
    • [BODY] Optional polish: replace "superuser credentials" with "owner credentials" (the role basketball is DB owner, not formally SUPERUSER). Non-blocking; SQL semantics are correct as written.
    • [BODY] Optional polish: add a one-line note that the Job's Secret holding admin_app password should be a NEW Secret (not basketball-api-secrets) for blast-radius isolation. Implied but worth being explicit.

    None of the optional polish items block READY. The arch-note carryover is a separate scoping ticket, not a body fix.

  • Verdict: APPROVED

    Re-review of board item #1092 after refinement. Both [BODY] gaps from review-1092-2026-04-25 are resolved. Scope is structurally READY for backlog -> todo.

    Refinement Diff vs v1

    • [x] [BODY] explicit blocker issue numbers -- Lineage now reads: "Hard depends on forgejo_admin/westside-admin#6 (scaffolding), #1 (Drizzle), #2 (Auth), #3 (Tenant helper). Indirect dep on forgejo_admin/pal-e-platform#301 (Keycloak client) + #302 (Postgres user)." Plus a "DEPENDS ON: #6, #1, #2, #3" line in Related. Dev agent can verify upstream merges before starting.
    • [x] [BODY] test runner pinned -- Test Expectations now ends with: "Test runner: whatever scaffolding #6 establishes (likely vitest)." Pinned to scaffolding outcome -- correct framing since #6 hasn't landed.

    Template Completeness (template-issue-feature)

    • [x] Type: Feature
    • [x] Lineage (now with explicit blocker issue numbers)
    • [x] Repo: forgejo_admin/westside-admin
    • [x] User Story (story-westside-admin-admin-row-crud)
    • [x] Context
    • [x] File Targets (Create + Modify + Do NOT create)
    • [x] Acceptance Criteria (9 testable)
    • [x] Test Expectations (with runner pinned)
    • [x] Constraints
    • [x] Checklist
    • [x] Related (with DEPENDS ON line)

    Traceability

    • [x] story:admin-row-crud label -- story note story-westside-admin-admin-row-crud exists, listed on project-westside-admin user-stories
    • [x] arch:page-server label -- backing artifact is arch-dataflow-westside-admin Flow 1 (referenced in Related). page-server is a SvelteKit primitive, no separate arch note expected (verified in v1 review)
    • [x] Forgejo issue forgejo_admin/westside-admin#4 -- open, well-formed
    • [x] Sensitive-column handling traces to arch-domain-westside-admin Key Decisions

    File Targets

    Same as v1 -- 5 new files + 1 modify, valid SvelteKit adapter-node paths, conditional on #6 scaffolding establishing the layout. No changes needed.

    Repo Placement

    OK. Single repo: forgejo_admin/westside-admin.

    Dependencies

    All 6 blockers verified on board-westside-admin:

    • #1087 (issue #6) arch:sveltekit-ssr -- backlog
    • #1089 (issue #1) arch:scoped-db -- backlog
    • #1090 (issue #2) arch:hooks-server -- backlog
    • #1091 (issue #3) arch:scoped-db -- backlog
    • #1096 (pal-e-platform #301) arch:keycloak -- backlog
    • #1097 (pal-e-platform #302) arch:postgres -- backlog

    Issue body now names #6, #1, #2, #3 explicitly, plus #301/#302 as indirect deps. Sequencing is unambiguous.

    Acceptance Criteria

    9 criteria, all testable. Unchanged from v1 (no flags). Test runner pinned to scaffolding outcome -- correct.

    Blast Radius

    Limited -- first feature in fresh repo, single coherent slice. Inline allowlist intentional per Constraints. Same assessment as v1.

    Decomposition Assessment

    5 new files + 1 modify, 9 AC, ~15-25 min agent time. At upper edge of 5-minute rule but tightly coupled (server load -> page render -> cell components). Decomposition would fragment a tight seam. No decomposition needed.

    Recommendation

    No action needed. Ticket is READY.

    Operational note for Ava: hold #1092 in todo after backlog -> todo move. Do not promote to next_up until all 6 blockers (#6, #1, #2, #3, pal-e-platform #301, #302) are merged. Sequencing is documented in the issue body so the dev agent can self-verify on dispatch.

  • Verdict: NEEDS_REFINEMENT

    Board item #1097 — Forgejo forgejo_admin/pal-e-platform#302. Type: Task. Reviewed against template-issue (Task variant uses Scope, not File Targets).

    Template Completeness

    • [x] Type — "Task"
    • [x] Lineage — companion to westside-admin deploy, can start independently
    • [x] Repo — forgejo_admin/pal-e-platform
    • [x] User Story — story-westside-admin-admin-row-crud
    • [x] Context
    • [x] Scope
    • [x] Acceptance Criteria — 8 testable items
    • [x] Test Expectations
    • [x] Constraints
    • [x] Checklist
    • [x] Related

    Traceability

    • [x] story:admin-row-crud label — present
    • [x] story note verified — story-westside-admin-admin-row-crud row exists in project-westside-admin user-stories table
    • [x] arch:postgres-grants label — present
    • [ ] arch note MISSING — arch-postgres-grants note does not exist in pal-e-docs. Parent arch-deployment-westside-admin (referenced from project-westside-admin Architecture section) also does not exist as a backing note. [SCOPE] Create arch-deployment-westside-admin (parent) and either arch-postgres-grants as a sub-note or roll grants into the parent.
    • [x] Forgejo issue — open, valid (#302)

    Repo Placement

    OK — pal-e-platform owns all IaC + Postgres provisioning. Companion kustomize wiring lives in pal-e-deployments (already covered by separate board item #1095, issue pal-e-deployments#133).

    Scope vs. Reality (file/code investigation)

    The issue body presents 3 provisioning options and tells the agent to "inspect ~/pal-e-platform/ to identify how Postgres users are currently managed." Pre-review findings:

    • No cyrilgdn/postgresql provider is registered. terraform/providers.tf + terraform/versions.tf declare hashicorp/kubernetes, hashicorp/helm, aminueza/minio. Option 1 is therefore add provider + create role, not "follow existing pattern."
    • basketball-api Postgres is NOT a CNPG cluster. It is a plain postgres:16-alpine Deployment + PVC in pal-e-deployments/overlays/basketball-api/prod/postgres.yaml, namespace basketball-api. CNPG-managed-user pattern (used for paledocs in terraform/modules/database/main.tf:69-78) does not apply here.
    • The closest existing precedent is the paledocs pattern: terraform creates a kubernetes_secret_v1 in the consumer namespace (pal-e-app) holding DATABASE_URL referencing the CNPG cluster. For basketball, there is no equivalent CNPG-managed-user mechanism — the role must be created via SQL against the running Deployment.
    • No existing admin_app or sub-user pattern exists anywhere in pal-e-platform or pal-e-deployments.

    Dependencies

    Foundational — blocks pal-e-deployments#133 (kustomize secret consumer, board item #1095) and the SvelteKit DB-connection work in westside-admin#6, #7. Currently 11 items on board-westside-admin in backlog; this is a top-of-pipeline item. No blockers above it.

    Acceptance Criteria

    All 8 AC are testable and verifiable by an agent (psql commands explicit, secret reference documentable). The blocked AC (CREATE TABLE → permission denied) is the strongest signal that grants are correct. AC are good.

    Blast Radius

    Low if executed correctly. Same plain-Postgres Deployment pattern is used by other westside services (westsidekingsandqueens, westside-streamlit) — the same admin_app pattern would extend cleanly if needed later, but only basketball is in scope here. Do not reuse basketball-api-secrets for the new credential — keep it in a fresh Secret to keep blast radius isolated.

    Decomposition Assessment

    ~3-4 files (terraform addition + secret + SOP if needed), 8 AC but all run in ~1 minute of psql. Estimated agent work: 5-10 min depending on path chosen. Fits a single agent pass. No decomposition needed.

    Recommendation

    • [BODY] Replace "Inspect ~/pal-e-platform/ to identify how Postgres users are currently managed" + 3-option fork with a concrete chosen approach. Pre-investigation already done in this review: cyrilgdn/postgresql not registered, basketball is not CNPG. Recommended approach: a one-shot Job/migration applied via the basketball-api kustomize overlay (Option 2), OR add a small SQL bootstrap (psql exec via terraform kubernetes_job_v1) — pick one explicitly.
    • [BODY] Clarify the secret-handoff: does pal-e-platform terraform create the kubernetes_secret_v1 "westside-admin-secrets" directly in the westside-admin namespace (paledocs precedent), or does the kustomize overlay in pal-e-deployments#133 create it from a SealedSecret/sops file? Pick one and document the contract so issue #1095 has an unambiguous consumer side.
    • [BODY] Remove or downgrade the "Terraform via cyrilgdn/postgresql" option — it would require also adding the provider + reaching into the basketball-api namespace's running Deployment, which is more scope than the ticket implies.
    • [SCOPE] Create architecture note arch-deployment-westside-admin (referenced by project-westside-admin but missing). Either include grants there or create separate arch-postgres-grants. Foundational refinement; should not block ticket execution but story:arch traceability is incomplete without it.

    Once body is tightened (concrete approach + secret handoff contract) and arch note is sketched (even a stub), this ticket is READY.

  • Verdict: NEEDS_REFINEMENT

    Scope is well-written and traceable. One [LABEL] fix and several [SCOPE] dependency-sequencing flags before this can move backlog -> todo. No structural rewrite needed.

    Template Completeness (template-issue-feature)

    • [x] Type: Feature
    • [x] Lineage
    • [x] Repo: forgejo_admin/westside-admin
    • [x] User Story (references story-westside-admin-admin-row-crud)
    • [x] Context (clear motivation, locked decision called out)
    • [x] File Targets (Create + Modify + Do NOT create)
    • [x] Acceptance Criteria (9 testable items)
    • [x] Test Expectations (integration + unit + manual)
    • [x] Constraints (scopedDb, no client fetch, no Tailwind, allowlist inline)
    • [x] Checklist
    • [x] Related

    Traceability

    • [x] story:admin-row-crud label - Story note story-westside-admin-admin-row-crud verified, entry exists on project-westside-admin user-stories section
    • [x] arch:page-server label - generic component label, not a backing-note expectation. Backing arch artifact is arch-dataflow-westside-admin Flow 1 (referenced in Related). No arch-page-server note exists and none is expected (page-server is a SvelteKit primitive, not a domain-level component).
    • [x] Forgejo issue forgejo_admin/westside-admin#4 - open, well-formed
    • [x] Sensitive-column handling traces to arch-domain-westside-admin Key Decisions (contract_token explicitly listed there)

    File Targets

    All listed files are new (the repo currently contains only README.md). Path conventions are correct for SvelteKit adapter-node. Paths cannot be pre-verified against existing code because scaffolding ticket #6 (arch:sveltekit-server) has not landed.

    • [x] src/routes/players/+page.server.ts - standard SvelteKit load + actions location
    • [x] src/routes/players/+page.svelte - standard
    • [x] src/lib/components/EnumBadge.svelte, DateCell.svelte, JsonbCell.svelte - standard $lib pattern, server-safe (no $lib/server import needed)
    • [x] src/routes/+layout.svelte - exists conceptually after scaffolding ticket #6
    • [!] All paths are valid conditional on #6 (sveltekit-server scaffolding) landing first. This is correctly captured in Lineage.

    Repo Placement

    OK. Single repo: forgejo_admin/westside-admin. No cross-repo work.

    Dependencies

    Lineage names them ("scaffolding + Drizzle + auth + tenant helper"). Mapped to board items:

    • #1087 (issue #6) arch:sveltekit-server - scaffolding (BLOCKER)
    • #1089 (issue #1) arch:scopeddb-helper - Drizzle setup (BLOCKER)
    • #1091 (issue #3) arch:scopeddb-helper - tenant helper (BLOCKER)
    • #1090 (issue #2) arch:hooks-server - auth (BLOCKER, #1093 also depends)
    • #1096 (pal-e-platform #301) arch:keycloak - Keycloak realm/client (BLOCKER for live auth)
    • #1097 (pal-e-platform #302) arch:postgres-grants - cross-ns DB access (BLOCKER for live data)

    None of these are in in_progress; all are siblings in backlog. The dependencies are correctly stated in Lineage but the issue body does not list specific Forgejo issue numbers. [BODY] recommendation: enumerate blocker issue numbers under Lineage so the dev agent can verify upstream merges before starting.

    Acceptance Criteria

    9 criteria, all testable. Each maps to either an integration test (rows, pagination, filter, auth gates) or a render assertion (badges, dates, jsonb collapse, sensitive-column exclusion). Test Expectations section names real vitest-shaped commands. Manual visual check is appropriate for v1. Allowlist enforcement at +page.server.ts level is correct (server-side, before serialization).

    Blast Radius

    Limited - first feature in a fresh repo. No sibling services to mirror. One forward-looking concern: the inline column allowlist is intentional per Constraints ("refactor when 2nd table arrives"). Good - prevents premature abstraction. No downstream consumers of this route yet.

    Decomposition Assessment (5-minute rule)

    • 5 new files + 1 modify = 6 file targets, single repo
    • 9 AC - at the threshold but tightly coupled (single route + 3 cell components)
    • Estimated agent time: 15-25 min for a competent agent (3 small Svelte components + 1 server load + 1 page render + nav link)

    This is at the upper edge of the 5-minute rule but is a single coherent slice. Decomposition would fragment a tight seam (server load -> page render -> cell components are mutually dependent). Recommendation: keep as one ticket, accept ~20 min agent run. No decomposition needed.

    Recommendation

    • [BODY] Add explicit blocker issue numbers in Lineage section: "Blocked by forgejo_admin/westside-admin#6 (scaffolding), #1, #3 (scopedDb), #2 (hooks/auth), and pal-e-platform#301 (Keycloak), #302 (postgres-grants)." This lets the dev agent verify upstream PRs are merged before starting.
    • [SCOPE] Confirm sequencing: this ticket cannot move past next_up until all 6 blockers are merged. Ava should hold this in todo after refinement and not promote to next_up until the dependency chain clears.
    • [BODY] Optional: name the test runner explicitly (likely npm test / vitest) once scaffolding ticket #6 establishes it.

    After the [BODY] addition, this ticket is structurally READY. The verdict is NEEDS_REFINEMENT only on dependency-traceability hygiene, not on scope quality.

Untyped 1
  • Validation: pal-e-services#73 — westside-admin Keycloak client → tofu + dev URI

    Verdict: PASS

    PR: forgejo_admin/pal-e-services#74 (squash-merged 2026-05-04 ~01:55 UTC)

    Issue: forgejo_admin/pal-e-services#73 (closed via Closes #73)

    Verification steps

    1. Local FF: git -C ~/pal-e-services pull --ff-only forgejo main → fast-forwarded to a80dc54 feat(keycloak): bring westside-admin client under tofu + add dev URI (#73) (#74).
    2. tfvars edit: appended the canonical westside-admin entry from k3s.tfvars.example into local terraform/k3s.tfvars (gitignored). tofu fmt clean. tofu validate Success.
    3. Import: tofu import -var-file=k3s.tfvars -lock=false 'keycloak_openid_client.this["westside-admin"]' westside-basketball/c5749fa6-4d1e-4b07-bdc0-e371bf65e1e5Import successful.
    4. Plan: tofu plan -var-file=k3s.tfvars -lock=false -target='keycloak_openid_client.this["westside-admin"]'0 to add, 1 to change, 0 to destroy exactly matching PR #74's documented Step 2 plan (URI + origin add only).
    5. Apply: tofu apply -var-file=k3s.tfvars -lock=false -target='keycloak_openid_client.this["westside-admin"]' -auto-approve → succeeded.
    6. Live verify (Keycloak admin API):
      redirectUris:
        https://westside-admin-dev.tail5b443a.ts.net/auth/callback
        https://westside-admin.tail5b443a.ts.net/auth/callback
      webOrigins:
        https://westside-admin-dev.tail5b443a.ts.net
        https://westside-admin.tail5b443a.ts.net
      Both prod and dev URIs/origins present.
    7. Prod regression check: curl -I https://westside-admin.tail5b443a.ts.net/ returns 302 to /auth/login?redirect=%2F — auth gate intact, no client-secret rotation, no admin-session disruption.

    What this enables

    Sibling work in pal-e-deployments#148/PR #149 can now drive an OIDC flow against the dev hostname. (See validation-148-* note when that ticket settles — currently superseded by pal-e-deployments#150 due to a PSA/namespace structural defect uncovered at apply time.)

    Discovered scope (tracked separately)

    • Schema extension use_refresh_tokens was applied to terraform/keycloak.tf + terraform/variables.tf despite the issue's "do not touch keycloak.tf" guidance. Justification documented in PR body and accepted by QA: required to mirror live state without rotating refresh tokens on the live admin session. Additive change with default false; verified zero-drift on 4 sibling clients (westside-landing, pal-e-app, mcd-tracker-app, playme2k).

    Cross-references

    • Board item: board-westside-admin #1142
    • Project: project-westside-admin (status section updated 2026-05-04)
    • Predecessor incident referenced from this validation: 2026-05-04 ~02:00 UTC westside-admin namespace cascade (recovered; tracked in pal-e-deployments#150)
Review 23
  • Verdict: NEEDS_REFINEMENT

    Reviewer: Dottie. Board item #1143 on board-westside-admin. Forgejo issue: forgejo_admin/pal-e-deployments#148. The scope is well-shaped (clear file list, real reference overlay, correct security posture rationale), but five concrete, blocking issues will cause the dev pod to fail to come up if shipped as written. Fix in the issue body before advancing to todo.

    Template Completeness

    • [x] Type — Feature
    • [x] Lineage — links pal-e-services#73 and notes blocking dependency
    • [x] Repo — pal-e-deployments
    • [x] User Story — Marcus / dev hot-reload / iteration
    • [x] Context — pattern reference, auth model, env vars, port rationale, ingress posture
    • [x] File Targets — five files enumerated with content sketch
    • [x] Acceptance Criteria — 7 items, all observable
    • [x] Test Expectations — kustomize lint + smoke curl
    • [x] Constraints — mirror reference, reuse Secret, no package.json edits, FF main pre-branch
    • [x] Checklist — present
    • [x] Related — pal-e-services#73 cross-reference

    Traceability

    • [x] type:feature label — present on board item
    • [x] arch:dev-overlay label — present on board item
    • [ ] arch:dev-overlay backing note MISSING — search_notes("arch-dev-overlay") returns empty. [SCOPE] Create arch-dev-overlay note (or reuse arch-deployment-westside-admin with a Dev section appended).
    • [x] story:marcus-admin-dev-access label — present on board item
    • [ ] story:marcus-admin-dev-access backing entry MISSING — project-westside-admin#user-stories contains only admin-row-crud. [SCOPE] Add marcus-admin-dev-access row to project-westside-admin User Stories table (Key, Story Note, Role=Marcus, Success Metric).
    • [x] Forgejo issue valid and open — pal-e-deployments#148.
    • [x] Sibling issue valid — pal-e-services#73 (also open, same backing story label).

    File Targets

    • [x] overlays/westside-admin/prod/* — verified to exist on origin/main (5 files, including `westside-admin-secrets.enc.yaml` and a `deployment-patch.yaml` that targets the standard base name `app`).
    • [x] overlays/pal-e-dictionary/dev/ reference — verified to exist locally on archbox at /home/ldraney/pal-e-deployments/overlays/pal-e-dictionary/dev/. NOTE: it does not exist on origin/main (untracked in cwd). Reference is informational only; the dev agent must read the local files to mirror them.
    • [x] ~/westside-admin/src/hooks.server.ts — verified. Per-request gate present (REQUIRED_ROLE = 'admin', isAuthExcluded excludes /auth/* + /health, role check at line 264). Auth model claim is accurate: dev pod inherits the gate automatically.
    • [x] Local ~/westside-admin is 3 commits behind origin/main at review time — Constraints section already calls this out (git pull origin main).
    • [x] Local ~/pal-e-deployments is on stale branch 75-rename-pal-e-production — Constraints already require FF main first.
    • [x] westside-admin-secrets Secret — verified to exist in westside-admin namespace. Contains 6 keys: COOKIE_SIGNING_KEY, DATABASE_URL, KEYCLOAK_CLIENT_ID, KEYCLOAK_CLIENT_SECRET, KEYCLOAK_REALM, KEYCLOAK_URL. The issue says DATABASE_URL is "NOT in this Secret" — that is wrong. See Issue 4 below.

    Repo Placement

    OK — issue correctly filed on pal-e-deployments. The redirect-URI sibling on pal-e-services is a separate issue (#73), correctly split.

    Dependencies

    • Blocking: pal-e-services#73 — Keycloak client must list https://westside-admin-dev.tail5b443a.ts.net/auth/callback as a valid redirect URI. Without it, OIDC callback returns 400. Issue body documents this clearly. Board item A on board-westside-admin#1142 tracks it.
    • Implicit dependency NOT documented: ArgoCD Application registration. The existing westside-admin ArgoCD Application is sourced at overlays/westside-admin/prod — NOT the parent directory. Adding overlays/westside-admin/dev/ will be invisible to ArgoCD until a new Application resource (e.g., westside-admin-dev) is created in the cluster pointing at the dev overlay path. The issue's AC #2 ("After ArgoCD syncs ... pod 1/1 Running") will silently never satisfy because Argo isn't watching the new path. [BODY] Add an explicit Constraints/File-Targets bullet: "Register a new ArgoCD Application westside-admin-dev pointing at overlays/westside-admin/dev (mirror the pattern used for pal-e-dictionary-dev) — OR — explicitly state that this overlay is applied via kubectl apply -k by Lucas and lives outside ArgoCD." Without that decision, the dev agent has no path to satisfy AC #2.

    Acceptance Criteria

    Seven AC, all observable. Mostly testable. Two concerns:

    • AC #2 is not satisfiable as written until the ArgoCD-vs-manual-apply question is resolved (see Dependencies).
    • AC #6 (nslookup westside-admin-dev.tail5b443a.ts.net from outside the tailnet does not resolve to a public IP) is testable but flaky — Tailscale magicdns sometimes returns nothing, sometimes returns the tailnet IP, depending on resolver. A more robust assertion: "from a non-tailnet host, an HTTPS request to the hostname connection-refuses or DNS-NXDOMAINs; from a tailnet host, the TLS cert is issued and the SvelteKit dev server responds." Optional polish, not blocking.

    Blast Radius

    Five hard blockers found. Listing in priority order.

    1. [BLOCKER 1 — Port 5177 is taken] The issue claims "Next free: 5177" based on observing dictionary=5175, kanban=5176. That observation is stale. Live cluster check at review time: pal-e-hub/pal-e-hub-dev stood up 6 min ago with containerPort=5177. Other in-cluster ports in the 5170–5200 range: mcd-tracker-dev=5173, westside-dev=5174, pal-e-dictionary-dev=5175, pal-e-kanban-dev=5176, pal-e-hub-dev=5177, playme2k-dev=5175 (own ns, no conflict). Next actually free: 5178. [BODY] Replace every 5177 in the issue body (Context, deployment.yaml sketch, service.yaml sketch) with 5178. Also update the kustomization.yaml header comment to "Vite port 5178".
    2. [BLOCKER 2 — PSA restricted will reject the dev pod] The westside-admin namespace has pod-security.kubernetes.io/enforce: restricted applied (verified live + present in overlays/westside-admin/prod/namespace.yaml on origin/main). The reference overlays/pal-e-dictionary/dev/deployment.yaml has no securityContext — and the pal-e-dictionary namespace has no PSA enforcement, which is why the reference works there. Mirroring "exactly" into westside-admin will fail admission with `seccompProfile not set / runAsNonRoot not set`. [BODY] Add to deployment.yaml sketch: pod-level securityContext with runAsNonRoot: true, runAsUser: 1000, runAsGroup: 1000, fsGroup: 1000, seccompProfile.type: RuntimeDefault; and container-level securityContext with allowPrivilegeEscalation: false, readOnlyRootFilesystem: false (Vite needs writes), capabilities.drop: ["ALL"]. Also note: node:22 default user is root — image will need an explicit non-root user. Two viable options: (a) set image: node:22-bookworm-slim and pre-create a writable workdir under /tmp/app, or (b) use the existing precedent: prod's deployment-patch.yaml sets runAsUser: 1000 on the standard base — replicate that. The simpler path (b): copy the prod patch's securityContext stanza verbatim into the new dev deployment.
    3. [BLOCKER 3 — Reference overlay contradicts "tailnet-only" requirement] Constraints say "Mirror overlays/pal-e-dictionary/dev/ structure exactly. Same filenames, same yaml shape, same idioms." The reference ingress.yaml has tailscale.com/funnel: "true". The Context section says "Do not add tailscale.com/funnel: \"true\"". These two instructions directly conflict — a literal mirror produces a public funnel, which the issue explicitly forbids per feedback_funnel_requires_auth. [BODY] Reword the Constraint to "Mirror overlays/pal-e-dictionary/dev/ structure (filenames + general yaml shape), with two intentional deviations: (a) port 5178 not 5175; (b) ingress.yaml omits tailscale.com/funnel: \"true\" — tailnet-only ingress only." Also note the prod ingress IS funneled — the dev pod is the deviation, and the reasoning (admin-grade data) is sound.
    4. [BLOCKER 4 — DATABASE_URL claim is wrong] Issue says: "DATABASE_URL is NOT needed for dev — it lives in a separate Secret (admin-app-db-url) used by prod only." Live check: westside-admin-secrets contains 6 keys including DATABASE_URL. Both Secrets contain it (the prod patch sources DATABASE_URL from admin-app-db-url, but the legacy mirrored copy in westside-admin-secrets still exists). Practical effect of envFrom: [secretRef: westside-admin-secrets]: the dev pod WILL receive a real DATABASE_URL. That is probably fine for Marcus's read paths — but the issue's claim is factually wrong, and a future reader will be misled. [BODY] Reword to: "DATABASE_URL is present in westside-admin-secrets (legacy mirror). The dev pod will inherit it via envFrom; this is acceptable for Marcus's read-only views but the dev pod must NOT be used to exercise write paths until a separate dev DB role is wired (out of scope for this ticket — file follow-up if write testing is needed)."
    5. [BLOCKER 5 — Selector collision risk] Issue says "deployment named distinctly from prod (suggest westside-admin-dev)". Good. But the prod deployment selector is app: westside-admin (verified live + in prod kustomization patch). The dev deployment must use a distinct label key/value across all three of: spec.selector.matchLabels, spec.template.metadata.labels, and the Service's spec.selector. The reference pal-e-dictionary overlay uses app: pal-e-dictionary-dev (distinct from a non-existent prod) — the dev agent could accidentally just use app: westside-admin if they copy too literally. [BODY] Add an explicit Constraint: "All three label fields (deployment selector, deployment pod template, service selector) must be app: westside-admin-dev — NOT app: westside-admin — to avoid selector collision with the prod deployment in the same namespace. Verify with kubectl -n westside-admin get pods -l app=westside-admin-dev returning only the dev pod and kubectl -n westside-admin get pods -l app=westside-admin returning only the prod pod after kubectl apply." Without this, an over-broad selector returns both pods and the dev Service load-balances 50% of dev traffic to prod (or vice versa).

    Decomposition Assessment

    Five files in one repo, one agent, ~5 ACs to verify post-merge. Estimated dev-agent time: 25–40 min including kustomize lint + PR + describing apply path. Below the 5-minute rule's per-AC ceiling but well within a single ticket. No decomposition needed. Keep as one ticket.

    Recommendation

    Verdict NEEDS_REFINEMENT. Before advancing to todo, apply these to the Forgejo issue body (the single source of truth dev agents read):

    • [BODY] Replace 5177 with 5178 everywhere in the issue (Context port table, deployment containerPort, service targetPort, kustomization comment).
    • [BODY] Add securityContext requirement (pod + container level) per Blocker 2; reference prod's deployment-patch.yaml securityContext as the template to copy.
    • [BODY] Reword the "mirror exactly" Constraint to call out the two intentional deviations (port + no funnel annotation).
    • [BODY] Fix the DATABASE_URL Context paragraph to reflect that the Secret DOES contain it; note dev is read-only-acceptable but flag for follow-up if write testing becomes needed.
    • [BODY] Add explicit Constraint requiring app: westside-admin-dev across deployment selector, pod template labels, and service selector to avoid selector collision with prod in the same namespace.
    • [BODY] Resolve the ArgoCD-vs-manual-apply question: either add a Constraint/File-Target requiring a new ArgoCD westside-admin-dev Application resource pointing at the new path, OR explicitly state the dev overlay is hand-applied with kubectl apply -k and lives outside ArgoCD. Reword AC #2 accordingly.
    • [SCOPE] Add marcus-admin-dev-access row to project-westside-admin#user-stories table. (Dottie can do this on Ava's instruction once Ava confirms wording.)
    • [SCOPE] Decide whether arch:dev-overlay deserves a standalone arch-dev-overlay note or appends a Dev section to arch-deployment-westside-admin.

    Once the six [BODY] items land in the issue, the ticket is ready for re-review and APPROVED → todo. The two [SCOPE] items are doc-side and don't block dev work but should be tracked so traceability is complete by the time the PR merges.

  • Verdict: APPROVED

    Board item: #1135 on board-pal-e-platform.
    Forgejo issue: forgejo_admin/westside-admin#15 (open).
    Type: Feature.
    Lineage: Sub-task 2 of 4 from decomposition of westside-admin#2. Depends on #14 (merged today as PR #18). Siblings still in backlog: #16 (auth endpoints), #17 (403 page).
    One-line: Scope is tight, file targets are accurate against post-#14 ground truth, all three claimed lib exports actually exist on main. Two tiny BODY tightening items noted but neither blocks dispatch.

    Template Completeness

    • [x] Type — Feature
    • [x] Lineage — cites parent #2, dependency #14, siblings #16/#17
    • [x] Repo — forgejo_admin/westside-admin
    • [x] User Story — story-westside-admin-admin-row-crud with funnel-auth rationale
    • [x] Context — explains the per-request enforcement role, lazy-refresh behavior, how it consumes #14, does-not-define for #16/#17
    • [x] File Targets — 1 create, 1 update, 3 explicit do-not-touch directories with their owning sub-tasks named
    • [x] Acceptance Criteria — 8 testable items
    • [x] Test Expectations — manual integration matrix (4 cases) with stated rationale for skipping vitest target until #17 lands
    • [x] Constraints — SvelteKit cookie API, locals-not-globalThis, exact cookie-name string, rewrite-not-redirect for 403, no route allowlist, import path style
    • [x] Checklist — includes funnel-auth review per feedback_funnel_requires_auth
    • [x] Related — project page, parent, dep, dataflow note, funnel-auth memory

    Traceability

    • [x] story:admin-row-crud label — matches project Safety Constraints contract
    • [x] story note verified — entry present in project-westside-admin user-stories table (story-westside-admin-admin-row-crud)
    • [x] arch:westside-admin label — umbrella satisfied by arch-dataflow-westside-admin Flow 1, which sequences this hook explicitly (browser to SK to hooks.server.ts to JWKS validation to role parse to 403 OR locals.user to load())
    • [!] arch:keycloak label has NO backing note — same gap as siblings #14 (review-1134), parent #1132, dep #1131. arch-keycloak still does not exist; arch-dataflow-westside-admin Flow 1 jointly covers the Keycloak surface for this ticket. Per the carry-over [SCOPE] recommendation in review-1134, Ava's call whether to author arch-keycloak now or defer. Not a per-ticket blocker.
    • [x] Forgejo issue valid and open. Target repo forgejo_admin/westside-admin, sub-task 2 of 4.

    File Targets

    Repo at /home/ldraney/westside-admin. Verified against ground truth on main (HEAD 982df5b, post-#18 merge, post-pull confirmed).

    • [x] src/hooks.server.ts (create) — does not exist yet (verified via find). Correct location: SvelteKit picks this path up automatically. OK to create.
    • [x] src/app.d.ts (update) — exists with the unmodified scaffold (App namespace block with all interfaces commented out). Adding App.Locals.user here is the one-line uncomment + fill, exactly the right surface.
    • [x] Do-NOT-touch:
      • src/lib/server/keycloak.ts — exists (PR #18 merged). 537 lines. All three claimed consumer exports verified by direct read: verifyKeycloakJwt(token: string): Promise<VerifiedJwt> at line 283; decryptCookiePayload(ciphertext: string): unknown | null at line 358 (returns null on tamper/wrong-key/malformed/non-JSON, lining up with AC #4); refreshTokensIfNeeded(tokens: KeycloakTokens): Promise<KeycloakTokens> at line 500 (no-op when not in window; throws TokenRefreshError on non-2xx Keycloak).
      • src/routes/auth/* — does not exist yet (sub-task #16). Hook redirects to it.
      • src/routes/(unauthorized)/* — does not exist yet (sub-task #17). Hook rewrites to it.
    • [x] vitest.config.ts + package.json test scripts landed via #14/PR #18 (verified). The 'no vitest target' note in Test Expectations is accurate scoping (manual integration only for this sub-task), but vitest IS now available if the agent wants to add a unit test for cookie parsing or 30s-window calculation as a free-throw.

    Sanity check on cookie-name attribution: the lib does NOT export a cookie-name constant. The issue body says the cookie name 'MUST be exactly westside_admin_session' and asserts this 'matches #14's expectation' — strictly, #14 has no expectation: the lib is cookie-name-agnostic by design (it operates on ciphertext strings). This is the right separation of concerns; the hook owns the cookie name. The directive is unambiguous; the attribution is just a hair off. Optional [BODY] below.

    Repo Placement

    OK — single repo (forgejo_admin/westside-admin). No cross-repo concerns. Env-var landing already merged in pal-e-deployments#147; ArgoCD reconciling per prompt context. The hook code itself does not block on cluster-Secret rollout — it reads env vars at request time, not at module load.

    Dependencies

    • westside-admin#14 (keycloak.ts lib) — MERGED as PR #18 (982df5b). Board item #1134 sits in validation column. Direct unblock.
    • pal-e-deployments#147 (KEYCLOAK_CLIENT_SECRET landing) — MERGED. ArgoCD reconciling per prompt. The hook's env-var reads (KEYCLOAK_URL, KEYCLOAK_REALM, KEYCLOAK_CLIENT_ID, KEYCLOAK_CLIENT_SECRET, COOKIE_SIGNING_KEY) all happen at request time via the lib's requireEnv; absent envs throw MissingEnvError. For dev validation against npm run dev on archbox, the agent will need a local .env shim — not a blocker but worth flagging.
    • Sibling sub-task #16 (auth endpoints) — OPEN, in backlog. The hook redirects to /auth/login?redirect={url}; sub-task #16 must create this endpoint. Hook does NOT block on #16 to land code, but end-to-end manual ACs cannot fully pass until #16 + #17 ship together. Acknowledged in the issue body's Test Expectations.
    • Sibling sub-task #17 (403 page) — OPEN, in backlog. Hook rewrites to the (unauthorized) route group; #17 creates the page itself. Hook code can ship; the rewrite target is a 404 until #17 lands.
    • Implicit ordering: #15 then #16 then #17, with full E2E only after #17. Acknowledged.

    Acceptance Criteria

    8 ACs, all verifiable. Notable strengths:

    • AC #4 (tampered cookie treated as anonymous, never throws) — directly maps to decryptCookiePayload's null-on-tamper contract verified above. Crisp.
    • AC #3 (missing-admin = rewrite, NOT redirect) — explicit and re-stated in Constraints. Prevents the common bug of redirecting to /403 (which would create a redirect loop if /403 itself is admin-gated).
    • AC #6 (cookie attributes on every refresh) — covers the failure mode where a refresh path forgets to re-set HttpOnly+Secure+SameSite.
    • AC #7 (no tokens / refresh tokens / ciphertext in any log) — mechanical grep verification post-implementation.
    • AC #8 (event.locals.user typed end-to-end) — couples the hook to the app.d.ts update; agent cannot half-finish either side.

    One small gap (NOT blocking): the issue body does not specify what happens if refreshTokensIfNeeded throws TokenRefreshError mid-request. The implication from AC #4 is 'treat as anonymous' (since refresh failure means session is no longer trustworthy). Agent is expected to infer this; the constraint 'tampered/expired/wrong-key cookie is treated as anonymous, never throws' reasonably extends to 'refresh-failure cookie.' Optional [BODY] below.

    Blast Radius

    Maximum. hooks.server.ts gates EVERY admin request — every page load, every form action, every health-check route. A bug here is a 100% outage or, worse, a 100% security hole on a public Tailscale funnel. feedback_funnel_requires_auth applies in full force.

    Specific risks the dev agent should be aware of:

    • Health-check exclusion: issue body says 'every route is admin-gated by default; the only exclusions are /auth/* and /(unauthorized)'. src/routes/health already exists (verified via ls). Either it must be admin-gated (and Kubernetes liveness/readiness probes need to be configured to skip auth, which they cannot directly — the funnel does not know about probes), OR the hook needs a third exclusion for /health. This is a real implementation question that the issue body does not resolve. See Recommendation.
    • Cookie attribute symmetry: the lib does not enforce cookie attributes (it returns ciphertext strings only). Every hook code path that calls event.cookies.set(...) must include { httpOnly: true, secure: true, sameSite: 'lax', path: '/' }. SvelteKit's secure default is only true in HTTPS contexts; httpOnly defaults to true but explicit is better. AC #6 covers this conceptually.
    • Locals shape lock-in: the App.Locals.user type defined in this PR becomes a load-bearing interface for every +page.server.ts in the project. Renaming roles to realmRoles later means touching every consumer. Issue body's explicit shape — { sub: string; email: string; name: string; roles: string[] } — is fine; just call it out as locked once shipped.

    Decomposition Assessment

    No decomposition needed.

    • File count: 1 create + 1 update = 2 files. Well under threshold.
    • AC count: 8 — over the 5-AC paper rule but, like #14, the ACs all map to a single file's behavior. They are facets of one handler function (handle), not orthogonal user-visible features.
    • Estimated time: 5-8 min agent run. The hook is roughly 60-100 lines of TypeScript.
    • Cohesion: handle is necessarily a single function in one file. Splitting (e.g., one PR for happy path, one for refresh, one for 403) would create N partially-broken intermediate states. Anti-pattern.

    Recommendation

    Each recommendation tagged for skill-refine-ticket machine consumption:

    • [BODY] (should-fix, not blocker) Resolve the /health route question. Add a third exclusion to the 'every route is admin-gated by default; the only exclusions are /auth/* and /(unauthorized)' sentence — either explicitly include /health as a third exclusion, or explicitly state '/health is admin-gated; Kubernetes probes hit the pod via a cluster-internal path.' Without this clarification the agent has to guess. Recommend: exclude /health (the simpler answer).
    • [BODY] (optional) Add to AC #4 or as a new AC: 'If refreshTokensIfNeeded throws TokenRefreshError, the request is treated as anonymous (cookie cleared, 302 to /auth/login).' Currently inferable but explicit is better.
    • [BODY] (optional, cosmetic) Tighten the Constraints sentence 'Cookie name MUST be exactly westside_admin_session (matches #14's expectation and the spec in #2)' to 'Cookie name MUST be exactly westside_admin_session (defined here for the first time; #14's lib is cookie-name-agnostic by design — the hook owns this constant; matches the spec in #2).' More accurate.
    • [SCOPE] (carry-over from review-1134) Decide whether to create arch-keycloak note now or continue deferring. Same status as before: arch-dataflow-westside-admin Flow 1 covers the surface this ticket touches. Ava's call.
    • Sequencing note for whoever dispatches: #15 is unblocked NOW (#14/PR #18 merged, env vars merged via #147). Ready to move backlog to todo to next_up to in_progress. Sibling sub-tasks #16/#17 can start in parallel if Ava wants — they don't share files with #15 (do-not-touch fences are clean) and the import surface they need from $lib/server/keycloak is already on main. The only constraint is end-to-end manual ACs require all four landed.

    Key Finding Summary

    The ticket is well-scoped and dispatch-ready. Three small [BODY] items: one should-fix (the /health route exclusion question), two optional (refresh-failure AC clarity, cookie-name attribution tightening). None block dispatch — the dev agent has enough context to infer reasonable answers. Verdict: APPROVED. Recommend Ava also routes the /health question to the dev agent's prompt rather than blocking the ticket on a body refinement, since it is a 30-second clarification.

  • Verdict: APPROVED

    Both READY and APPROVED are accepted as passing verdicts by the check-board-advance hook. Use either keyword interchangeably.

    Template Completeness

    • [x] Type — Feature
    • [x] Lineage — explicit decomposition from #2, sub-task 3 of 4, depends on #14, independent of sub-tasks 2 and 4
    • [x] Repo — forgejo_admin/westside-admin
    • [x] User Story — story-westside-admin-admin-row-crud with rationale (these endpoints are the OIDC protocol surface, sit OUTSIDE the admin-role gate)
    • [x] Context — three-paragraph walkthrough of each endpoint's behavior, transient-cookie semantics, and Keycloak interactions
    • [x] File Targets — 3 create paths + 3 do-not-touch paths
    • [x] Acceptance Criteria — 7 ACs covering login state-set, callback CSRF (state mismatch + missing state), happy-path token exchange, token-endpoint failure handling, logout SLO, POST-only logout, and logging hygiene
    • [x] Test Expectations — manual integration tests against staging Keycloak (pnpm dev + browser), no vitest target (correctly noted as integration boundary)
    • [x] Constraints — fetch only (no axios/got), event.cookies API only, two clearly-articulated PKCE storage options, exact redirect URI match
    • [x] Checklist — 5 items including PR funnel-auth review per feedback_funnel_requires_auth
    • [x] Related — links to project, parent #2, dependency #14, SOP, and feedback note

    Traceability

    • [x] story:admin-row-crud label — verified, single v1 story per project-westside-admin Safety Constraints contract
    • [x] story note verified — story-westside-admin-admin-row-crud (id 1616) exists, listed in project-westside-admin#user-stories table
    • [~] arch:westside-admin label — no umbrella arch-westside-admin note, but project page Architecture section links three concrete arch notes (arch-domain-westside-admin, arch-dataflow-westside-admin, arch-deployment-westside-admin) and arch-scoped-db-westside-admin. The label is treated as a satisfied umbrella per the same precedent set in review-1132-2026-05-03. Acceptable.
    • [!] arch:keycloak has NO backing note — search_notes("arch-keycloak") returns empty. This gap was already flagged in review-1132-2026-05-03 for board item #1132. Carrying forward the same recommendation rather than re-blocking each sub-task; this label is a known umbrella gap across the entire admin-row-crud chain (#1132, #1135, #1136, #1134) and Ava holds the call on whether to backfill arch-keycloak or treat the dataflow note as sufficient documentation.
    • [x] Forgejo issue — forgejo_admin/westside-admin#16 is open, body is current, no Forgejo labels (board labels are the source of truth here)

    File Targets

    • [x] src/routes/auth/login/+server.ts — does not exist (correctly listed as "Create"). Parent src/routes/ exists with current children +layout.svelte, +page.svelte, health/ — no collision.
    • [x] src/routes/auth/callback/+server.ts — does not exist. No collision.
    • [x] src/routes/auth/logout/+server.ts — does not exist. No collision.
    • [x] src/lib/server/keycloak.ts (do-not-touch) — verified post-#18 merge. Public surface contains all three names the issue body imports: generateOidcState (line 408), verifyOidcState (line 428), encryptCookiePayload (line 340). Bonus exports the dev will likely need: decryptCookiePayload, verifyKeycloakJwt, refreshTokensIfNeeded, KeycloakTokens interface, and the four typed error classes (JwksUnreachableError, JwtVerificationError, TokenRefreshError, MissingEnvError).
    • [x] src/hooks.server.ts (do-not-touch) — sub-task 2 (#15), still open, correctly excluded.
    • [x] src/routes/(unauthorized)/* (do-not-touch) — sub-task 4 (#17), still open, correctly excluded.

    Repo Placement

    OK. All work lands in forgejo_admin/westside-admin. No cross-repo touch (env vars already landed via pal-e-deployments#147, Keycloak client config matches the issue body via the Playwright snapshot under /home/ldraney/pal-e-platform/.playwright-mcp/page-2026-05-03T14-28-45-062Z.yml — Root URL, Valid redirect URI /auth/callback, post-logout URI /, web origin all confirmed).

    Dependencies

    • Blocked-by: #14 — DONE (PR #18 merged into main at 982df5b, board item #1134 in validation). All three named primitives exist on main.
    • Blocked-by: pal-e-deployments#147 — DONE (closed). 5 env vars landed in cluster Secret. ArgoCD reconciliation noted in router context — verify before declaring "deployed and working" but not a scope-review blocker.
    • Independent of: #15 (hooks.server.ts) — sub-task 2, currently open. The hook redirects TO these endpoints but does not define them. Issue correctly notes orthogonality.
    • Independent of: #17 (403 page) — sub-task 4, currently open. Orthogonal.
    • Blocks: #15 indirectly — hooks.server.ts cannot be exercised end-to-end until /auth/login + /auth/callback exist, but the hook itself can be coded without them. No scope adjustment needed.
    • Five env vars are explicit and match keycloak.ts requireEnv usage (lines 111, 127, 136, 506, 507).

    Acceptance Criteria

    All 7 ACs are agent-verifiable post-implementation:

    • State-cookie attributes (HttpOnly; Secure; SameSite=Lax; Path=/auth/callback; Max-Age=600) — verifiable via curl -i + cookie header inspection.
    • Authorize URL query params (client_id, redirect_uri, response_type, scope, state, code_challenge_method, code_challenge) — verifiable from the 302 Location header.
    • state_mismatch / state_missing 400 responses — directly testable.
    • Token exchange ordering ("must NOT have run" before state validation) — code-review verifiable per the existing checklist item "State validation runs BEFORE token exchange (verified by reading the callback code top-to-bottom)".
    • Generic 502 on Keycloak /token failure (no error-body echo) — code-review + log-grep verifiable.
    • Logout post_logout_redirect_uri + id_token_hint — verifiable from 302 Location header.
    • Logging hygiene (no tokens, ciphertext, or refresh tokens in logs) — already on the checklist as "No tokens in logs (grep verified)".

    Edge case worth a one-line confirmation in the PR: the AC for "valid state" cookie sets Max-Age={refresh_expiry}. Verify the dev pulls this from refresh_expires_in in the token response (Keycloak default ~30 minutes) rather than guessing — this is implementation-level guidance, not a scope gap.

    Blast Radius

    Low. Three new endpoint files in a previously empty src/routes/auth/ tree. No shared modules touched. The transient state cookie path (Path=/auth/callback) confines its surface to the callback handler. Logout is POST-only (correctly), so a stale GET /auth/logout link cannot be CSRF-baited. Funnel-auth concern is satisfied because these endpoints intentionally sit OUTSIDE the admin-role gate (the gate runs in hooks.server.ts AFTER callback sets the session) — issue body explicitly addresses this in the User Story section. A grep confirms no other code in westside-admin consumes these route paths today.

    Decomposition Assessment

    5-minute rule check:

    • 3 file targets across 1 repo — at threshold but within rule (rule is >3 files across >2 repos).
    • 7 ACs — over the >5 trigger, BUT 4 of the 7 ACs are /auth/callback sub-conditions (mismatched state, missing state, happy path, token-endpoint failure) which collapse to a single handler with a state-machine branch. Functionally this is closer to "3 endpoints, 4 behavioral branches in callback" than 7 independent units of work.
    • Estimated agent work — three thin SvelteKit RequestHandler files (~40–60 lines each), all primitives already exported from keycloak.ts, no test scaffolding, no schema work, no migrations. ~3–5 minutes.

    No decomposition needed. Splitting login from callback would create pointless coordination overhead (login generates the state cookie that callback consumes) and would not reduce per-agent work meaningfully. Logout could theoretically split into a fourth ticket, but it's ~15 lines and shares the same env-var + cookie-handling context — splitting buys nothing.

    Recommendation

    APPROVED. Two minor BODY nits worth fixing while in the queue, but neither blocks moving to todo:

    • [BODY] Forgejo title says "/auth/callback + /auth/logout" (2 endpoints) but body adds /auth/login as a third file target. The expanded scope is cohesive (a coherent OIDC protocol surface) and we recommend keeping all 3 in this ticket. Update the Forgejo title to "/auth/login + /auth/callback + /auth/logout endpoints: OIDC code exchange + SLO (consumes #14)" so a future contributor reading the title alone doesn't miss /auth/login. The board item #1136 title (which matches the new spec) is already correct.
    • [BODY] Constraints section mentions PKCE code_verifier storage but never says where the code_verifier is generated. Add one line: "Use Node's crypto.randomBytes + base64url + SHA-256 to derive code_verifier + code_challenge; PKCE generation is not exported from keycloak.ts and the dev creates it inline in +server.ts." This prevents an agent from inventing a new export on the frozen keycloak.ts.
    • [SCOPE] Carry forward from review-1132-2026-05-03: arch-keycloak backing note is missing across the admin-row-crud chain. Ava decision: backfill or treat dataflow note as sufficient. Not a blocker for this ticket; tracked as an umbrella gap on the chain.

    Both BODY nits are issue-body cleanups, not scope changes. Recommend Ava either (a) edit the body inline before promoting to todo, or (b) advance to todo and trust the dev to handle the implicit guidance. Either path is consistent with the convention.

  • Verdict: NEEDS_REFINEMENT

    Board item: #1132 on board-pal-e-platform (mirror of board-westside-admin item #1090).
    Forgejo issue: forgejo_admin/westside-admin#2 (open).
    Type: Feature.
    One-line: Scope is technically sound and the issue body is high quality, but the ticket is over the 5-minute rule by its own admission, two label/arch notes need fixing, and the "prior art" hint is misleading.

    Template Completeness

    • [x] Type
    • [x] Lineage (cites #6 scaffolding + pal-e-platform#301 Keycloak client)
    • [x] Repo
    • [x] User Story (story-westside-admin-admin-row-crud, ties to Keycloak admin gate safety constraint)
    • [x] Context (funnel auth rationale, env vars, prior art note, dataflow reference)
    • [x] File Targets (6 create, 1 update, 1 explicit do-not-modify)
    • [x] Acceptance Criteria (10 testable items)
    • [x] Test Expectations (unit + integration + manual)
    • [x] Constraints (cookie attrs, cookie name, AES-GCM, no-log rule, redirect URI, OIDC state)
    • [x] Decomposition note (self-aware: ticket is at upper edge of 5-min rule, proposes 4-way split fallback)
    • [x] Checklist
    • [x] Related

    Traceability

    • [!] story label MISMATCH — board item carries story:superuser-onboard-service, but project-westside-admin Safety Constraints section explicitly states "Every ticket on the board carries story:admin-row-crud." The issue body correctly references story-westside-admin-admin-row-crud. Mirrored items on board-westside-admin (item #1090) already use story:admin-row-crud. [LABEL] Replace story:superuser-onboard-service with story:admin-row-crud on board item #1132.
    • [x] story note verified — story-westside-admin-admin-row-crud (note 1616) exists; entry present in project-westside-admin user-stories table.
    • [~] arch:westside-admin label — no single "arch-westside-admin" note exists, but the project page architecture section links three concrete arch notes (arch-domain-westside-admin, arch-dataflow-westside-admin, arch-deployment-westside-admin) and the issue body cites arch-dataflow-westside-admin Flow 1 specifically. Treat the label as an umbrella satisfied by those three. Acceptable.
    • [!] arch:keycloak has NO backing notelist_notes(tags="arch", project="westside-admin") returns no arch-keycloak, and global search_notes("arch-keycloak") is empty. Same gap exists on board item 1096 (#301) which already lives in todo. [SCOPE] Create arch-keycloak architecture note covering realm westside-basketball, JWKS endpoint, client registry, and SSO surface across westside-app + westside-admin. Could be deferred to a follow-on if Ava decides keycloak is documented well enough by the dataflow note.
    • [x] Forgejo issue valid and open. Comment count 2, last updated today.

    File Targets

    Repo at /home/ldraney/westside-admin verified. Current source tree: src/app.css, src/app.d.ts, src/app.html, src/lib/server/ (empty), src/routes/+layout.svelte, src/routes/+page.svelte, src/routes/health/+server.ts. Adapter is @sveltejs/adapter-node (confirmed in svelte.config.js) — correct for SSR cookie auth.

    • [x] src/hooks.server.ts — does not exist (correct, to create).
    • [x] src/routes/auth/callback/+server.ts — route does not exist (correct, to create).
    • [x] src/routes/auth/logout/+server.ts — route does not exist (correct, to create).
    • [x] src/routes/(unauthorized)/+page.svelte — route group does not exist (correct, to create). SvelteKit route-group naming is valid.
    • [x] src/lib/server/keycloak.tssrc/lib/server/ directory exists and is empty; correct location for server-only code (SvelteKit enforces no client import from $lib/server).
    • [x] src/app.d.ts — exists with empty App namespace stubs; App.Locals is commented out, ready to extend.
    • [x] package.json — exists. jose not currently a dependency (verified via head -50 of package.json); cookie also not present. Both need adding. Acceptable.
    • [ ] "Prior art" hint about westside-contracts SSR auth helpers — misleading. grep -rn -i "keycloak\|jose\|JWKS" /home/ldraney/westside-contracts/src returns zero hits. westside-contracts uses signed-token URLs (no Keycloak). westside-app uses browser-side keycloak-js (adapter-static). Neither is SSR cookie auth prior art. [BODY] Replace the "Prior art" sentence with: "No SSR cookie-auth prior art exists in this codebase — westside-contracts uses signed tokens, westside-app uses browser-side keycloak-js. Implement from scratch using jose per Constraints below." Minor; not a blocker if dev agent reads context carefully.

    Repo Placement

    OK — forgejo_admin/westside-admin. The work is purely SvelteKit code in this repo. Configuration changes (Keycloak client, k8s secret) are correctly separated into pal-e-platform#301 (closed-equivalent — client created in admin console today per prompt) and pal-e-deployments#147 (open — client_secret + 4 other env vars).

    Dependencies

    Three external dependencies, ordering matters:

    1. westside-admin#6 (scaffolding) — CLOSED. Source tree confirms scaffold is in place.
    2. pal-e-platform#301 (Keycloak westside-admin OIDC client creation + SOP) — per prompt context, the client was created today (2026-05-03) per sop-keycloak-client-creation. Client is confidential (validated by the existence of a KEYCLOAK_CLIENT_SECRET in the secret). Effectively done.
    3. pal-e-deployments#147 (land real KEYCLOAK_CLIENT_SECRET; full env-var set) — OPEN, QA-approved, awaiting merge. The five env vars (KEYCLOAK_URL, KEYCLOAK_REALM, KEYCLOAK_CLIENT_ID, KEYCLOAK_CLIENT_SECRET, COOKIE_SIGNING_KEY) are confirmed in overlays/westside-admin/prod/westside-admin-secrets.enc.yaml on the keycloak-westside-admin-secret branch. Must merge + ArgoCD sync before manual / SSO acceptance criteria can be validated. Code can be written and unit-tested without it; integration / manual flow cannot.

    Sibling tickets in board-westside-admin todo (Drizzle setup #1, scoped-db helper #3, players list #4, players row edit #5) all carry story:admin-row-crud and depend on this hooks.server.ts ticket landing first because all data routes need event.locals.user populated. This ticket is correctly first-in-line; recommend pulling it directly into next_up after refinement.

    Acceptance Criteria

    Strong. 10 criteria, every one verifiable by an agent:

    • Anonymous redirect path verifiable via Playwright follow-redirects=false.
    • state CSRF check verifiable via crafted callback request with mismatched state.
    • Cookie attribute set verifiable via curl -i + grep for HttpOnly; Secure; SameSite=Lax.
    • Role gate verifiable via two test users (one with admin, one without).
    • JWKS cache TTL verifiable via Keycloak rotation + cache-hit timing.
    • JWT validation legs (audience, exp, iss) verifiable via unit tests with crafted tokens (jose offers fixtures).
    • Token refresh verifiable via fast-forwarding system clock or short access-token lifetime.
    • No-log rule verifiable via grep over server logs after running auth flow.
    • SSO verification is the one manual-only AC; acceptable.

    One implicit gap: no AC for the case where Keycloak is unreachable (JWKS fetch fails). Suggest adding: "If JWKS fetch fails on a cold cache, request returns 503; existing cached JWKS continues serving until TTL expires." [BODY] Add resilience AC. Optional — would tighten the spec.

    Blast Radius

    Self-contained: code lives entirely in westside-admin. Downstream board items (Drizzle, scoped-db, players list, players row edit) all consume event.locals.user — that contract must remain stable. The App.Locals.user shape ({ sub, email, name, tenantId }) becomes a load-bearing interface for every subsequent feature; freeze it carefully. tenantId hardcoded to 1 matches Safety Constraint #2 ("v1: hardcoded TENANT_ID = 1").

    No similar bug to fix elsewhere — no other adapter-node + cookie SSR + Keycloak service exists in this org yet (westside-app is adapter-static, westside-contracts uses signed tokens). This ticket establishes the pattern; future cookie-SSR services will copy keycloak.ts.

    Decomposition Assessment

    NEEDS DECOMPOSITION (advisory).

    • File count: 6 new files + 1 updated file + new dependencies. Over the 3-file threshold.
    • AC count: 10. Over the 5-AC threshold.
    • Estimated time: issue body itself says "30-60 min agent run." Well over the 5-min rule.
    • Cryptographic correctness on critical path: CSRF state generation+validation, AES-GCM encrypt/decrypt, JWT signature+audience+expiry+issuer verification, token refresh window. Mistakes here bypass funnel auth and replay the 2026-04-10 PII leak.

    The issue body proposes a 4-way split as fallback:

    1. keycloak.ts libsrc/lib/server/keycloak.ts + unit tests. Pure functions: JWKS fetch+cache, JWT verify, AES-GCM encrypt/decrypt, state generate+validate. No SvelteKit coupling. Largest crypto surface; deserves isolated test pass.
    2. hooks.server.ts — consumes keycloak.ts; reads cookie, validates JWT, populates event.locals.user, redirects anonymous to Keycloak with state. Also extends src/app.d.ts with App.Locals.user.
    3. auth/callback + auth/logout endpoints+server.ts handlers for OIDC code exchange, cookie set, logout + Keycloak SLO redirect. Depends on (1) and (2).
    4. (unauthorized)/+page.svelte — small 403 page with logout button. Trivial; could fold into (3).

    Tradeoff: the modules are tightly coupled (keycloak.ts is consumed by both hooks.server.ts and the callback endpoint), so a 4-way split costs ~3 PR coordination cycles vs ~1 oversized PR. The crypto correctness argument tips it toward decomposition: the keycloak.ts unit-test surface alone is large enough that bundling it with the integration code makes review hard.

    Recommendation: Ava's call. If Ava trusts a single-shot dev agent on this, accept the issue body's self-flag and proceed. If Ava wants the safety net, route to skill-decompose-ticket with the 4-way split.

    Recommendation

    • [LABEL] Fix on board item #1132: replace story:superuser-onboard-service with story:admin-row-crud to match project Safety Constraints contract.
    • [SCOPE] Decide whether to create arch-keycloak note now or defer (label currently lacks a backing note; same gap exists on item #1096 / pal-e-platform#301).
    • [BODY] Replace the misleading "Prior art: westside-contracts may have helpers" sentence; westside-contracts has no Keycloak code. Optional but would save the dev agent a wasted grep.
    • [BODY] Optionally add a JWKS-unreachable resilience AC (cold-cache failure → 503; warm-cache continues serving until TTL).
    • [DECOMPOSE] 6 files + 10 AC + crypto + OIDC state on funnel-auth critical path. Over 5-min rule. Issue body's proposed 4-way split (keycloak.ts / hooks.server.ts+app.d.ts / auth endpoints / 403 page) is sound. Route to skill-decompose-ticket if Ava wants the safety net; otherwise accept oversized with the dev agent on standby for escalation.
    • Sequencing note for whoever dispatches: do not move to in_progress until pal-e-deployments#147 is merged and ArgoCD has synced the secret — otherwise the manual / SSO ACs cannot be validated. Code+unit can begin earlier, but a dev agent that hits the "deploy and SSO-verify" AC will block.
  • Verdict: APPROVED

    Note: Both READY and APPROVED are accepted as passing verdicts by the check-board-advance hook.

    This is the FIRST review under refined Option A scope (post-architectural reframing 2026-05-02). The original "rotate the placeholder" framing was scoped out; the refined scope is the correct unit of work. Minor [BODY] polish recommended before tofu apply runs, but nothing blocks the PR from being cut.

    Template Completeness

    • [x] ### Type — Bug
    • [x] ### Lineage — links validation note + parent #144 + layered debugging history
    • [x] ### Repo — pal-e-deployments (overlay) + coordinated pal-e-services (terraform)
    • [x] ### What Broke — placeholder string, base64 error, dual-IaC root cause explained
    • [x] ### Repro Steps — three concrete commands (kubectl pod state, secret decode, Harbor API)
    • [x] ### Expected Behavior — TWO coordinated changes with explicit sequence
    • [x] ### Environment — service, cluster, Harbor project id, target robot name, refs
    • [x] ### Acceptance Criteria — 8 items, mix of file-presence + Harbor API + cluster state + funnel
    • [x] ### Constraint — secrets path, -target= scoping, -lock=false convention reference
    • [x] ### Related — parent #144, sibling layers (#137, westside-admin#13), validation note, sop-harbor-robot-import
    • [x] ### Scope — narrow (westside-admin only), explicit out-of-scope

    Traceability

    • [x] story:admin-row-crud label — verified entry in project-westside-admin user-stories table (story note story-westside-admin-admin-row-crud)
    • [x] arch:harbor label — verified row "Harbor registry" in arch-deployment-westside-admin Components table (column "arch: label" = arch:harbor)
    • [x] type:bug label — matches refined Type (Bug) in body
    • [x] blocks:deploy-chain label — accurate; this is the LAST blocker on westside-admin's M1 bootstrap arc
    • [x] Forgejo issue — forgejo_admin/pal-e-deployments#143, state: open
    • [x] Architectural parent forgejo_admin/pal-e-deployments#144 — verified open, body explicitly names westside-admin as "first concrete consumer" with #143 as the migration ticket

    File Targets

    All paths verified against origin/main of pal-e-deployments and the live ~/pal-e-services/terraform/ tree:

    • [x] overlays/westside-admin/prod/kustomization.yaml — line 8 of resources: list contains - harbor-creds.enc.yaml. Confirmed.
    • [x] overlays/westside-admin/prod/harbor-creds.enc.yaml — file exists, SOPS-encrypted with age recipient age15ct78fr4scv4vxzj3k6q76wshywzlu0mdc64a624e264dst7zfaq6tjzjr, lastmodified 2026-04-25. Confirmed.
    • [x] ~/pal-e-services/terraform/services.tfresource "kubernetes_secret_v1" "harbor_creds" at lines 94-115 with for_each = var.services. Resource composes auths from harbor_robot_account.service_pull[each.key].secret. Confirmed.
    • [x] ~/pal-e-services/terraform/services.tfresource "harbor_robot_account" "service_pull" at lines 57-77, name "${each.key}-pull", project-scoped. Resolves to robot robot$westside-admin+westside-admin-pull. Matches issue body.
    • [x] ~/pal-e-services/terraform/k3s.tfvarsservices map at lines 199-308 includes westside-admin at lines 288-295 (forgejo_repo, image_repo=westside-admin/app, port 3000, funnel=false, source_path=overlays/westside-admin/prod). Confirmed. funnel=false is correct because the overlay manages its own ingress (ingress.yaml present).
    • [x] ~/secrets/pal-e-services/secrets.envHARBOR_ADMIN_PASSWORD present (also duplicated in k3s.tfvars line 2 — terraform reads from tfvars).

    Repo Placement

    OK. The overlay PR is correctly filed against forgejo_admin/pal-e-deployments. The terraform apply step is correctly framed as a separate operator action against forgejo_admin/pal-e-services (no PR needed for the apply itself — the .tf code is already on main and unchanged; this is a state-only operation). No multi-repo coordination required beyond the documented sequence.

    Dependencies

    • Architectural parent: forgejo_admin/pal-e-deployments#144 — open, lists westside-admin as first migration. This ticket is documented as the proof-of-pattern; lessons feed back to #144.
    • Layered prerequisites already done: layer-1 (westside-admin#13, image tag 40-zeros fix) and layer-2 (#137, PSA admission) are done/qa. This is layer-3, the last visible blocker.
    • No in-progress blockers: board scan shows nothing in in_progress; this ticket is the only item in backlog on board-westside-admin and is the gating M1 ticket.
    • Downstream: 13 more overlays carry harbor-creds.enc.yaml (basketball-api, gcal-scheduler, mcd-tracker, mcd-tracker-app, pal-e-app, pal-e-docs, pal-e-mail, pal-e-production, platform-validation, twitch-2k-wager, westside-ai-assistant, westside-streamlit, westsidekingsandqueens). Out-of-scope per Option A; tracked under #144.
    • No CI dependency: ~/pal-e-services/ has no .woodpecker.yaml or other CI config. The tofu apply is unambiguously a manual operator step. The issue body's "Terraform state access (likely remote backend; verify before apply)" is slightly imprecise — current state is LOCAL (per pal-e-platform/CLAUDE.md: "Local terraform state for now. Remote backend is a post-bootstrap concern."). Worth tightening (see [BODY] recs below).

    Acceptance Criteria

    8 AC items, all testable by an agent with kubectl + curl + Harbor admin creds + ArgoCD CLI. Strong coverage on outcomes (Harbor API for robot existence, dockerconfigjson parses, deployment 1/1 ready, funnel responds 200). Two minor gaps (called out below as [BODY] polish, not blockers):

    • No explicit AC for "no-drift verification" — i.e., tofu plan -target='harbor_robot_account.service_pull["westside-admin"]' -target='kubernetes_secret_v1.harbor_creds["westside-admin"]' -lock=false returns "0 to add, 0 to change, 0 to destroy" AFTER the apply. User flagged this as item #4. Add as AC #9.
    • No explicit AC for "ArgoCD shows no OutOfSync on westside-admin Application" — verifies that the overlay change + terraform-managed Secret reach a stable steady state. Add as AC #10.

    Blast Radius

    • Cluster: westside-admin pod is already in CreateContainerError. Removing the placeholder Secret cannot make it worse. Brief gap (overlay prune → tofu apply) is non-regressive. Acknowledged in body.
    • Other services: -target= on two specific keys means kubernetes_secret_v1.harbor_creds["westside-admin"] and harbor_robot_account.service_pull["westside-admin"] only. terraform's -target follows transitive dependencies, so it will ALSO read/touch harbor_project.service["westside-admin"] and kubernetes_namespace_v1.service["westside-admin"] (referenced via interpolations on lines 75 and 99). Both already exist and should plan as no-op. Other services' state is not touched. Acceptable — but worth a [BODY] note that operator should review the plan diff before applying.
    • Harbor: ZERO project-scoped robots currently exist (verified live: GET /api/v2.0/robots?page_size=50 returned only robot$image-updater). This will be the FIRST harbor_robot_account.service_pull to apply in the current Harbor instance. Pattern is unproven in production — that's the intentional value of doing westside-admin first under the Option A migration.
    • Robot count claim: issue body says "other 11 services migrate" but actual overlay count carrying harbor-creds.enc.yaml is 14 total (13 remaining after westside-admin). Cosmetic — does not affect this ticket's execution.

    Decomposition Assessment

    Within the 5-minute / <3 file targets / <5 AC heuristics:

    • File targets: 2 (kustomization.yaml edit, harbor-creds.enc.yaml deletion) — under the 3-file ceiling.
    • AC items: 8 (the heuristic ceiling is 5, but 6 of these are post-merge cluster verifications that the validate skill will run automatically, not implementation work).
    • Implementation work for the dev agent is exactly 2 file changes in one repo. Estimated 2-3 minutes including PR scaffold.
    • Operator step (tofu apply) is a separate action, properly sequenced post-merge.

    No decomposition needed.

    Recommendation

    APPROVED. Move 1120 to todo. The four [BODY] polish items below are nits — not blockers — and can be appended as a comment on #143 or absorbed during dev/validate without forcing a re-review:

    • [BODY] Add AC #9: "tofu plan -target='harbor_robot_account.service_pull[\"westside-admin\"]' -target='kubernetes_secret_v1.harbor_creds[\"westside-admin\"]' -lock=false after the apply returns 0 changes (no drift)."
    • [BODY] Add AC #10: "ArgoCD shows Application westside-admin as Synced with no OutOfSync resources after both changes settle."
    • [BODY] Constraint section: replace "Terraform state access (likely remote backend; verify before apply)" with "Terraform state is LOCAL (per pal-e-platform/CLAUDE.md); operator runs tofu apply from ~/pal-e-services/terraform/ with the local state file."
    • [BODY] Add to Constraint: "Operator must review the tofu plan diff before applying — -target= follows transitive dependencies, so harbor_project.service[\"westside-admin\"] and kubernetes_namespace_v1.service[\"westside-admin\"] will also appear in the plan (both already exist; expect no-op). Abort if anything else changes."

    No [LABEL], [SCOPE], or [DECOMPOSE] recommendations.

    Architectural Concerns Evaluated (per request)

    • SOPS-prune-before-tofu-apply gap: NOT a regression. Pod is already broken with CreateContainerError; removing the placeholder Secret does not change that. Acknowledged in body. No action needed.
    • Unexpected terraform state touches: -target follows transitive deps. The two named resources pull in harbor_project.service["westside-admin"] and kubernetes_namespace_v1.service["westside-admin"]. Both exist and should plan as no-op. Other for_each keys are NOT re-evaluated by -target (that's the whole point of the flag). Acceptable; covered by [BODY] rec to review plan before apply.
    • Per-overlay PR vs. separate tofu apply ticket: I considered whether the operator-run tofu apply needs its own Forgejo issue. Recommendation: NO — keep it as part of #143's expected behavior. Reasons: (a) the apply has no code artifact, only a state transition; (b) splitting it would introduce a coordination handoff that doubles the chance the sequence is mis-ordered; (c) the validate-ticket skill can verify both changes in one pass. If we want a paper trail of WHO ran the apply and when, that belongs in a comment on #143 or a one-line entry in validation-143-{date}, not a separate ticket.

    Live Evidence Captured

    • git ls-tree origin/main overlays/westside-admin/prod/: kustomization.yaml, harbor-creds.enc.yaml, deployment-patch.yaml, ingress.yaml, namespace.yaml, westside-admin-secrets.enc.yaml all present.
    • GET https://harbor.tail5b443a.ts.net/api/v2.0/robots?page_size=50: returns 1 robot (robot$image-updater). ZERO project-scoped robots exist. ?q=name%3D~westside-admin: 0 results. Confirms terraform's harbor_robot_account.service_pull["westside-admin"] has never successfully applied.
    • cat ~/secrets/pal-e-services/secrets.env | grep HARBOR_ADMIN_PASSWORD: present.
    • find ~/pal-e-services -name ".woodpecker*": nothing. tofu apply is manual.
    • Parent #144 body: explicitly names #143 as "first concrete consumer" and "first overlay to migrate."
  • Verdict: APPROVED

    Single-line bug fix with verified file targets, complete traceability, and intact backing notes. Ready to advance backlog -> todo.

    Template Completeness

    Issue body matches template-issue-bug:

    • [x] Type -- Bug
    • [x] Lineage -- explains pipeline #1 manual trigger as discovery vector; webhook installed ~5 min after #10/#11 merged so latent until manual run
    • [x] Repo -- forgejo_admin/westside-admin
    • [x] What Broke -- exact step (validate), exact assertion (test -f build/index.html), root cause (adapter mismatch)
    • [x] Repro Steps -- 3 numbered steps reproducible against main
    • [x] Expected Behavior -- canonical fix specified (test -f build/index.js)
    • [x] Environment -- repo + SHA + pipeline ID + adapter + build image
    • [x] Acceptance Criteria -- 3 testable checkboxes
    • [x] Related -- project, originating PR (#11), and explicit unrelated cross-link to pal-e-platform #321

    Traceability

    • [x] story:admin-row-crud label -- admin row CRUD story
    • [x] story note verified -- story-westside-admin-admin-row-crud listed in project-westside-admin user-stories section (sole story for v1)
    • [x] arch:ci-pipeline label -- Woodpecker CI per convention-architecture-ids Deployment Components table
    • [x] arch note verified -- arch-ci-pipeline exists in pal-e-docs (project: pal-e-platform), describes the shared Woodpecker -> Harbor -> pal-e-deployments -> ArgoCD loop
    • [x] type:bug label -- correct (regression-style: validate step fails on the artifact actually produced; smoke check encoded the wrong adapter assumption)
    • [x] Forgejo issue -- https://forgejo.tail5b443a.ts.net/forgejo_admin/westside-admin/issues/12, open
    • Adjacent CI item #1094 (issue #8) used the same story/arch axes -- conventions already proven for this kind of work.

    File Targets

    • [x] ~/westside-admin/.woodpecker.yaml -- verified at SHA 82ac5255a9. Line 33 reads exactly - test -f build/index.html inside the validate step (lines 27-35). Bug confirmed in the live tree.
    • [x] ~/westside-admin/svelte.config.js -- verified @sveltejs/adapter-node with out: 'build' (line 1, 8-10). Confirms build/index.html is never produced; build/index.js is the canonical Node entry.
    • [x] ~/westside-admin/package.json -- verified "start": "node build/index.js" (line 10). Production runtime already targets build/index.js; smoke check should mirror.

    Repo Placement

    OK. Single-file, single-repo change in forgejo_admin/westside-admin. No cross-repo coordination needed -- no sibling overlay/registry work, no upstream update-kustomize-tag.sh change required (the script reference in .woodpecker.yaml is unaffected).

    Dependencies

    None active. The originating commits (#10 Dockerfile, #11 .woodpecker.yaml) are merged. Sibling CI item #1094 is in qa already. No in_progress blockers; no items downstream gated on this fix in the listed board (the next-up coding stories #1, #2, #4 don't depend on green CI to be reviewed, but they will need this fix to actually deploy). Worth fast-tracking so subsequent feature merges have a working pipeline.

    Acceptance Criteria

    All three AC mechanically verifiable:

    • AC1 (assertion change) -- grep .woodpecker.yaml for build/index.js after merge
    • AC2 (pipeline reaches build-and-push) -- visible in Woodpecker UI on next push to main
    • AC3 (PR runs still gate Kaniko) -- the validate step still runs on pull_request per the when block (line 34-35), and Kaniko's when block already excludes pull_request via event: [push, manual] (line 53). No structural change required to preserve gating.

    Optional: dev agent may add build/handler.js assertion as defense-in-depth (issue body explicitly allows this).

    Blast Radius

    Searched all sibling SvelteKit + API repos for the same smoke-check pattern:

    • westside-app: uses adapter-static -- test -f build/index.html is CORRECT. No change needed.
    • westside-contracts: uses adapter-node, no smoke check at all (separate scoping concern, not a regression).
    • pal-e-app, mcd-tracker-app: adapter-static, no smoke check in .woodpecker.yaml.
    • Non-SvelteKit repos (basketball-api, pal-e-docs, mcd-tracker-api): N/A.

    Conclusion: westside-admin is the only repo with this bug. No fan-out fix required. No downstream consumers of build/index.html to break (it never existed for this repo).

    Decomposition Assessment

    1 file, 1 line change, 3 AC. Estimated agent work: under 1 minute (edit + commit + PR). No decomposition needed.

    Recommendation

    No action needed. Ready to advance backlog -> todo.

    • review-1094-2026-04-25-v2 -- adjacent CI ticket APPROVED with the same arch:ci-pipeline label and story:admin-row-crud axes
    • arch-ci-pipeline -- backing architecture note for this work
    • story-westside-admin-admin-row-crud -- sole user story for westside-admin v1
    • convention-architecture-ids -- confirms arch:ci-pipeline as canonical for Woodpecker CI
    • feedback_yaml_parse_validation -- dev agent should yaml.safe_load the file before commit; QA must verify
  • Verdict: READY

    All v1 refinements have been applied and verified. Ticket is ready to advance backlog → todo per feedback_todo_means_reviewed.

    v1 Refinements Verified

    • [x] [LABEL] Label changed arch:k8s-deployarch:harbor — confirmed via list_board_items: item #1098 labels = story:admin-row-crud,arch:harbor,type:infra. Matches sibling #1088 convention.
    • [x] [BODY] Port 3000 explicit — confirmed in Scope ("port: 3000") and AC #2 ("Entry specifies port 3000 and image_repo westside-admin/app").
    • [x] [BODY] Woodpecker secret model documentation required — confirmed in Scope ("Document in the PR description whether the new service uses the existing global secrets..."), AC #3 ("PR description documents which Woodpecker secret model is used"), and Checklist ("PR description documents Woodpecker secret model choice").
    • [x] [SCOPE] arch-deployment-westside-admin doc drift fixed — verified directly:
      • Components table now has explicit arch: label column.
      • Harbor registry row: arch:harbor label, project name = westside-admin, image path = harbor.tail5b443a.ts.net/westside-admin/app:<sha>.
      • Deployment row image path also corrected to harbor.tail5b443a.ts.net/westside-admin/app:<sha>.
      • Key Decisions "Image registry follows..." bullet now reads correctly: project = westside-admin, NOT forgejo_admin.

    Template Completeness

    • [x] Type — Task
    • [x] Lineage — cites #1094 scope review + reviewer pre-investigation of services.tf:7
    • [x] Repo — forgejo_admin/pal-e-services
    • [x] User Story — story-westside-admin-admin-row-crud
    • [x] Context — existing pattern + reference entries cited
    • [x] Scope — clear, port 3000 + Woodpecker secret model both addressed
    • [x] Acceptance Criteria — 8 criteria, all testable
    • [x] Test Expectations — tofu plan + Harbor UI + first kaniko push
    • [x] Constraints — tofu, -lock=false, naming convention
    • [x] Checklist — present, includes secret model documentation gate
    • [x] Related — project, arch, conventions, blocks linkage

    Traceability

    • [x] story:admin-row-crud label — admin row CRUD story
    • [x] story note verified — story-westside-admin-admin-row-crud exists; listed in project-westside-admin user-stories table (single v1 story)
    • [x] arch:harbor label — Harbor registry component, backed by the "Harbor registry" row in arch-deployment-westside-admin Components table (per fractal arch convention used by sibling #1088)
    • [~] No standalone arch-harbor note exists in pal-e-docs — OBSERVATION not blocker. The Harbor component is documented as a row inside the deployment-level arch note, which is the convention this board uses (sibling #1088 also labels arch:harbor with the same backing). If platform-wide standalone arch-harbor note is desired, file as a separate doc-cleanup ticket. Does NOT block #1098.
    • [x] Forgejo issue — https://forgejo.tail5b443a.ts.net/forgejo_admin/pal-e-services/issues/64, open

    File Targets

    Task type — file targets verified during v1 review and unchanged:

    • [x] ~/pal-e-services/terraform/k3s.tfvars — reference entry westsidekingsandqueens (lines 224-231) confirmed; schema (forgejo_repo, image_repo, port, funnel, source_repo, source_path) confirmed.
    • [x] ~/pal-e-services/terraform/services.tf — for_each over var.services, key drives Harbor project name + robot accounts (line 7 comment, line 13 name = each.key).

    Repo Placement

    OK — single-repo change in forgejo_admin/pal-e-services.

    Dependencies

    • Blocks: #1094 (westside-admin .woodpecker.yaml), #1095 (pal-e-deployments overlay).
    • No upstream blockers. Independent of #1087 (scaffolding), #1088 (Dockerfile).
    • Only #1093 in todo; rest of board is todo or backlog. No conflicts.

    Acceptance Criteria

    All 8 ACs are testable. AC #3 (Woodpecker secret model documented in PR) and AC #2 (port 3000 + image_repo) directly address the v1 [BODY] flags. AC #8 verifies end-to-end via kaniko push test. The previous ambiguity around "(or document how the new project authenticates)" is resolved — the PR description must explicitly state which model (global vs per-repo).

    Blast Radius

    Harbor naming correct (verified v1). Doc drift in arch-deployment-westside-admin resolved — downstream tickets (#1094 .woodpecker.yaml, #1095 overlay) will now read consistent harbor.tail5b443a.ts.net/westside-admin/app:<sha> from the arch note. No risk of divergence at runtime.

    Decomposition Assessment

    1 file edit (k3s.tfvars), 8 AC (mostly post-apply verifications, not separate work units), ~5-10 min agent work. No decomposition needed. Single-pass agent task.

    Recommendation

    READY — advance #1098 backlog → todo. No action needed on this ticket.

    • Optional follow-up (not blocking): Consider filing a low-priority doc-cleanup ticket to create a standalone arch-harbor note for platform-wide reuse. Currently the Harbor component lives only as rows in per-deployment arch notes. This is a convention question for Ava, not a #1098 blocker.
  • Verdict: APPROVED

    Re-review v3 of review-1091-2026-04-25-v2. v2 verdict was NEEDS_REFINEMENT with one outstanding [SCOPE] recommendation: missing backing arch note for arch:scoped-db. That recommendation is now resolved.

    Changes Since v2 Review

    • [x] [SCOPE] RESOLVED — arch-scoped-db-westside-admin note created (note id 1639, note_type architecture, project westside-admin, tagged architecture,active). Verified directly via get_note(slug="arch-scoped-db-westside-admin"). The note describes scopedDb as a sub-component nested inside arch-dataflow-westside-admin, which matches v2's recommended option (a).
    • [x] Components table maps arch:scoped-db to two real components: scopedDb (tenant-scoped query builder, lives in src/lib/server/scopedDb.ts) and systemDb (unscoped builder for system tables). Plus internal db and tenant.ts rows. The label is no longer phantom.
    • [x] Implementation Tickets section in the arch note explicitly lists forgejo_admin/westside-admin#1 (board #1089) and forgejo_admin/westside-admin#3 (board #1091) — both helper-layer tickets share this arch note as agreed in v2.
    • [x] Issue body unchanged from v2 (caller confirmed). Related section points to arch-dataflow-westside-admin; that's still correct since the new arch-scoped-db-westside-admin nests under it. Optional polish: the issue could also reference the new sub-component note, but this is cosmetic — both arch notes are reachable from the project page and from each other.

    Template Completeness

    • [x] Type — Feature
    • [x] Lineage — explicit issue numbers (#1, #6)
    • [x] Repo — forgejo_admin/westside-admin
    • [x] User Story — story-westside-admin-admin-row-crud
    • [x] Context — clear motivation, swap-point design, lint enforcement rationale
    • [x] File Targets — Create + Modify + Do NOT modify lists present
    • [x] Acceptance Criteria — 6 ACs, behavioral + type-system + lint + comment
    • [x] Test Expectations — unit + integration described
    • [x] Constraints — no JWT-derived tenant_id (v1), Drizzle typed builders only
    • [x] Checklist — PR / ACs / lint verification
    • [x] Related — project page + arch note + DEPENDS ON line

    Traceability

    • [x] story:admin-row-crud label — present
    • [x] story note verified — story-westside-admin-admin-row-crud in project-westside-admin user-stories section (verified in v2, unchanged)
    • [x] arch:scoped-db label — present
    • [x] arch note verified — arch-scoped-db-westside-admin exists in pal-e-docs (note_type architecture, components table maps the label to scopedDb/systemDb)
    • [x] type:feature label — present
    • [x] Forgejo issue — https://forgejo.tail5b443a.ts.net/forgejo_admin/westside-admin/issues/3 — open

    File Targets

    Repo state unchanged from v2 — forgejo_admin/westside-admin still contains only README.md on main. All file targets sit downstream of #6 (scaffolding) and #1 (Drizzle setup). Paths are well-formed and match the helper-layer design. Dev agent must re-verify after #6 + #1 land.

    • [~] src/lib/server/tenant.ts (Create) — plausible after #1
    • [~] src/lib/server/scopedDb.ts (Create) — well-formed
    • [~] src/lib/server/db.ts (Modify) — created by #1
    • [~] package.json (Modify) — exists after #6

    Repo Placement

    OK. All file targets in westside-admin. No multi-repo fan-out.

    Dependencies

    • Hard blocker: #1 (board #1089, backlog) — creates db.ts and schema.ts.
    • Hard blocker: #6 (board #1087, backlog) — creates SvelteKit shell.
    • Downstream: #1090 (hooks-server), #1092 (page-server tables), #1093 (page-server rows, currently in todo) all consume scopedDb. Board ordering note for Ava: #1093 in todo while its dependency #1091 sits in backlog — not this ticket's blocker but worth flagging.
    • Documented? Yes — Lineage pins both upstream issues by number.

    Acceptance Criteria

    All 6 ACs agent-verifiable (unchanged from v2):

    • AC1–AC2: SQL output assertions via snapshot test
    • AC3: TypeScript type error on system-table misuse via expectTypeOf or tsc --noEmit
    • AC4: CI lint check fails on direct db import — verifiable on fixture
    • AC5: 6+ unit tests — quantitative
    • AC6: swap-procedure comment in tenant.ts — verifiable via grep

    Integration test (URL/form-body tenant_id mutation still produces correct WHERE clause) remains the strongest data-leak guarantee.

    Blast Radius

    • First Drizzle consumer in the cluster — pattern set here informs future adoption
    • Downstream rebases on #1090, #1092, #1093 if API shape is wrong; agent should sanity-check Drizzle's actual builder API before locking signatures
    • Grep-based lint is brittle to renames/re-exports; acceptable for v1
    • TENANT_ID = 1 in TS file works for v1; swap-point comment is the discipline

    Decomposition Assessment

    4 file targets (2 create + 2 modify), 6 ACs, ~6 unit tests + 1 integration test. Estimated agent work 30–60 min one-pass. No decomposition needed. Within 5-minute-rule envelope.

    Recommendation

    No action needed. APPROVED — ready to advance backlog → todo.

    Optional cosmetic polish (not gating): the issue's Related section could also list arch-scoped-db-westside-admin alongside arch-dataflow-westside-admin, but the parent-arch link is sufficient since the new note nests under it.

  • Verdict: READY

    Third review of board item #1089 (forgejo_admin/westside-admin#1) following review-1089-2026-04-25-v2 (NEEDS_REFINEMENT). The sole remaining v2 blocker — missing backing note for the arch:scoped-db label — has been resolved. Issue body is unchanged from v2 (already approved on body grounds). Ticket is dispatch-ready.

    v2 Recommendation Resolution

    • [x] [SCOPE] Resolve arch:scoped-db backing note — RESOLVED via Path A (preferred path). New architecture note arch-scoped-db-westside-admin created (note_type=architecture, project=westside-admin, tags=architecture,active). Note includes:
      • Mermaid diagram of the scopedDb/systemDb seam between page-server code and the unscoped Drizzle db.
      • Components table with explicit arch: label column — scopedDb and systemDb both map to arch:scoped-db; db (internal), tenant.ts, and schema.ts documented as supporting components.
      • Five Key Decisions (single entry point + lint enforcement, type-system distinction between tenant/system tables, hardcoded TENANT_ID swap point, composable where-clause injection, sub-component rationale).
      • Implementation Tickets section explicitly cross-references board #1089 (issue #1) and #1091 (issue #3).

    Template Completeness

    • [x] Type, Lineage, Repo, User Story, Context, File Targets, Acceptance Criteria, Test Expectations, Constraints, Checklist, Related — all present (unchanged from v2).

    Traceability

    • [x] story:admin-row-crud label — verified in project-westside-admin user-stories table.
    • [x] arch:scoped-db label — verified.
    • [x] arch note verified — arch-scoped-db-westside-admin exists in pal-e-docs with a components table that explicitly maps arch:scoped-db to two real components (scopedDb, systemDb). Note also lists this ticket (#1089/issue #1) in Implementation Tickets.
    • [x] Forgejo issue — https://forgejo.tail5b443a.ts.net/forgejo_admin/westside-admin/issues/1, open.

    File Targets

    Greenfield repo (existence checks N/A; all targets are net-new). All targets carried forward from v2:

    • [x] drizzle.config.ts
    • [x] src/lib/server/db.ts
    • [x] src/lib/server/schema.ts
    • [x] package.json
    • [x] scripts/check-schema-drift.sh

    Repo Placement

    OK. Single-repo ticket on forgejo_admin/westside-admin.

    Dependencies

    • Hard blocker: forgejo_admin/westside-admin#6 (scaffolding) — explicit in Lineage.
    • Hard blocker for AC verification: forgejo_admin/pal-e-platform#302 (admin_app Postgres user) — explicit in Lineage.
    • Decoupled: forgejo_admin/westside-admin#8 (Woodpecker CI wiring) — explicitly out of scope per "Do NOT create".
    • Downstream consumers: #2, #3, #4, #5 import from src/lib/server.

    Acceptance Criteria

    6 ACs, all verifiable. Drift script is locally runnable with explicit exit-code semantics. No CI coupling.

    Blast Radius

    Greenfield repo. Driver choice (pg) matches westside-app convention. jsonb narrowing flagged with documented defer option.

    Decomposition Assessment

    5 file targets, 6 ACs, single repo, ~5–10 min agent work. No decomposition needed.

    Recommendation

    No action needed. All v1 [BODY] fixes (lineage explicitness, CI drift narrowing) and the v2 [SCOPE] backing-note gap are resolved. The arch note arch-scoped-db-westside-admin provides the documented anchor that arch:scoped-db labels on this ticket and #1091 now point to.

    Advance #1089 backlog → todo.

    • review-1089-2026-04-25 — v1 review (NEEDS_REFINEMENT, [BODY] items)
    • review-1089-2026-04-25-v2 — v2 review (NEEDS_REFINEMENT, [SCOPE] backing note)
    • arch-scoped-db-westside-admin — new arch note that resolves the v2 blocker
    • project-westside-admin — project page
    • story-westside-admin-admin-row-crud — driving user story
  • Verdict: NEEDS_REFINEMENT

    Scope is fundamentally correct: it describes adding a westside-admin entry to terraform/k3s.tfvars, mirroring existing service entries, to provision a Harbor project + CI robot account. Critically, the ticket gets the Harbor naming convention right (correctly invoking feedback_harbor_project_naming as: service key = Harbor project = image_repo prefix; explicitly NOT forgejo_admin/...). This directly corrects the prior #1094 reviewer's flag. However, two backing-doc and one label issue need fixing before todo→next_up, and a doc-drift issue in arch-deployment-westside-admin must be resolved or downstream tickets will inherit the wrong image path.

    Template Completeness

    • [x] Type — Task
    • [x] Lineage — cites discovery during #1094 scope review
    • [x] Repo — forgejo_admin/pal-e-services
    • [x] User Story — story-westside-admin-admin-row-crud, foundational
    • [x] Context — describes existing onboarding pattern + names sibling services
    • [x] Scope — clear: add k3s.tfvars entry, run tofu plan/apply
    • [x] Acceptance Criteria — 6 criteria, all testable
    • [x] Test Expectations — tofu plan review + Harbor UI check + first kaniko push
    • [x] Constraints — tofu (not terraform), -lock=false, naming convention nailed
    • [x] Checklist — present
    • [x] Related — project, arch, conventions, blocks linkage

    Traceability

    • [x] story:admin-row-crud label — admin row CRUD story
    • [x] story note verified — story-westside-admin-admin-row-crud exists; listed in project-westside-admin user-stories section
    • [~] arch:k8s-deploy label — misaligned. This ticket provisions Harbor projects + robot accounts (registry/IAM concern), not k8s deployment topology. arch:k8s-deploy is correctly used on #1095 (kustomize overlay). Recommend arch:harbor (matches sibling #1088) or arch:deployment-westside-admin (matches the arch note explicitly cited in Related). [LABEL]
    • [ ] arch note MISSING — no arch-k8s-deploy note exists in pal-e-docs (search returned 0 results). The arch label needs to point to a real backing note. [SCOPE]
    • [x] Forgejo issue — https://forgejo.tail5b443a.ts.net/forgejo_admin/pal-e-services/issues/64, open

    File Targets

    Task type — verified the implied file targets:

    • [x] ~/pal-e-services/terraform/k3s.tfvars — verified exists. Contains a services = { ... } map with 11 active entries. Schema confirmed: forgejo_repo, image_repo, port, funnel, source_repo, source_path. Reference entry westsidekingsandqueens (lines 224-231) uses key westsidekingsandqueens, image_repo = "westsidekingsandqueens/app". Mirroring confirmed feasible.
    • [x] ~/pal-e-services/terraform/services.tf — verified exists. Line 7 comment: "service key = Harbor project name = image_repo prefix. No dedup needed when naming is consistent." Line 13: name = each.key on harbor_project.service. Lines 24, 60: robot accounts named ${each.key}-ci and ${each.key}-pull. Module is for_each over var.services, so adding one entry to k3s.tfvars provisions the full stack (Harbor project, both robot accounts, namespace, image pull secret, ArgoCD app, optional funnel).
    • [x] Reference entry westsidekingsandqueens (services.tf-driven, k3s.tfvars:224-231) — matches scope description exactly.

    Repo Placement

    OK — k3s.tfvars belongs in forgejo_admin/pal-e-services. Single-repo change.

    Dependencies

    • This ticket BLOCKS: #1094 (forgejo_admin/westside-admin#8 — Woodpecker CI cannot push), #1095 (forgejo_admin/pal-e-deployments#133 — ArgoCD overlay cannot pull image). Documented in Related.
    • This ticket has no upstream blockers. Independent of scaffolding (#1087) and Dockerfile work — can run in parallel.
    • No in_progress items conflict. Only #1093 (page-server) is in todo; rest of board is backlog.
    • State lock note: ticket correctly invokes feedback_tofu_lock_false (-lock=false on plan). Keycloak/CNPG entries co-resident in same tfvars; modifying only services map limits drift surface.

    Acceptance Criteria

    All 6 ACs are testable:

    • k3s.tfvars entry: trivial diff check.
    • tofu fmt + tofu validate: standard pre-PR gate.
    • tofu plan -lock=false output in PR shows ONLY new additions: verifiable by reviewer.
    • Harbor project exists post-apply: Harbor UI check.
    • CI robot account exists with push perms: Harbor UI check.
    • Credentials wired: ambiguous — ACs say "(or document how the new project authenticates)". Per service-onboarding-sop, robot creds are per-service via the kubernetes_secret_v1.harbor_creds resource in services.tf (image pull secret harbor-creds in the namespace). For Woodpecker push secrets specifically, need to verify whether existing harbor_username/harbor_password global Woodpecker secrets work for the new project (Harbor project-level robot creds vs system-level creds). Dev should verify and document explicitly. [BODY]

    Blast Radius

    Harbor naming — this ticket gets it RIGHT. Verified against codebase:

    • services.tf:7 comment: "service key = Harbor project name = image_repo prefix"
    • services.tf:13: harbor_project.service uses name = each.key
    • 11 existing entries in k3s.tfvars: keys are westsidekingsandqueens, basketball-api, pal-e-app, pal-e-docs, mcd-tracker, mcd-tracker-app, gcal-scheduler, pal-e-mail, westside-ai-assistant, playme2k, platform-validation. None use Forgejo owner (forgejo_admin) as project name.
    • Ticket explicitly says (Constraints): "Service key naming: westside-admin (NOT forgejo_admin/westside-admin — owner namespace is irrelevant per feedback_harbor_project_naming)." This is exactly right.

    However: doc drift in arch-deployment-westside-admin. The arch note (cited in Related) says: "Project: forgejo_admin per feedback_harbor_project_naming" and "Image: harbor.tail5b443a.ts.net/forgejo_admin/westside-admin:<sha>". That is wrong. If downstream tickets (#1094 .woodpecker.yaml, #1095 overlay) read the arch note and copy forgejo_admin/westside-admin as the image path, they will diverge from what THIS ticket provisions (westside-admin/app). The chain breaks at runtime. Fix the arch note OR raise a sibling [SCOPE] item to update it BEFORE #1094 and #1095 are dispatched. [SCOPE]

    Sibling pattern check: mcd-tracker-app is the closest analog (frontend SvelteKit app, separate from API). Its k3s.tfvars entry uses key mcd-tracker-app, image_repo = "mcd-tracker-app/app", port = 80, funnel = true. westside-admin should mirror this exactly — and the ticket already proposes image_repo = "westside-admin/app". Port should likely be 3000 (per arch note: SvelteKit adapter-node on :3000), not 80. Dev should verify the chosen port matches what the Dockerfile/Service expects. [BODY]

    Decomposition Assessment

    1 file edit (k3s.tfvars), 6 AC, ~5-10 min agent work (mostly waiting on tofu plan/apply). No decomposition needed. Single-pass agent task once the [LABEL] and [SCOPE] items below are resolved.

    Recommendation

    • [LABEL] Replace arch:k8s-deploy with arch:harbor (matches #1088) or arch:deployment-westside-admin (matches the arch note in Related). arch:k8s-deploy belongs to #1095 (overlay), not this Harbor-provisioning ticket.
    • [SCOPE] Either create arch-k8s-deploy note (if keeping the label) or rely on existing arch-deployment-westside-admin. Recommend the latter for consistency with sibling tickets.
    • [SCOPE] Fix doc drift in arch-deployment-westside-admin: the "Harbor registry" row says project=forgejo_admin and image path=harbor.tail5b443a.ts.net/forgejo_admin/westside-admin:<sha>. Update to project=westside-admin, image path=harbor.tail5b443a.ts.net/westside-admin/app:<sha>. Also fix the "Key Decisions" bullet "Image registry follows Harbor naming convention" which has the same error. Without this, #1094 and #1095 risk inheriting wrong values. Could be a one-line cleanup ticket OR fold into this ticket's PR.
    • [BODY] Verify and document the chosen port for the service entry (likely 3000 per arch note's SvelteKit adapter-node design, NOT 80). Pre-empts a later mismatch with #1095 overlay's Service definition.
    • [BODY] Verify Woodpecker secret model: does the new westside-admin-ci robot account need its own per-repo secrets in Woodpecker, or do existing global secrets work? Document the answer in the AC explicitly — currently AC says "or document how the new project authenticates," which leaves the dev guessing.

    Once labels are fixed and arch-note doc drift is resolved (or scoped as a sibling ticket), this ticket is READY. The core terraform work is well-specified, the convention is correctly cited, and the file targets are verified.

  • Verdict: APPROVED

    Re-review of review-1094-2026-04-25 (NEEDS_REFINEMENT). All previously flagged issues have been addressed in the refined Forgejo issue body.

    Diff vs Previous Review

    • [BODY] Harbor project naming — FIXED. Body now explicitly states repo: westside-admin/app with rationale "service key = Harbor project = image_repo prefix" matching pal-e-services/terraform/services.tf:7 convention. Acceptance Criteria includes "project = westside-admin, NOT forgejo_admin". Constraints section reinforces.
    • [BODY] failure: ignore on update-kustomize-tag — FIXED. Step 4 in Scope explicitly says "Add failure: ignore per Woodpecker pipeline syntax so a failed kustomize tag bump does NOT mark the pipeline as failed". AC restates it.
    • [BODY] Service-registry onboarding dep — FIXED. Lineage names forgejo_admin/pal-e-services#<NEW>, board now has #1098 → pal-e-services#64 ("Onboard westside-admin: Harbor project + CI robot account via terraform"), state: open.
    • [LABEL] Arch label canonical — FIXED. Label changed from arch:woodpecker-ci to arch:ci-pipeline, which matches convention-architecture-ids "Deployment Components" table.
    • [SCOPE] Story + project page — RESOLVED. project-westside-admin exists with user-stories section listing admin-row-crudstory-westside-admin-admin-row-crud (verified via get_section).

    Template Completeness

    • [x] Type — Task
    • [x] Lineage — names #6, #7, pal-e-services#<NEW>
    • [x] Repo — forgejo_admin/westside-admin
    • [x] User Story — story-westside-admin-admin-row-crud
    • [x] Context — Harbor naming critical-block called out
    • [x] Scope — 4 steps fully specified, including failure: ignore
    • [x] Acceptance Criteria — 9 testable criteria
    • [x] Test Expectations — manual verification path defined
    • [x] Constraints — version pins, secrets, Harbor project name
    • [x] Checklist
    • [x] Related — references project, arch, feedback memory, reference repo, deps

    Traceability

    • [x] story:admin-row-crud label — admin row CRUD story
    • [x] story note verified — story-westside-admin-admin-row-crud exists, listed in project-westside-admin user-stories section
    • [x] arch:ci-pipeline label — Woodpecker CI per convention-architecture-ids Deployment Components table
    • [x] arch entry verified — arch:ci-pipeline documented in canonical convention note (Woodpecker CI). No standalone arch- note required; convention table is the source of truth.
    • [x] type:infra label — correct (CI pipeline config)
    • [x] Forgejo issue — https://forgejo.tail5b443a.ts.net/forgejo_admin/westside-admin/issues/8, open

    File Targets

    • [x] ~/westside-app/.woodpecker.yaml (reference) — verified: 4 steps (clone, validate, build-and-push with repo: westsidekingsandqueens/app, update-kustomize-tag with OVERLAY: westsidekingsandqueens) confirms structure to mirror
    • [x] ~/pal-e-platform/scripts/update-kustomize-tag.sh — verified exists; required env: FORGEJO_TOKEN, OVERLAY, IMAGE_TAG; defaults: OVERLAY_ENV=prod, FORGEJO_HOST=forgejo-http.forgejo.svc.cluster.local, DEPLOY_REPO=forgejo_admin/pal-e-deployments. Path written by ticket: overlays/westside-admin/prod/kustomization.yaml — matches sibling #1095.
    • [x] pal-e-services/terraform/services.tf — verified line 7 comment "Convention: service key = Harbor project name = image_repo prefix" + sibling examples (westsidekingsandqueens/app, basketball-api/api, mcd-tracker-app/app). New westside-admin/app entry follows pattern.
    • [x] Target file ~/westside-admin/.woodpecker.yaml — repo not yet cloned locally (scaffold ticket #6 not landed). Acceptable: this ticket is blocked-by #6 + #7, will be created after scaffold lands.

    Repo Placement

    OK — .woodpecker.yaml belongs in forgejo_admin/westside-admin. Single-repo target. Cross-repo coordination (overlay creation, service registry entry, robot account) is handled by sibling tickets (#1095, #1098) per the lineage section.

    Dependencies

    • Hard blocked-by: #1087 (westside-admin#6 scaffolding) — kaniko needs buildable code
    • Hard blocked-by: #1088 (westside-admin#7 Dockerfile) — kaniko needs build instructions
    • Hard blocked-by: #1098 (pal-e-services#64 service onboarding) — Harbor project westside-admin + CI robot account must exist before kaniko can push
    • Soft dep: #1095 (pal-e-deployments#133 overlay) — without overlay, update-kustomize-tag step fails; failure: ignore covers this so the build itself isn't blocked, but end-to-end ArgoCD deploy needs the overlay
    • All deps explicitly named in Lineage + Related sections.

    Acceptance Criteria

    9 criteria, all testable post-merge in Woodpecker UI + Harbor UI + pal-e-deployments commit history. YAML parse-validation criterion is mechanically verifiable. Image-path AC explicitly disambiguates the convention. Branch behavior (push/PR/manual) is enumerable. No untestable assertions.

    Blast Radius

    Low. The ticket's primary risk (Harbor naming drift causing the 36-hour outage scenario) is now explicitly defended-against in three places (Context paragraph, AC, Constraints). Service onboarding dep prevents kaniko from pushing into a non-existent project. The failure: ignore on the tag-bump step prevents this pipeline from breaking other pipelines via shared deployment-repo race conditions.

    Decomposition Assessment

    1 file, 4 pipeline steps, 9 AC. ~5 min agent work. No decomposition needed.

    Recommendation

    No action needed. Ready to advance backlog → todo.

    • review-1094-2026-04-25 — previous verdict (NEEDS_REFINEMENT)
    • convention-architecture-ids — confirms arch:ci-pipeline canonical
    • feedback_harbor_project_naming — Harbor naming convention enforced
    • feedback_yaml_parse_validation — YAML parse-validation enforced in body
  • Verdict: NEEDS_REFINEMENT

    Re-review of review-1091-2026-04-25. Previous verdict was NEEDS_REFINEMENT with two recommendations: [SCOPE] resolve arch label mismatch, [BODY] pin Lineage to explicit issue numbers.

    Changes Since v1 Review

    • [x] [BODY] Lineage pinned — issue body now reads "Hard depends on forgejo_admin/westside-admin#1 (Drizzle setup ...). Hard depends on forgejo_admin/westside-admin#6 (scaffolding)." Related section also adds "DEPENDS ON: #6, #1". Resolved.
    • [~] [SCOPE] Arch label changed from arch:scopeddb-helper to arch:scoped-db. Sibling ticket #1089 (Drizzle setup) also carries arch:scoped-db, so the two helper-layer tickets now share one arch label cleanly. Partially resolved — the label is consistent across the helper layer, but no backing arch-scoped-db note exists.

    Template Completeness

    • [x] Type — Feature
    • [x] Lineage — explicit issue numbers (#1, #6)
    • [x] Repo — forgejo_admin/westside-admin
    • [x] User Story — story-westside-admin-admin-row-crud
    • [x] Context — clear motivation, swap-point design, lint enforcement rationale
    • [x] File Targets — Create + Modify + Do NOT modify lists present
    • [x] Acceptance Criteria — 6 ACs, behavioral + type-system + lint + comment
    • [x] Test Expectations — unit + integration described
    • [x] Constraints — no JWT-derived tenant_id (v1), Drizzle typed builders only
    • [x] Checklist — PR / ACs / lint verification
    • [x] Related — project page + arch note + DEPENDS ON line

    Traceability

    • [x] story:admin-row-crud label — present
    • [x] story note verified — found in project-westside-admin user-stories table (story-westside-admin-admin-row-crud, Admin role, "Zero psql UPDATEs / zero fix-X texts over 30 days")
    • [x] arch:scoped-db label — present (was arch:scopeddb-helper in v1; now matches sibling #1089)
    • [ ] arch note MISSING — [SCOPE] No arch-scoped-db note exists in pal-e-docs (verified via search_notes queries "arch-scoped-db" and "scoped-db westside-admin", both empty). The project-westside-admin Architecture section lists exactly three arch notes: arch-domain-westside-admin, arch-dataflow-westside-admin, arch-deployment-westside-admin. The issue's Related section still points to arch-dataflow-westside-admin. The arch label and the issue's Related link disagree. Two clean resolutions: (a) create arch-scoped-db-westside-admin as a sub-component note nested under arch-dataflow-westside-admin — this matches the sibling-ticket pattern and gives the helper layer a real architecture home, or (b) relabel both #1091 and #1089 to arch:dataflow-westside-admin and drop scoped-db as a phantom component. Recommend (a): scoped-db is a real architectural seam (the only legal entry point to the database from page-server code), worth its own note.
    • [x] type:feature label — present
    • [x] Forgejo issue — https://forgejo.tail5b443a.ts.net/forgejo_admin/westside-admin/issues/3 — open

    File Targets

    Repo forgejo_admin/westside-admin still contains only README.md on main (verified via Forgejo contents API). Every File Target is part of the dependent ticket chain (#6 scaffolding → #1 Drizzle setup → this ticket). Cannot be live-verified; scoped relative to post-#1 tree.

    • [~] src/lib/server/tenant.ts (Create) — path well-formed, plausible after #1
    • [~] src/lib/server/scopedDb.ts (Create) — path well-formed
    • [~] src/lib/server/db.ts (Modify) — issue #1 explicitly creates this; consistent
    • [~] package.json (Modify) — root path, exists after scaffolding #6

    Acceptable for backlog→todo. Dev agent must re-verify after #6 + #1 land.

    Repo Placement

    OK. All file targets in westside-admin; matches issue's Repo header. No multi-repo fan-out.

    Dependencies

    • Hard blocker: #1 (Drizzle setup, board item #1089, backlog) — creates db.ts and schema.ts. Now explicitly named in Lineage.
    • Hard blocker: #6 (scaffolding, board item #1087, backlog) — creates SvelteKit shell. Now explicitly named in Lineage.
    • Downstream: #2 hooks-server (#1090), #4 page-server tables (#1092), #5 page-server rows (#1093, already in todo) all consume scopedDb. Note: #1093 is in todo column despite depending on this ticket which is still backlog — that's a board ordering question for Ava, not this ticket's concern.
    • Documented? Yes — Lineage now pins both upstream issues by number.

    Acceptance Criteria

    Unchanged from v1, all 6 ACs agent-verifiable:

    • AC1–AC2: SQL output assertions — verifiable via snapshot test of generated query
    • AC3: TypeScript type error on system-table misuse — verifiable via expectTypeOf or tsc --noEmit
    • AC4: CI lint check fails on direct db import — verifiable by running lint on fixture
    • AC5: 6+ unit tests — quantitative
    • AC6: multi-line swap-procedure comment in tenant.ts — verifiable via grep

    Integration test (URL/form-body tenant_id mutation still produces correct WHERE clause) remains the strongest data-leak guarantee. Solid.

    Blast Radius

    Unchanged from v1:

    • First Drizzle consumer in the cluster — pattern set here informs future adoption
    • Downstream rebases on #1090, #1092, #1093 if API shape is wrong; agent should sanity-check Drizzle's actual builder API before locking signatures
    • Grep-based lint is brittle to renames/re-exports; acceptable for v1
    • TENANT_ID = 1 in TS file works for v1; swap-point comment is the discipline

    Decomposition Assessment

    4 file targets (2 create + 2 modify), 6 ACs, ~6 unit tests + 1 integration test. Estimated agent work 30–60 min one-pass. No decomposition needed. Within 5-minute-rule envelope.

    Recommendation

    • [SCOPE] Create arch-scoped-db-westside-admin note (or chosen slug) under arch-dataflow-westside-admin. Document: helper API surface (scopedDb + systemDb), tenant-table vs system-table split, lint-enforced single entry point, TENANT_ID swap procedure. Same note backs sibling ticket #1089. Once created, also align board labels and the issue's Related section so they reference the new arch note instead of (or alongside) arch-dataflow-westside-admin. Ava's call on slug + whether to relabel.

    One unresolved [SCOPE] from v1 remains. The [BODY] Lineage fix landed cleanly. Ticket otherwise reads READY — strong scope, crisp ACs, well-defined seam. Once the arch note exists (or Ava decides the label is purely organizational and waives the requirement), this flips to APPROVED.

  • Verdict: APPROVED

    Re-review of board item #1096 — forgejo_admin/pal-e-platform#301. Original verdict review-1096-2026-04-25 was NEEDS_REFINEMENT due to false-premise (terraform-managed Keycloak client did not exist). Issue body has been entirely rewritten to path A: manually create the client via Keycloak admin console + document procedure as sop-keycloak-client-creation SOP. Re-reviewed 2026-04-25.

    Resolution of Original Findings

    • [x] False premise removed — ticket no longer claims a terraform westside-app client to "clone." Lineage section explicitly documents the discovery and links review-1096-2026-04-25.
    • [x] Outage risk acknowledged and avoided — Constraints explicitly prohibit declaring keycloak_realm or adding the mrparkers provider.
    • [x] File path issue moot — no terraform file targets in path A.
    • [x] Public vs confidential decision — explicitly resolved with default (public + PKCE) and pattern-matching fallback (match westside-app's choice).
    • [x] Secret export path — defined: SOPS-encrypted Secret in pal-e-deployments/overlays/westside-admin/prod/westside-admin-secrets.enc.yaml, coordinated with #133.

    Template Completeness

    • [x] Type — Task
    • [x] Lineage (with scope-revision history)
    • [x] Repo
    • [x] User Story
    • [x] Scope (two well-bounded deliverables)
    • [x] Acceptance Criteria (8, all testable)
    • [x] Test Expectations
    • [x] Constraints (explicit DO-NOTs that prevent the discovered outage risk)
    • [x] Checklist
    • [x] Related

    Traceability

    • [x] story:admin-row-crud — verified in project-westside-admin user-stories section
    • [x] story note story-westside-admin-admin-row-crud exists
    • [x] arch:keycloak label present
    • [ ] arch note arch-keycloak still MISSING — search returned no results. [SCOPE] Carryover from v1 review: create architecture note arch-keycloak. Not blocking this ticket since path A is deliberately admin-console (no IaC); the SOP itself partially compensates by describing the component boundary. Recommend tracking as a separate small backlog item rather than blocking #1096.
    • [x] Forgejo issue #301 — open, rewritten

    File Targets

    Path A has no source-code file targets in pal-e-platform. Deliverables:

    • [x] Keycloak admin console at https://keycloak.tail5b443a.ts.net/admin — verified URL pattern matches existing infra
    • [x] pal-e-docs SOP note sop-keycloak-client-creation — does not yet exist (verified: search empty), to be created by this ticket
    • [x] Optional: pal-e-deployments/overlays/westside-admin/prod/westside-admin-secrets.enc.yaml — only if confidential client; coordinated with #133

    Repo Placement

    OK. Tracking in forgejo_admin/pal-e-platform is correct (platform-team responsibility for Keycloak), even though the SOP artifact lands in pal-e-docs. Lineage paragraph explicitly explains the placement.

    Dependencies

    Cleanly documented:

    • BLOCKS forgejo_admin/westside-admin#2 (auth code consumes client_id)
    • BLOCKS forgejo_admin/pal-e-deployments#133 (overlay's KEYCLOAK_CLIENT_SECRET secret only meaningful if confidential client chosen)
    • No upstream blockers — Keycloak server already deployed

    Acceptance Criteria

    All 8 ACs are testable. Key strengths: explicit visibility check in admin console, SOP self-sufficiency criterion ("future agent or human can follow it without prior Keycloak knowledge"), explicit "no terraform changes" guard, .well-known endpoint sanity check.

    Blast Radius

    Path A eliminates the v1 outage risk entirely — no terraform state changes, no realm-level operations, no risk of overwriting existing westside-app SSO. Constraints section explicitly enumerates DO-NOTs that prevent scope creep into IaC territory.

    Decomposition Assessment

    Two deliverables (admin-console client creation + SOP note) are tightly coupled to a single Keycloak admin session and a single doc-author session. Estimated under 5 minutes for an executor with admin access. No decomposition needed.

    Recommendation

    APPROVED. Ready to advance backlog → todo.

    • [SCOPE] (non-blocking carryover) Create architecture note arch-keycloak as a separate backlog item documenting the Keycloak component and the admin-console-vs-IaC management boundary. Do not block #1096 on this — the SOP this ticket produces will partially fulfill the same role.

    All v1 findings resolved. Body is concrete, ACs are testable, scope is well-bounded, outage risk is avoided.

  • Verdict: READY

    Re-review of board item #1087 — forgejo_admin/westside-admin#6 — supersedes review-1087-2026-04-25 (NEEDS_REFINEMENT). All v1 blockers resolved.

    Changes Since v1

    • Arch label fixed: arch:sveltekit-serverarch:sveltekit-ssr. The new label is a canonical example in convention-architecture-ids ("SvelteKit server-side rendering" under Data Flow Components). Backing convention exists; per-label arch note not required.
    • /health route added to scope with corresponding AC ("GET /health returns HTTP 200") and Test Expectations entry. Unblocks Dockerfile HEALTHCHECK in #7.
    • westside-app reference clarified: issue body now states westside-app is adapter-static + plain JS + no ESLint, recommends fresh npm create svelte@latest scaffold and points at westside-contracts as a closer adapter-node reference if available.
    • Version pinning narrowed: Constraints now scope "match westside-app versions" to kit/svelte/svelte-check/vite only; new deps (typescript, eslint, prettier, adapter-node) at latest stable.
    • Blocker list explicit in Lineage: blocks #1, #2, #3, #4, #5, #7, #8.

    Template Completeness

    Type: Task → reviewed against template-issue (base + Scope).

    • [x] Type
    • [x] Lineage (with explicit blocker list)
    • [x] Repo
    • [x] User Story
    • [x] Context (with adapter-node rationale + reference repo caveat)
    • [x] Scope (with explicit anti-scope listing #1, #2, #7, #8)
    • [x] Acceptance Criteria (7 AC, all agent-verifiable)
    • [x] Test Expectations
    • [x] Constraints
    • [x] Checklist
    • [x] Related

    Traceability

    • [x] story:admin-row-crud — backing note story-westside-admin-admin-row-crud (id 1616) exists, status active
    • [x] arch:sveltekit-ssr — canonical label per convention-architecture-ids Data Flow Components table. No per-label arch note required (component sub-element of arch-dataflow-westside-admin).
    • [x] type:infra — appropriate for scaffolding
    • [x] Forgejo issue #6 — open, well-formed, type=Task

    File Targets

    N/A (Task type). Scope enumerates files to be CREATED in empty repo. New: src/routes/health/+server.ts added to support Dockerfile HEALTHCHECK. All paths conventional SvelteKit.

    Repo Placement

    OK. Single repo (forgejo_admin/westside-admin). No cross-repo touches.

    Dependencies

    None upstream — first foundational ticket. Blocks #1, #2, #3, #4, #5, #7, #8 (now explicit in Lineage). Anti-scope correctly defers db.ts, hooks.server.ts, Dockerfile, .woodpecker.yaml.

    Acceptance Criteria

    7 AC, all agent-verifiable via shell commands. /health AC ties scope to downstream Dockerfile work cleanly. No ambiguity, all testable.

    Blast Radius

    None. Empty repo, no consumers, no production system touched.

    Decomposition Assessment

    7 AC, 1 repo, ~11 files of conventional scaffolding. Borderline on the 5-minute rule but cohesive (one PR, one commit). The added /health route is trivial. No decomposition needed.

    Recommendation

    No action needed. Ticket is READY to advance backlog → todo.

    Carry-over (board-wide, NOT this ticket): v1 flagged 7 other arch labels on board-westside-admin (arch:scoped-db, arch:keycloak, arch:hooks-server, arch:page-server, arch:harbor, arch:ci-pipeline, arch:postgres, arch:k8s-deploy). Per the now-confirmed reading of convention-architecture-ids (labels = component IDs from diagrams, no per-label note required), these are acceptable as-is provided each maps to a node in arch-dataflow-westside-admin or arch-deployment-westside-admin. Recommend a separate audit ticket to spot-check the 7 labels resolve to real diagram nodes — not blocking any individual ticket.

  • Verdict: NEEDS_REFINEMENT

    Re-review of board item #1089 (forgejo_admin/westside-admin#1) after refinement per review-1089-2026-04-25. Type=Feature. All v1 [BODY] recommendations resolved. One [SCOPE] item remains: arch label backing note still missing.

    v1 Recommendations Resolution

    • [x] [BODY] "Blocked by #6 + pal-e-platform#302" — RESOLVED. Lineage now reads: "Hard depends on forgejo_admin/westside-admin#6 ... Hard depends on forgejo_admin/pal-e-platform#302". Explicit issue numbers present.
    • [x] [BODY] CI drift AC narrowed — RESOLVED. AC now reads "scripts/check-schema-drift.sh runs locally with DATABASE_URL set and exits 0 if schema matches, non-zero on diff". Woodpecker wiring explicitly excluded under "Do NOT create" with cross-ref to #8. Script is a new file target.
    • [~] [SCOPE] Resolve arch label mismatch — PARTIALLY RESOLVED. Label was renamed arch:scopeddb-helperarch:scoped-db on board item #1089 (and #1091). However, no arch-scoped-db note exists in pal-e-docs (404 on direct slug, 0 hits on search). The project-westside-admin Architecture section still lists only 3 arch notes: arch-domain-westside-admin, arch-dataflow-westside-admin, arch-deployment-westside-admin. The new label still has no backing note.

    Template Completeness

    • [x] Type, Lineage, Repo, User Story, Context, File Targets, Acceptance Criteria, Test Expectations, Constraints, Checklist, Related — all present and well-formed.
    • [x] File Targets section now includes scripts/check-schema-drift.sh.

    Traceability

    • [x] story:admin-row-crud label — verified in project-westside-admin user-stories table (single v1 story).
    • [x] arch:scoped-db label — present (renamed since v1).
    • [ ] arch note MISSING — no arch-scoped-db note exists; project page Architecture section does not list it. [SCOPE] Either (a) create arch-scoped-db note documenting the tenant-scoped query helper component and add it to the project page Architecture list, OR (b) re-label this ticket and #1091 to arch:domain-westside-admin (closest existing fit since the artifact this ticket produces — schema.ts — is the schema dimension already documented there).
    • [x] Forgejo issue — https://forgejo.tail5b443a.ts.net/forgejo_admin/westside-admin/issues/1, open.

    File Targets

    Greenfield repo (only README.md on main). All targets are net-new creates; existence checks are N/A.

    • [x] drizzle.config.ts — config spec clear (dialect=postgresql, schema path, env-driven creds)
    • [x] src/lib/server/db.ts — pattern stated (lazy init, SIGTERM handling, @internal)
    • [x] src/lib/server/schema.ts — generated, committed
    • [x] package.json — deps + db:pull script
    • [x] scripts/check-schema-drift.sh — new in v2; locally runnable, exit-coded

    Repo Placement

    OK. Forgejo issue is on forgejo_admin/westside-admin. No cross-repo file targets.

    Dependencies

    • Hard blocker: westside-admin#6 (scaffolding) — explicit in Lineage. Must merge first.
    • Hard blocker for AC verification: pal-e-platform#302 (admin_app Postgres user) — explicit in Lineage. Needed for db:pull and integration AC.
    • Decoupled: westside-admin#8 (Woodpecker CI) — drift check runs locally; CI wiring is out of scope per "Do NOT create" section. Good separation.
    • Downstream consumers: #2 (hooks-server), #4/#5 (page-server), #3 (scoped-db query helper) all import from src/lib/server.

    Acceptance Criteria

    6 ACs, all verifiable. Strong: explicit table count (22), enum count (18), local drift script with exit code, lazy init + SIGTERM. The CI-coupling concern from v1 is gone.

    Blast Radius

    Greenfield repo, no existing patterns to break. Driver choice (pg) matches westside-app convention. jsonb narrowing flagged with defer-or-document option.

    Decomposition Assessment

    5 file targets (was 4), 6 ACs, single repo, ~5–10 min agent work. Borderline acceptable as one ticket. No decomposition needed.

    Recommendation

    One remaining item:

    • [SCOPE] Resolve arch:scoped-db backing note. Two viable paths:
      • Path A (preferred if scoped-db is a real component): Create arch-scoped-db note describing the tenant-scoping query helper (the constraint Safety #2 enforces) and add it to project-westside-admin architecture list. This is a docs task (~5 min) and naturally pairs with #1091 (issue #3) which produces the helper itself.
      • Path B (if no dedicated component is intended): Re-label #1089 and #1091 to arch:domain-westside-admin. Schema.ts is the domain artifact; arch-domain-westside-admin already documents the 22 tables.

    All [BODY] fixes from v1 are clean. Ticket body is dispatch-ready. The arch-note gap is the sole reason for NEEDS_REFINEMENT; depending on Ava's call this is a 5-minute docs fix or a label change. Once resolved, advance backlog → todo.

  • Verdict: READY

    Re-review of board item #1087 — forgejo_admin/westside-admin#6 — column: backlog (gate: backlog→todo). Supersedes review-1087-2026-04-25 (NEEDS_REFINEMENT).

    Changes Since v1

    • Arch label changed from arch:sveltekit-serverarch:sveltekit-ssr (canonical entry in convention-architecture-ids Data Flow Components table).
    • Issue body Context now explicitly flags westside-app as NOT a transferable scaffold (adapter-static + plain JS + no ESLint), points to westside-contracts as a better reference if adapter-node, and instructs use of npm create svelte@latest.
    • Added /health route to Scope, AC, Test Expectations, and Checklist (needed by Dockerfile ticket #7 HEALTHCHECK).
    • Lineage now lists explicit blocked tickets (#1, #2, #3, #4, #5, #7, #8) — confirms downstream sequencing.
    • Constraints narrowed the version-pin scope to packages actually shared with westside-app.

    Template Completeness

    Type=Task, reviewed against template-issue:

    • [x] Type
    • [x] Lineage
    • [x] Repo
    • [x] User Story
    • [x] Context
    • [x] Scope
    • [x] Acceptance Criteria
    • [x] Test Expectations
    • [x] Constraints
    • [x] Checklist
    • [x] Related

    Traceability

    • [x] story:admin-row-crud — story note story-westside-admin-admin-row-crud (id 1616) verified, listed in project-westside-admin user-stories
    • [x] arch:sveltekit-ssr — canonical entry in convention-architecture-ids (Data Flow Components: "SvelteKit server-side rendering"). Per the convention's Mermaid-derived component model, this label points to a diagram component, not a standalone arch note. The backing diagram notes (arch-deployment-westside-admin, arch-dataflow-westside-admin) are linked from the issue Related section.
    • [x] type:infra — appropriate for scaffolding
    • [x] Forgejo issue — #6 open, body well-formed

    File Targets

    N/A — Task type. Scope enumerates files to be CREATED in an empty repo. Repo confirmed exists (forgejo_admin/westside-admin, default branch main, public). All scaffold paths are conventional SvelteKit + adapter-node.

    Repo Placement

    OK. Issue on forgejo_admin/westside-admin; scope entirely in that repo. Does not touch pal-e-deployments, pal-e-platform, or basketball-api.

    Dependencies

    None upstream — first foundational ticket. Explicitly blocks #1 (Drizzle), #2 (Auth/hooks.server.ts), #3 (Tenant helper), #4 (Players list), #5 (Players edit), #7 (Dockerfile), #8 (Woodpecker). Anti-scope is explicit (no Drizzle, no auth, no Dockerfile, no CI here). Sequencing is correct.

    Acceptance Criteria

    7 AC, all agent-verifiable via shell commands (npm install, npm run check, npm run build, npm run dev, node build/index.js, curl /health, ESLint+Prettier). Borderline on the 5-AC threshold but each maps 1:1 to a single command — no compound verification, no ambiguity. Test Expectations adds a build sanity check.

    Blast Radius

    None. New empty repo, no consumers. Version-pin constraint is now scoped to packages actually shared with westside-app.

    Decomposition Assessment

    1 repo, ~10–11 files of scaffolding, 7 AC, all conventional npm create svelte + adapter swap + /health + ESLint config. Estimated agent time: 5–10 minutes. AC count is one over the 5-AC trigger but the work is highly cohesive (one repo, one PR, one commit) and decomposing scaffolding would create more overhead than it saves. No decomposition needed.

    Recommendation

    No action needed. All v1 concerns resolved:

    • arch label is now canonical and matches the convention
    • westside-app misleading reference is explicitly disclaimed in Context
    • /health route requirement is explicit (unblocks #7)
    • Blocker list is explicit (improves planning for downstream tickets)

    Ready to advance backlog → todo.

    • review-1087-2026-04-25 — v1 (NEEDS_REFINEMENT)
    • story-westside-admin-admin-row-crud
    • convention-architecture-ids
    • board-westside-admin
  • Verdict: NEEDS_REFINEMENT

    Scope is solid in structure (mirrors westside-app pipeline correctly) but contains a Harbor-naming inconsistency that will break the build-and-push step at runtime, plus missing backing notes (story + arch) and missing user-stories project page.

    Template Completeness

    • [x] Type — Task
    • [x] Lineage — depends on scaffolding + Dockerfile tickets
    • [x] Repo — forgejo_admin/westside-admin
    • [x] User Story — story-westside-admin-admin-row-crud (foundational)
    • [x] Context — clear 4-step pipeline mirror description
    • [x] Scope — detailed step-by-step intent
    • [x] Acceptance Criteria — 8 criteria, mostly testable
    • [x] Test Expectations — manual verification path defined
    • [x] Constraints — version pins + secrets noted
    • [x] Checklist — present
    • [x] Related — references reference repo + arch + project

    Traceability

    • [x] story:admin-row-crud label — admin row CRUD story
    • [ ] story note MISSING — no project-westside-admin page found in pal-e-docs (search returned no results). [SCOPE] Create user story entry on project-westside-admin user-stories section, OR confirm the project page exists under a different slug.
    • [x] arch:woodpecker-ci label — Woodpecker CI pipeline component
    • [ ] arch note MISSING — no arch-woodpecker-ci note found in pal-e-docs. [SCOPE] Create architecture note arch-woodpecker-ci for the CI component (or rename label to match an existing arch note such as arch-deployment-westside-admin referenced in the issue body).
    • [x] Forgejo issue — https://forgejo.tail5b443a.ts.net/forgejo_admin/westside-admin/issues/8, open

    File Targets

    • [x] ~/westside-app/.woodpecker.yaml (reference) — verified exists, 4 steps as described (clone, validate, build-and-push, update-kustomize-tag)
    • [x] ~/pal-e-platform/scripts/update-kustomize-tag.sh — verified exists, env contract matches (FORGEJO_TOKEN, OVERLAY, IMAGE_TAG required; OVERLAY_ENV default prod)
    • [x] Target file ~/westside-admin/.woodpecker.yaml — repo cloned locally; file does not yet exist (this ticket creates it). Correct.
    • [ ] Implied target overlay overlays/westside-admin/prod in pal-e-deployments — does NOT yet exist (sibling ticket #1095 / pal-e-deployments#133 creates it). The update-kustomize-tag step will fail until #1095 lands. The "must NOT block the pipeline if it fails" requirement covers this, but document the explicit blocker dep.

    Repo Placement

    OK — .woodpecker.yaml belongs in forgejo_admin/westside-admin. No multi-repo concerns for this single file.

    Dependencies

    • Blocked by: scaffolding ticket (westside-admin#6, board #1087) and Dockerfile ticket — without a Dockerfile, kaniko build will fail. Lineage section names them but does not link issue numbers.
    • Blocked by: deployment overlay ticket (pal-e-deployments#133, board #1095) — update-kustomize-tag step has nothing to update until the overlay's kustomization.yaml with a newTag field exists.
    • Blocked by: service-registry onboarding for westside-admin in pal-e-services/terraform/k3s.tfvars — required to provision the Harbor project + CI robot account before kaniko can push. NOT mentioned in the ticket. [SCOPE]
    • Required secrets: ticket assumes harbor_username, harbor_password, forgejo_token exist as repo or global secrets in Woodpecker. Per service-onboarding-sop, per-service Harbor robot creds are provisioned by tofu apply on services.tf — agent must verify the secrets are wired to the westside-admin repo in Woodpecker before merge, not after.

    Acceptance Criteria

    Mostly testable. Notes:

    • AC "Harbor project: forgejo_admin (matches repo owner per feedback_harbor_project_naming)" — this is wrong. See Blast Radius below. Convention is "service key = Harbor project = image_repo prefix" (per pal-e-services/terraform/services.tf line 7), NOT repo owner. The reference westside-app uses repo: westsidekingsandqueens/app (Harbor project = westsidekingsandqueens), not forgejo_admin/westside-app.
    • AC "OVERLAY env var = westside-admin" — assumes overlay name; depends on #1095 using that exact name (verify consistency across tickets).
    • AC "image tag = ${CI_COMMIT_SHA} (full SHA)" — matches reference. OK.
    • AC "update-kustomize-tag failure does NOT break the pipeline" — reference uses depends_on: build-and-push + when: event=push branch=main. There is no explicit failure: ignore in the reference. The cited precedent commit 1d54939 ("run update-kustomize-tag step even when test step fails") changed when-conditions, not failure tolerance for the tag step itself. Dev agent must verify the actual mechanism (likely needs failure: ignore on the step) and not assume it is implicit. [BODY]

    Blast Radius

    Harbor naming convention conflict (high severity). The ticket cites feedback_harbor_project_naming to justify Harbor project = forgejo_admin, but that memory says Harbor projects derive from image_repo prefix, which in pal-e-services/terraform/services.tf is the service key (e.g. westsidekingsandqueens, basketball-api, pal-e-app, mcd-tracker-app). Forgejo owner names (forgejo_admin) have never been used as Harbor project names. Consequences if shipped as written:

    • Service registry needs a westside-admin entry with image_repo = "westside-admin/app" (or similar) to provision a westside-admin Harbor project.
    • If kaniko pushes to harbor.harbor.svc.cluster.local with repo: forgejo_admin/westside-admin, push will fail (no forgejo_admin project) OR create a forgejo_admin Harbor project (drift from convention; failed migration would mirror the 36-hour outage cited in the convention).
    • The kustomize overlay's image reference will need to match whatever Harbor path is chosen — if #1095 hardcodes a different path, ArgoCD will pull-fail.

    Recommendation: Pick a service key for westside-admin (suggest westside-admin matching the repo name, mirroring mcd-tracker-app), set image_repo = "westside-admin/app", and use kaniko repo: westside-admin/app. Coordinate this name across: ticket #8 (this one), service registry onboarding (a missing dep), kustomize overlay #1095, and Dockerfile ticket. [BODY]

    Decomposition Assessment

    1 file target, 4 pipeline steps, 8 AC, ~5 min agent work. No decomposition needed. Single-pass agent task once scope is corrected.

    Recommendation

    • [BODY] Fix the Harbor project AC: replace "Harbor project: forgejo_admin (matches repo owner)" with the chosen service key (likely westside-admin, matching image_repo prefix per services.tf convention). Update kaniko repo: setting in Scope to match.
    • [BODY] Add explicit failure: ignore (or equivalent step-level pattern) to update-kustomize-tag in the Scope, since reference repo's "non-blocking" behavior is not actually enforced by structure — verify how it currently behaves and document explicitly.
    • [BODY] Document the dependency on service-registry onboarding (new entry in pal-e-services/terraform/k3s.tfvars + tofu apply) before this pipeline can succeed end-to-end. Either add it as a Lineage item or open a sibling ticket.
    • [SCOPE] Create user story entry on project-westside-admin user-stories section (or create the project page if missing).
    • [SCOPE] Create architecture note arch-woodpecker-ci (or relabel the board item to arch:deployment-westside-admin matching the Related section in the issue body).
    • [LABEL] Confirm the board's arch label resolves to a real arch note before todo→next_up.
  • 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.

  • Verdict: NEEDS_REFINEMENT

    Board item #1096 — forgejo_admin/pal-e-platform#301 — Terraform: Keycloak OIDC client westside-admin. Reviewed 2026-04-25.

    Critical Finding

    The ticket's core premise is incorrect. It instructs the dev agent to "find the westside-app client definition and clone its structure." No such terraform-managed client exists. The Keycloak terraform module at terraform/modules/keycloak/main.tf deploys only the Keycloak server (namespace, secret, PVC, deployment, service, theme configmap). The keycloak terraform provider is not configured in terraform/providers.tf, no keycloak_realm resource exists, and no keycloak_openid_client resource exists. The westside-basketball realm and existing westside-app client are managed manually via the Keycloak admin console, not by terraform.

    This means the ticket as written cannot be executed. The dev agent will either (a) get blocked, or (b) bootstrap the keycloak provider + realm import + client resource themselves, which is materially larger scope than "add a client resource."

    Template Completeness

    • [x] Type — Task
    • [x] Lineage
    • [x] Repo
    • [x] User Story
    • [x] Context
    • [x] Scope
    • [x] Acceptance Criteria
    • [x] Test Expectations
    • [x] Constraints
    • [x] Checklist
    • [x] Related

    Traceability

    • [x] story:admin-row-crud label — Admin Row CRUD
    • [x] story note verified — story-westside-admin-admin-row-crud exists; user-stories section on project-westside-admin exists
    • [x] arch:keycloak label
    • [ ] arch note MISSING — search for arch-keycloak returned no results. Existing arch notes for the project are arch-domain-westside-admin, arch-dataflow-westside-admin, arch-deployment-westside-admin. No keycloak-component arch note exists in pal-e-docs. [SCOPE] Create architecture note arch-keycloak documenting the Keycloak component (realm, clients, terraform vs manual management boundary).
    • [x] Forgejo issue — #301, open

    File Targets

    Ticket references ~/pal-e-platform/keycloak.tf "or equivalent." Verified state of repo:

    • [ ] ~/pal-e-platform/keycloak.tf — DOES NOT EXIST. Keycloak terraform lives at terraform/modules/keycloak/main.tf.
    • [ ] "westside-app client definition" — DOES NOT EXIST in terraform. Grepped terraform/ for keycloak_openid_client, keycloak_realm, provider "keycloak", mrparkers, keycloak/keycloak — zero matches. The realm and any existing clients are unmanaged by IaC.
    • [x] terraform/modules/keycloak/main.tf — exists, deploys Keycloak server only.
    • [x] terraform/providers.tf — exists; declares kubernetes, helm, tailscale, minio. No keycloak provider.

    Repo Placement

    Repo (forgejo_admin/pal-e-platform) is correct for terraform-managed Keycloak resources. No mismatch.

    Dependencies

    Implicit prerequisite NOT declared in ticket:

    • Configuring the keycloak terraform provider in providers.tf (with admin credentials sourced from existing keycloak-admin secret or a new tfvar).
    • Importing or declaring the westside-basketball realm as a terraform resource (or marking it as data-only via a data "keycloak_realm" lookup so the new client can reference it without taking over realm management).
    • Decision needed: does this ticket also adopt the existing westside-app client into terraform (import), or leave it manual and only add the new westside-admin client? The ticket assumes the latter is trivially possible by "cloning"; in reality there is no terraform pattern to clone.

    Downstream board items relying on this:

    • #1087 (westside-admin SvelteKit scaffold) — needs this client to wire OIDC env vars, but does not block scaffolding.
    • #1088 (deployment) and #1095 (pal-e-deployments overlay) — consume the client_id and (if confidential) client_secret from k8s secret. Currently no defined export path.

    Acceptance Criteria

    ACs are testable as written, but several depend on the unstated bootstrap work (provider config, realm reference). The "If confidential: secret available in pal-e-platform secret store" criterion has no defined target — there is no existing secret-export pattern in this module to follow. Needs an explicit decision.

    Blast Radius

    If the dev agent attempts to manage the westside-basketball realm via a fresh keycloak_realm resource without import, terraform will try to create a realm that already exists, returning a 409 from Keycloak — or worse, if force-imported wrong, it will overwrite realm settings (login theme, password policies, registered users not stored in terraform) and could break westside-app SSO for Lucas and Marcus. This is a real outage risk. Realm must be imported via tofu import or referenced as a data source — not declared fresh.

    Decomposition Assessment

    If the bootstrap work (provider config + realm import + client resource + secret export) is included, this exceeds the 5-minute single-agent window: 3+ file targets in 1 repo, 7 ACs, plus a real outage risk requiring careful import sequencing. NEEDS DECOMPOSITION after the SCOPE questions are answered. Suggested split:

    1. Add keycloak terraform provider + import (or data lookup) the westside-basketball realm. PR shows tofu plan with zero diff after import.
    2. Add keycloak_openid_client resource for westside-admin with the spec'd settings.
    3. If confidential: add secret export to k8s for the deployment overlay to consume.

    Recommendation

    • [SCOPE] Lucas/Ava: confirm strategy — bootstrap keycloak terraform provider in this ticket, or first land a separate "adopt Keycloak under IaC" ticket and rebase this on top? Premise of "clone existing client pattern" is false; pick the replacement plan.
    • [SCOPE] Decide: public+PKCE or confidential client? Ticket leaves it open, but the answer drives whether secret-export work is in scope.
    • [SCOPE] Create architecture note arch-keycloak documenting the component and the IaC boundary (what's terraform-managed vs admin-console-managed).
    • [BODY] Fix file path reference: ~/pal-e-platform/keycloak.tfterraform/modules/keycloak/main.tf.
    • [BODY] Replace "find the westside-app client definition and clone" with the actual bootstrap path chosen in the SCOPE decision above.
    • [BODY] Add explicit AC for realm management: "westside-basketball realm referenced via data "keycloak_realm" lookup OR imported via tofu import, with no realm-level drift in tofu plan."
    • [DECOMPOSE] After SCOPE answered: split into 2-3 sub-tickets per Decomposition Assessment. Route to skill-decompose-ticket.
  • Verdict: NEEDS_REFINEMENT

    Note: Both READY and APPROVED are accepted as passing verdicts by the check-board-advance hook. This ticket needs minor refinement before todo.

    Template Completeness

    • [x] Type — Feature
    • [x] Lineage — "Depends on Drizzle setup ticket" (issue #1)
    • [x] Repo — forgejo_admin/westside-admin
    • [x] User Story — story-westside-admin-admin-row-crud
    • [x] Context — clear motivation, swap-point design, lint enforcement rationale
    • [x] File Targets — Create + Modify + Do NOT modify lists present
    • [x] Acceptance Criteria — 6 ACs, behavioral + type-system + lint + comment
    • [x] Test Expectations — unit + integration described
    • [x] Constraints — no JWT-derived tenant_id (v1), Drizzle typed builders only
    • [x] Checklist — PR / ACs / lint verification
    • [x] Related — project page + arch note linked

    Traceability

    • [x] story:admin-row-crud label — present
    • [x] story note verified — found in project-westside-admin user-stories table (story-westside-admin-admin-row-crud, Admin role, "Zero psql UPDATEs / zero fix-X texts over 30 days")
    • [x] arch:scopeddb-helper label — present
    • [ ] arch note MISSING — [SCOPE] No arch-scopeddb-helper note exists in pal-e-docs. The project page Architecture section lists three arch notes: arch-domain-westside-admin, arch-dataflow-westside-admin, arch-deployment-westside-admin. The issue body's Related section points to arch-dataflow-westside-admin — that is the actual backing arch note. Either (a) update the board label to arch:dataflow-westside-admin to match an existing arch note, or (b) create a dedicated arch-scopeddb-helper sub-component note. Recommend (a) since scopedDb is a layer in the data-flow diagram, not a standalone component.
    • [x] type:feature label — present
    • [x] Forgejo issue — https://forgejo.tail5b443a.ts.net/forgejo_admin/westside-admin/issues/3 — open

    File Targets

    Repo forgejo_admin/westside-admin currently contains only README.md on main — no src/ tree exists yet. This is expected: every File Target listed is part of the dependent ticket chain (scaffolding #6 → Drizzle setup #1 → this ticket #3). File Targets cannot be live-verified against the repo; they are scoped relative to the post-#1 tree.

    • [~] src/lib/server/tenant.ts (Create) — path well-formed, plausible after Drizzle setup
    • [~] src/lib/server/scopedDb.ts (Create) — path well-formed
    • [~] src/lib/server/db.ts (Modify) — issue #1 explicitly creates src/lib/server/db.ts; consistent
    • [~] package.json (Modify) — root path, will exist after scaffolding #6

    No live grep possible. Dev agent must verify after #6 + #1 land. Acceptable for backlog→todo.

    Repo Placement

    OK. All file targets are in westside-admin; matches the issue's Repo header. No multi-repo fan-out needed.

    Dependencies

    • Hard blocker: westside-admin issue #1 "Drizzle setup + drizzle-kit pull schema.ts" (board item #1089, also backlog) — creates src/lib/server/db.ts and schema.ts that this ticket modifies. Lineage line on issue body acknowledges this.
    • Implicit blocker: westside-admin issue #6 "scaffolding" (board item #1087, backlog) — must create the SvelteKit project shell before #1 can run.
    • Downstream: All arch:page-server tickets (#1092 issue #4, #1093 issue #5) consume scopedDb, plus the hooks-server ticket (#1090 issue #2). Shipping this ticket unblocks the entire admin row-crud surface.
    • Documented? Lineage section names "Drizzle setup ticket" but does not name issue #1 or scaffolding #6. [BODY] minor: pin Lineage to "Depends on #1 (Drizzle setup), which depends on #6 (scaffolding)."

    Acceptance Criteria

    All 6 ACs are agent-verifiable:

    • AC1–AC2: SQL output assertions — verifiable by snapshot test of the generated query.
    • AC3: TypeScript type error on system-table misuse — verifiable via expectTypeOf or tsc --noEmit on a negative-fixture file.
    • AC4: CI lint check fails on direct db import — verifiable by running the lint script on a fixture import.
    • AC5: 6+ unit tests — quantitative, easy to verify.
    • AC6: multi-line swap-procedure comment in tenant.ts — verifiable via grep.

    Test Expectations cleanly map to ACs. Integration test ("manually mutating tenant_id in URL or form body still produces correct WHERE clause") is the strongest signal — that's the data-leak guarantee. Solid.

    Blast Radius

    • Same pattern elsewhere: basketball-api owns the source-of-truth tenant_id columns. westside-admin is the first Drizzle consumer; westside-app + westside-contracts use raw pg. No same-bug-elsewhere risk yet, but the pattern set here will inform any future Drizzle adoption.
    • Downstream consumers: Every admin page-server load function (#1092, #1093) and hooks-server import (#1090) will couple to this API surface. Shipping a wrong API shape causes rebases on 3+ tickets. The decision to expose builders select / update / insert rather than a query-string helper is correct (composes with arbitrary where/and/or per the constraint), but agent should sanity-check Drizzle's actual builder API before locking signatures.
    • Lint enforcement risk: A grep-based lint that fires on import { db } from '$lib/server/db' is brittle to renames or re-exports. Acceptable for v1 per the issue's framing. Flag for arch-note follow-up, not a blocker here.
    • Tenant constant location: TENANT_ID = 1 in a TS file works for v1. The swap-point comment is the discipline. Acceptable.

    Decomposition Assessment

    4 file targets (2 create + 2 modify), 6 ACs, ~6 tests + 1 integration test. Estimated agent work: 30–60 min one-pass (TypeScript generics for the tenant-vs-system table type discrimination is the only non-trivial piece). No decomposition needed. Within the 5-minute-rule envelope for a single agent.

    Recommendation

    • [SCOPE] Resolve arch:scopeddb-helper label mismatch. Either (preferred) relabel the board item to arch:dataflow-westside-admin to match the existing arch note that the issue's Related section already points to, or create a new arch-scopeddb-helper sub-component note under arch-dataflow-westside-admin. Ava's call.
    • [BODY] Pin Lineage to explicit issue numbers: "Depends on #1 (Drizzle setup) which depends on #6 (scaffolding)." Helps the dev agent and downstream readers.

    Once the arch label resolves and Lineage is pinned, this ticket is READY. Scope, ACs, file targets, and constraints are all crisp. Strong ticket.

  • Verdict: NEEDS_REFINEMENT

    Board item #1089 — Forgejo issue forgejo_admin/westside-admin#1. Type=Feature. Backlog -> review gate.

    Template Completeness

    • [x] Type — Feature
    • [x] Lineage — "Depends on scaffolding ticket" (vague, see Recommendation)
    • [x] Repo — forgejo_admin/westside-admin
    • [x] User Story — story-westside-admin-admin-row-crud
    • [x] Context
    • [x] File Targets (Create section)
    • [x] Acceptance Criteria
    • [x] Test Expectations
    • [x] Constraints
    • [x] Checklist
    • [x] Related

    Traceability

    • [x] story:admin-row-crud label — story-westside-admin-admin-row-crud
    • [x] story entry verified — present in project-westside-admin user-stories table (single v1 story)
    • [x] arch:scopeddb-helper label — present
    • [ ] arch note MISSING — search_notes finds no note matching arch-scopeddb-helper. Project page architecture section lists arch-domain-westside-admin, arch-dataflow-westside-admin, arch-deployment-westside-admin — none correspond to the scopeddb-helper label. The "scoped DB helper" is described in Safety Constraint #2 (tenant scoping) but has no dedicated arch note. [SCOPE] Create arch-scopeddb-helper note OR re-label this ticket to an existing arch (e.g. arch-domain-westside-admin which covers the schema/introspection topic).
    • [x] Forgejo issue — https://forgejo.tail5b443a.ts.net/forgejo_admin/westside-admin/issues/1, open

    File Targets

    Repo is greenfield — only README.md exists on main. All targets are net-new creates, so file-existence verification is N/A. Targets are well-specified:

    • [x] drizzle.config.ts — config spec clear
    • [x] src/lib/server/db.ts — pattern + non-export discipline stated
    • [x] src/lib/server/schema.ts — generated, committed
    • [x] package.json — deps + db:pull script

    Note: src/lib/server/ doesn't exist yet — it will be produced by scaffolding ticket #6. This is fine; just confirms the hard dependency on #6 landing first.

    Repo Placement

    OK. Forgejo issue is on forgejo_admin/westside-admin. No cross-repo file targets in this ticket.

    Dependencies

    • Hard blocker: westside-admin#6 (Scaffold SvelteKit adapter-node + TypeScript + ESLint) — must merge before this ticket starts (no src/ tree exists yet). Issue body says "Depends on scaffolding ticket" but does not link the issue number. [BODY] Replace "Depends on scaffolding ticket" with explicit "Blocked by #6".
    • Hard blocker for AC verification: pal-e-platform#302 (admin_app Postgres user) — needed for the integration AC ("db.select().from(schema.tenants)") and CI drift check. Currently in backlog. [BODY] Add explicit reference to pal-e-platform#302 under Lineage so dispatch order is unambiguous.
    • Downstream consumers (not blocking this ticket but worth noting): westside-admin#2 (hooks-server), #4/#5 (page-server), #3 (scopeddb helper) all import from src/lib/server/schema.ts and src/lib/server/db.ts.

    Acceptance Criteria

    Verifiable and testable. Strong points: explicit table count (22), enum count (18), CI drift check, lazy-init pool, SIGTERM handling. One concern:

    • AC "CI step added: re-run db:pull against live DB, fail on diff" requires Woodpecker CI to be wired AND DATABASE_URL secret available in CI. Woodpecker CI for this repo is scoped under issue #8 (woodpecker-ci) which is also backlog. The CI drift check may need to be deferred or the dependency made explicit. [BODY] Either narrow this AC to "drift check script exists and runs locally" or note explicit dependency on #8.

    Blast Radius

    Greenfield repo — no existing patterns to break. Pattern choice (pg driver, drizzle-orm, env-driven config) matches westside-app's connection pool convention per Constraints. jsonb narrowing decision is flagged in Constraints with a defer-or-document option, which is acceptable.

    Decomposition Assessment

    4 file targets, 6 acceptance criteria, single repo, tight scope (drizzle install + pull + commit + CI). Estimated agent work ~5–10 min. Borderline but acceptable as one ticket. No decomposition needed.

    Recommendation

    • [BODY] Replace "Depends on scaffolding ticket" with "Blocked by westside-admin#6 (scaffolding) and pal-e-platform#302 (admin_app DB user)".
    • [BODY] Either narrow the CI drift AC to "script exists, runnable locally" OR add explicit dependency on westside-admin#8 (woodpecker-ci wiring).
    • [SCOPE] Resolve arch label mismatch: either create arch-scopeddb-helper note (if a dedicated component note is intended) or re-label this ticket with an existing arch slug (arch-domain-westside-admin is the closest fit since this ticket produces the schema artifact). Same arch:scopeddb-helper label is also used on board item #1091/issue #3, so the decision should be made jointly.

    Once these are addressed, this ticket is ready to advance backlog -> todo. No decomposition required.

  • Verdict: READY

    Board item #1088 — Dockerfile: multi-stage node:22-alpine build (forgejo_admin/westside-admin#7). Type: Task. Scope is solid, traceability complete, AC testable, fits in a single agent pass.

    Template Completeness

    • [x] Type — Task
    • [x] Lineage — depends on scaffolding (#6)
    • [x] Repo — forgejo_admin/westside-admin
    • [x] User Story — story-westside-admin-admin-row-crud (foundational)
    • [x] Context
    • [x] Scope (Tasks use Scope instead of File Targets — correct per template-issue)
    • [x] Acceptance Criteria
    • [x] Test Expectations
    • [x] Constraints
    • [x] Checklist
    • [x] Related

    Traceability

    • [x] story:admin-row-crud label — Admin Row CRUD
    • [x] story note verified — story-westside-admin-admin-row-crud exists, listed on project-westside-admin user-stories table
    • [x] arch:deployment label — deployment topology
    • [x] arch note verified — arch-deployment-westside-admin exists, references kaniko→Harbor→kustomize-tag→ArgoCD pipeline this Dockerfile feeds into
    • [x] Forgejo issue — https://forgejo.tail5b443a.ts.net/forgejo_admin/westside-admin/issues/7 (open)
    • [x] type:infra label

    File Targets

    Task type — no File Targets section required. Scope identifies two new files to create:

    • [x] Dockerfile — to be created at repo root
    • [x] .dockerignore — to be created at repo root

    Repo currently has no Dockerfile (verified; repo is empty pending scaffolding ticket #6). No conflicting file paths.

    Repo Placement

    OK. Issue filed on forgejo_admin/westside-admin, scope is a new file in that same repo. Single-repo change.

    Dependencies

    • Hard dep: Scaffolding ticket forgejo_admin/westside-admin#6 (board item #1087). The Dockerfile assumes package.json, npm run build, and a build/index.js output — all produced by #6. #1087 is also backlog; both can be reviewed in parallel but dev work on #1088 cannot start until #1087 lands.
    • Downstream: Woodpecker CI ticket #8 (board item #1094) and pal-e-deployments overlay #133 (board item #1095) consume the image this Dockerfile produces. Image path harbor.tail5b443a.ts.net/forgejo_admin/westside-admin:<sha> matches the deployment manifest expectation in arch-deployment-westside-admin.
    • Documented in the ticket's ### Lineage section.

    Acceptance Criteria

    All AC are agent-verifiable:

    • docker build succeeds — runnable
    • docker run -p 3000:3000 serves on 3000 — runnable + curlable
    • Image size < 250MB — docker images output
    • No build secrets — docker history output
    • EXPOSE 3000 — Dockerfile inspection
    • HEALTHCHECK on GET /health returns 200 — docker inspect + curl

    Minor gap: Constraints require non-root user (UID 1000+), but no AC line item enforces it. Recommend adding an AC bullet so QA verifies via docker run ... id -u.

    Blast Radius

    • Reference Dockerfile ~/westside-app/Dockerfile is adapter-static + nginx, NOT adapter-node. The ticket acknowledges this fallback ("if it exists; otherwise mirror the SvelteKit adapter-node official guide"), but the dev agent should be steered to the adapter-node pattern explicitly to avoid copy-pasting the wrong reference. westside-contracts (also adapter-node) may be a closer pattern reference if it has a Dockerfile.
    • Pattern established here will be the template for any future SvelteKit adapter-node service. Worth getting right.
    • HEALTHCHECK requires a /health route to actually exist in the SvelteKit app. Scaffolding ticket #6 does not include a /health route. Either #6 needs to add it, or this Dockerfile's HEALTHCHECK will fail until a later ticket adds the route. Recommend adding a /health route placeholder to #6's scope OR softening the HEALTHCHECK requirement here.

    Decomposition Assessment

    2 new files, 6 AC, single repo, well-understood pattern. Estimated agent work: under 5 min. No decomposition needed.

    Recommendation

    Verdict is READY — the ticket can move backlog → todo as-is. The notes below are nice-to-haves for the dev agent, not blockers:

    • [BODY] Add AC line: "Container runs as non-root user (UID 1000+); verify with docker run --rm westside-admin:test id -u returns ≥ 1000." — closes the constraint→AC gap.
    • [BODY] Replace reference hint: westside-app uses adapter-static + nginx. Steer dev agent to the SvelteKit adapter-node official Dockerfile pattern (or westside-contracts if it has one) to avoid wrong-pattern copy-paste.
    • [SCOPE] Coordinate with scaffolding ticket #6: either add a /health route placeholder to #6's scope, or note here that the HEALTHCHECK will be smoke-tested only after a separate health-route ticket lands. Without this, the HEALTHCHECK AC will fail in isolation.

    None of these block READY. Dev agent can proceed once #6 merges.

  • Verdict: NEEDS_REFINEMENT

    Board item #1087 — forgejo_admin/westside-admin#6 — column: backlog (review gate: backlog→todo).

    Template Completeness

    Issue declares ### Type: Task, so reviewed against template-issue (base template with Scope, no File Targets).

    • [x] Type
    • [x] Lineage
    • [x] Repo
    • [x] User Story
    • [x] Context
    • [x] Scope
    • [x] Acceptance Criteria
    • [x] Test Expectations
    • [x] Constraints
    • [x] Checklist
    • [x] Related

    All required sections present for Task type. Body is clear, scope is well-bounded, anti-scope is explicit.

    Traceability

    • [x] story:admin-row-crud — story name confirmed on project page
    • [x] story note verified — story-westside-admin-admin-row-crud exists, listed in project-westside-admin user-stories table
    • [ ] arch:sveltekit-server — no matching arch note exists. Project page lists exactly three arch notes: arch-domain-westside-admin, arch-dataflow-westside-admin, arch-deployment-westside-admin. The label arch:sveltekit-server does not correspond to any of them. The issue body's Related section actually points to arch-deployment-westside-admin and arch-dataflow-westside-admin, which suggests the correct label is arch:deployment-westside-admin (or arch:dataflow-westside-admin).
    • [x] Forgejo issue — #6 open, valid, well-formed
    • [x] type:infra — appropriate for scaffolding work

    Board-wide observation (not blocking this ticket): 10 other items on board-westside-admin use component-style arch labels (arch:scopeddb-helper, arch:keycloak, arch:hooks-server, arch:page-server, arch:postgres-grants, arch:woodpecker-ci, arch:deployment) that also have no backing arch notes. Either the convention here is "label = arch component (sub-element of an arch diagram)" — in which case the three diagram notes are the universe of arch notes and labels point INTO them — or these labels need 7+ new arch notes. This is a project-level scoping decision, not a fix for this single ticket. Flagging for Ava.

    File Targets

    N/A — Type is Task. Scope section enumerates files to be CREATED in an empty repo (forgejo_admin/westside-admin, README only). All paths are conventional SvelteKit scaffolding paths and don't need to be verified against the repo (it's empty by design).

    Repo Placement

    OK. Issue filed on forgejo_admin/westside-admin; scope is entirely in that repo. Does not touch pal-e-deployments, pal-e-platform, or basketball-api.

    Dependencies

    None upstream — this is the first foundational ticket per Lineage ("First foundational ticket for westside-admin. No parent issue."). The scope explicitly defers src/lib/server/db.ts (Drizzle), hooks.server.ts (Keycloak), Dockerfile, and .woodpecker.yaml to their own tickets — those tickets exist on the board (#1, #2, #7, #8 etc.) and are correctly downstream of this one. This ticket BLOCKS most/all other westside-admin repo tickets, which is correct sequencing.

    Acceptance Criteria

    Six AC, all agent-verifiable via shell commands (npm install, npm run check, npm run build, npm run dev, node build/index.js, lint/prettier). Concrete, testable, no ambiguity. Test Expectations adds a sensible build sanity check (node build/index.js responds 200 on /).

    Blast Radius

    None. New empty repo, no consumers. The only interaction with existing systems is the "match package versions to westside-app" constraint, which is advisory and easy to verify.

    Soft note on the constraint itself: westside-app uses adapter-static + plain Svelte + jsconfig.json (no TypeScript) + vite.config.js + no ESLint/Prettier config. This ticket requires TypeScript + ESLint + adapter-node. So "match package versions where applicable" really only applies to @sveltejs/kit (^2.16.0), svelte (^5.19.0), svelte-check (^4.1.4), vite (^6.1.0). Dev agent should be told this explicitly so they don't go hunting for ESLint config in westside-app and find nothing. Minor [BODY] tightening, not blocking.

    Decomposition Assessment

    6 AC, 1 repo, ~10 files of scaffolding, all conventional SvelteKit boilerplate. Estimated agent time: 5–10 minutes (mostly npm create svelte, configure adapter, add ESLint). Borderline on the 5-minute rule but the work is highly cohesive (one repo, one PR, one commit) and decomposing scaffolding into sub-tickets would create more overhead than it saves. No decomposition needed.

    Recommendation

    • [LABEL] Change arch:sveltekit-server to arch:deployment-westside-admin (matches arch-deployment-westside-admin note that the issue's Related section actually links to). OR adopt a component-label convention project-wide and create the matching arch notes — this is a project-level call for Ava, not a per-ticket fix.
    • [BODY] (minor) In Constraints, narrow "Match package versions to forgejo_admin/westside-app" to specifically: @sveltejs/kit ^2.16.0, svelte ^5.19.0, svelte-check ^4.1.4, vite ^6.1.0. Note that westside-app uses adapter-static and has no TypeScript or ESLint config, so those areas have no reference to match.
    • [SCOPE] (board-wide, not this ticket) Decide on arch label convention for board-westside-admin. Either (a) labels reference the three diagram notes only (arch:domain-westside-admin, arch:dataflow-westside-admin, arch:deployment-westside-admin) and component-level grouping happens inside those notes, or (b) create component-level arch notes for the 7 currently-orphaned labels. Affects 10 other board items.

    Once the arch label is corrected (single-line fix on the board item) this ticket is READY. Body refinement is nice-to-have, not blocking.

Architecture 4
  • Deployment: westside-admin arch-deployment-westside-admin

    Deployment: westside-admin

    Diagram

    graph TB
        subgraph Internet[Public Internet]
            User[Admin Browser]
        end
    
        subgraph TS[Tailscale Funnel]
            Funnel[westside-admin.tail5b443a.ts.net]
        end
    
        subgraph K3S[k3s cluster]
            subgraph WA[ns: westside-admin]
                Ing[Ingress: tailscale + funnel]
                Svc[Service: westside-admin :3000]
                Pod[Pod: westside-admin SvelteKit adapter-node]
            end
            subgraph BAPI[ns: basketball-api]
                PG[(Postgres: basketball db)]
            end
            subgraph KC[ns: keycloak]
                KCPod[Keycloak: westside-basketball realm]
            end
        end
    
        User --> Funnel
        Funnel --> Ing
        Ing --> Svc
        Svc --> Pod
        Pod -- DATABASE_URL --> PG
        Pod -- OIDC token + JWKS --> KCPod
        User -. Keycloak login redirect .-> KCPod
    

    Components

    Component arch: label Purpose Notes
    Tailscale Funnel arch:tailscale-funnel Public hostname + TLS Annotation tailscale.com/funnel: "true". Hostname westside-admin.tail5b443a.ts.net.
    Ingress (tailscale) arch:k8s-deploy Routes traffic to service ingressClassName: tailscale. tls.hosts[0]: westside-admin.
    Service arch:k8s-deploy ClusterIP for the pod Port 3000 → containerPort 3000.
    Deployment arch:k8s-deploy SvelteKit adapter-node container Image: harbor.tail5b443a.ts.net/westside-admin/app:<sha> (Harbor project = service key per feedback_harbor_project_naming; NOT forgejo_admin/...). Single replica v1.
    Postgres (basketball-api/postgres) arch:postgres Data source Cross-namespace via service DNS: postgres.basketball-api.svc.cluster.local:5432. Pod connects as dedicated admin_app user.
    Keycloak arch:keycloak Identity provider Same realm/client used by westside-app + westside-contracts. New OIDC client westside-admin.
    k8s Secret: westside-admin-secrets arch:k8s-deploy Holds DATABASE_URL, KEYCLOAK_CLIENT_SECRET, COOKIE_SIGNING_KEY SOPS-encrypted in pal-e-deployments.
    Harbor registry arch:harbor Container image storage Project name = westside-admin (matches service key per feedback_harbor_project_naming). Provisioned by pal-e-services terraform onboarding (forgejo_admin/pal-e-services#64).
    Woodpecker CI arch:ci-pipeline Build + push pipeline Builds on push to main, tags by commit sha, updates kustomize image tag.
    ArgoCD arch:k8s-deploy Continuous deployment Watches pal-e-deployments, syncs westside-admin overlay.

    Key Decisions

    • Public funnel chosen over tailnet-only. User accessibility (Lucas + Marcus from any device, no Tailscale install required for Marcus). Per feedback_funnel_requires_auth, this is permitted only because Keycloak admin role gate is airtight + documented.
    • Separate namespace. westside-admin gets its own ns rather than sharing basketball-api. Cleaner RBAC, cleaner network policy, cleaner ArgoCD app boundary.
    • Cross-namespace Postgres access. Pod connects to postgres.basketball-api.svc.cluster.local. Network policy explicitly allows this egress.
    • Separate DB user, not the basketball superuser. Pod uses a dedicated admin_app Postgres role with read+write grants only (no CREATE/ALTER/DROP). Limits blast radius if pod is compromised. Provisioned by forgejo_admin/pal-e-platform#302 via k8s Job.
    • SvelteKit adapter-node. Drizzle requires server runtime; adapter-static is incompatible. Container runs node build/index.js.
    • Image registry follows feedback_harbor_project_naming. Harbor project = service key = image repo prefix. westside-admin uses Harbor project westside-admin, image path harbor.tail5b443a.ts.net/westside-admin/app:<sha>. Owner namespace (forgejo_admin) is NOT part of the path. This convention came from the 36-hour outage cited in feedback_harbor_project_naming.
    • Service onboarding via pal-e-services terraform. Harbor project + CI robot account are provisioned declaratively by an entry in pal-e-services/terraform/k3s.tfvars (forgejo_admin/pal-e-services#64). NOT created out-of-band.
    • Single replica v1. No HPA. Admin tool has low concurrent users. Add HPA when more than one admin uses it simultaneously and load matters.
    • PSA-restricted namespace. Namespace labeled pod-security.kubernetes.io/enforce: restricted. Deployment overlay supplies pod-level + container-level securityContext per forgejo_admin/pal-e-deployments#141. Dockerfile uses USER node (UID 1000) so the manifest assertion holds at runtime. System-wide PSA hardening tracked in forgejo_admin/pal-e-deployments#140.
    • Hybrid Secret ownership (post-2026-05-03 convention). App-level Secrets (KEYCLOAK_CLIENT_SECRET, COOKIE_SIGNING_KEY, DATABASE_URL bundled into westside-admin-secrets) are SOPS-encrypted in pal-e-deployments and applied by ArgoCD via the CMP plugin. Harbor pull credentials (harbor-creds) are terraform-managed by pal-e-services per sop-harbor-creds-migration. Salt pillars handle pre-cluster secrets (Tailscale OAuth, Harbor admin, GPG keys). Three layers, three owners — different lifecycle for different concerns. Origin: forgejo_admin/pal-e-deployments#143 (first migration) and #144 (platform-wide rollout).
    • arch-domain-westside-admin — sibling: what entities
    • arch-dataflow-westside-admin — sibling: how data flows
    • arch-scoped-db-westside-admin — sub-component: tenant-scoping query layer
    • project-westside-admin — parent project page
    • sop-network-security — network policy rules
    • feedback_harbor_project_naming — image registry convention (Harbor project = service key, NOT Forgejo owner)
    • feedback_funnel_requires_auth — funnel safety rule
  • scopedDb helper: westside-admin arch-scoped-db-westside-admin

    scopedDb helper: westside-admin

    Sub-component focus on the scopedDb / systemDb query layer. Sits inside arch-dataflow-westside-admin as the Postgres access seam — every server route reads/writes through this helper, never directly through the unscoped Drizzle db.

    Diagram

    graph LR
        PageServer["+page.server.ts"]
        Action["form action"]
        ScopedDb["scopedDb (tenant-scoped builder)"]
        SystemDb["systemDb (no tenant scoping)"]
        Db["db (unscoped, @internal)"]
        PG["Postgres: basketball db"]
        Tenant["tenant.ts: TENANT_ID = 1"]
    
        PageServer --> ScopedDb
        Action --> ScopedDb
        ScopedDb -- "auto-inject WHERE tenant_id = ?" --> Db
        ScopedDb -- reads --> Tenant
        SystemDb -- "alembic_version, player_teams" --> Db
        Db --> PG
    

    Components

    Component arch: label Purpose Notes
    scopedDb arch:scoped-db Tenant-scoped query builder. Auto-injects WHERE tenant_id = TENANT_ID for every select/update/insert. The ONLY exported DB entry point for tenant-scoped tables. Lives in src/lib/server/scopedDb.ts.
    systemDb arch:scoped-db Unscoped builder for system tables that have no tenant_id column. Compile-time error if used on a tenant-scoped table. Tables: alembic_version, player_teams.
    db (internal) Raw Drizzle client. Marked @internal; only scopedDb.ts may import. Lint rule + CI grep enforce single entry point.
    tenant.ts Source of TENANT_ID. v1: hardcoded constant 1. Documented swap point for multi-tenant future (read from JWT claim instead).
    Drizzle schema.ts Type information consumed by both helpers. Generated by drizzle-kit pull. The TS types tell scopedDb which tables have tenant_id.

    Key Decisions

    • Single entry point, lint-enforced. Any import { db } from '$lib/server/db' outside scopedDb.ts fails CI. Prevents the "forgot the WHERE clause" class of bug at the type system level rather than at code review level.
    • Type system distinguishes tenant tables from system tables. Misusing scopedDb.select(systemTable) or systemDb.select(tenantTable) is a TypeScript error, not a runtime guard. Drizzle's column metadata makes this provable at compile time.
    • tenant_id sourced from constant, not JWT. v1 has one tenant. The constant + helper makes the future swap to claim-sourced trivial — change one file, not 50 call sites.
    • Helper composes with arbitrary where/and/or. Tenant scoping is added via and(eq(tenant_id, TENANT_ID), ...userClauses) — not a string concat. Drizzle's typed builders make this safe.
    • This is a deliberate sub-component. The helper is logically part of the dataflow described in arch-dataflow-westside-admin, but its surface area (two exports, one lint rule) is large enough to warrant its own arch note. Tickets touching this layer carry arch:scoped-db.

    Implementation Tickets

    • forgejo_admin/westside-admin#1 (board #1089) — creates the unscoped db, generates schema.ts
    • forgejo_admin/westside-admin#3 (board #1091) — creates scopedDb + systemDb + lint rule + tenant.ts
    • arch-dataflow-westside-admin — parent dataflow note (this is the DB-access seam within it)
    • arch-domain-westside-admin — table inventory; tenant_id presence drives scope decisions
    • project-westside-admin — project page
    • story-westside-admin-admin-row-crud — driving user story (this helper is a Safety Constraint guarantee)
    • convention-architecture-ids — labeling convention
  • Data Flow: westside-admin arch-dataflow-westside-admin

    Data Flow: westside-admin

    Diagram

    Flow 1: Authenticated page load

    sequenceDiagram
        participant B as Browser
        participant SK as SvelteKit Server
        participant H as hooks.server.ts
        participant KC as Keycloak (westside-basketball realm)
        participant DB as Postgres (basketball db)
    
        B->>SK: GET /players
        SK->>H: handle(event)
        H->>H: read session cookie (HttpOnly)
        alt no cookie
            H-->>B: 302 to Keycloak /authorize (PKCE)
            B->>KC: GET /authorize
            KC-->>B: SSO via existing session OR login form
            B->>SK: GET /auth/callback?code=...
            SK->>KC: POST /token (code exchange)
            KC-->>SK: access_token + refresh_token + id_token
            SK->>SK: set HttpOnly Secure SameSite=Lax cookie
        end
        H->>KC: validate JWT signature (JWKS, cached TTL)
        H->>H: parse realm_access.roles
        alt no admin role
            H-->>B: 403 page
        end
        H->>H: locals.user = {sub, email, tenant_id: 1}
        SK->>SK: +page.server.ts load()
        SK->>DB: scopedDb.select().from(players)
        DB-->>SK: typed rows
        SK-->>B: SSR HTML
    

    Flow 2: Row mutation via form action

    sequenceDiagram
        participant B as Browser
        participant SK as SvelteKit Server
        participant DB as Postgres
    
        B->>SK: POST /players/123 (form-encoded)
        SK->>SK: hooks.server.ts validates JWT (same as Flow 1)
        SK->>SK: action handler validates input via Drizzle column metadata
        SK->>DB: BEGIN
        SK->>DB: UPDATE players SET ... WHERE id=123 AND tenant_id=1
        SK->>DB: INSERT INTO contract_audit_log (actor, old_state, new_state)
        SK->>DB: COMMIT
        SK-->>B: 303 redirect to row view
        B->>SK: GET /players/123 (re-fetches via Flow 1)
    

    Components

    Component Purpose Notes
    Browser Renders SSR HTML, submits forms No JS-accessible tokens. No keycloak-js.
    SvelteKit Server (adapter-node) Receives requests, runs load()/actions Single Node process per pod.
    hooks.server.ts Auth gate on every request Validates JWT, populates event.locals.
    Keycloak Identity provider OIDC code+PKCE flow. JWKS cached with TTL.
    Postgres basketball db Connection pool via pg. tenant_id scoping enforced by helper.
    scopedDb helper Tenant-scoped query builder Wraps Drizzle db; auto-injects tenant_id WHERE clause.
    +page.server.ts Server-only data loading + actions The seam. Imports Drizzle here only.
    +page.svelte Renders typed PageData Cannot import from $lib/server/* (compile error).

    Key Decisions

    • JWT validated on EVERY request. JWKS is cached, JWT is not. Trades CPU for security against revocation lag.
    • HttpOnly cookies, no Bearer in browser. Funnel exposure forces this. Tokens never reach JS — no XSS exfiltration path.
    • tenant_id sourced from constant, not claim. v1 single-tenant; constant + helper makes the future swap to claim-sourced trivial.
    • Mutations use SvelteKit form actions, not REST endpoints. Form submission semantics, progressive enhancement, ActionResult typing.
    • Audit row written in same transaction as data mutation. Either both succeed or both fail. No mutations without audit trail.
    • Server-side token refresh. Browser never knows tokens exist; refresh happens transparently when within 30s of expiry.
    • arch-domain-westside-admin — sibling: what entities flow
    • arch-deployment-westside-admin — sibling: where the flow lives
    • project-westside-admin — parent project page
    • feedback_funnel_requires_auth — drives the cookie SSR choice
  • Domain Model: westside-admin arch-domain-westside-admin

    Domain Model: westside-admin

    Diagram

    erDiagram
        TENANT ||--o{ PARENT : has
        TENANT ||--o{ COACH : has
        TENANT ||--o{ TEAM : has
        TENANT ||--o{ EVENT : has
        PARENT ||--o{ PLAYER : "parent_of"
        PLAYER }o--o{ TEAM : "via player_teams"
        TEAM ||--o| COACH : "head_coach"
        PLAYER ||--o{ ORDER : has
        PARENT ||--o{ ORDER : has
        PRODUCT ||--o{ ORDER : "ordered_as"
        PLAYER ||--o{ REGISTRATION : has
        PLAYER ||--o{ CONTRACT_AUDIT_LOG : "audited"
        TOURNAMENT ||--o{ TOURNAMENT_PRODUCT : has
        TENANT ||--o{ SPONSOR : has
        TENANT ||--o{ EMAIL_LOG : has
        TENANT ||--o{ INTEREST_LEAD : has
        TENANT ||--o{ PRACTICE_SCHEDULE : has
        TENANT ||--o{ OUTBOX : has
    

    Components

    Component Purpose Notes
    tenants Multi-tenant root 6 cols. v1 hardcoded to id=1 (Westside).
    parents Parent/guardian contact 11 cols, ~99 rows. Holds waiver fields + magic-link registration_token (sensitive).
    coaches Coaching staff 16 cols. Stripe Connect onboarding state.
    players Player profile (widest table) 37 cols, ~58 rows. 6 enum fields, jsonb contract_overrides.
    teams Team roster + config 10 cols, 7 rows. contract_config jsonb + GroupMe links.
    player_teams Junction table 2 cols. Many-to-many player↔team.
    events Calendar entries 13 cols. Self-ref via parent_event_id for recurring events.
    tournaments Tournament definitions 6 cols.
    tournament_products Tournament↔product↔team junction 5 cols.
    practice_schedules Recurring practice slots 12 cols.
    products Stripe products catalog 10 cols. custom_fields jsonb.
    orders Stripe checkout records 12 cols, 85 rows.
    registrations Tryout/signup payments 12 cols, 50 rows.
    email_log Outbound email audit 8 cols, 609 rows (largest table).
    interest_leads Marketing capture 8 cols.
    sponsors Sponsor CRM 13 cols, 44 rows.
    oauth_tokens Third-party API tokens 7 cols. token_data jsonb is SENSITIVE — hide from UI.
    password_reset_tokens Reset flow tokens 6 cols. token field SENSITIVE.
    contract_audit_log Contract state diffs 8 cols. old_state/new_state jsonb.
    jersey_public_orders Public jersey intake 19 cols. UUID PK (only one).
    outbox Outbox event pattern 7 cols.
    alembic_version Migration tracking 1 col. EXCLUDE from admin UI.

    Key Decisions

    • Live-Postgres-as-source-of-truth. Domain comes from drizzle-kit pull, not a hand-maintained TS file. Ensures admin tool never lies about reality.
    • tenant_id is universal but not enforced at DB level. Most tables have it; the application layer enforces scoping via the query helper. This is a known limitation worth a future RLS upgrade.
    • jersey_public_orders uses UUID PK while all others use serial integer. Inherited from public-submission flow needing unguessable IDs. Admin UI must handle both PK shapes.
    • Sensitive columns identified upfront for visible-columns-config. Tokens (oauth_tokens.token_data, password_reset_tokens.token, parents.registration_token, players.contract_token) hidden by default.
    • alembic_version excluded from admin UI. Migration tracking is not user data; editing it manually breaks alembic.
    • arch-dataflow-westside-admin — sibling: how data moves
    • arch-deployment-westside-admin — sibling: where it runs
    • project-westside-admin — parent project page
    • arch-domain-westside-basketball — same DB, modeled from basketball-api side
User Story 1
  • Story: Admin Row CRUD story-westside-admin-admin-row-crud

    story: Admin Row CRUD

    Role

    Admin (Lucas primarily, Marcus occasionally)

    Key

    admin-row-crud

    Want

    As an admin, I want to view and edit any row in the basketball database through a web UI gated by my existing Keycloak admin role

    So That

    So that I can fix data issues directly — without running raw UPDATEs in psql (banned per feedback_never_write_prod_db after the 16U Local Queens schedule wipe), and without waiting for a new admin route to be hand-built in westside-app every time a new data shape needs touching

    Acceptance Criteria

    • [ ] SSO from westside-app — opening westside-admin in same browser does not require second login form
    • [ ] Non-admins (no admin realm role) see a 403 page with logout button, never any data
    • [ ] Index page lists all visible tables in basketball db (sensitive tables hidden)
    • [ ] Click any table → paginated, type-aware grid of rows (50/page, filter inputs, enums as badges, dates formatted, jsonb expandable)
    • [ ] Click any row → editable form with type-correct inputs (text, number, date picker, enum dropdown, jsonb code editor, boolean toggle)
    • [ ] Save → row updates in Postgres, audit trail row written in same transaction
    • [ ] tenant_id scoping enforced in every query — URL tampering can't leak other tenants' rows
    • [ ] Failed save shows Postgres error inline next to offending field

    Success Metric

    • Lucas: zero kubectl exec ... psql sessions for UPDATE statements over a 30-day window after launch
    • Marcus: zero "Lucas, can you fix X for me?" messages for routine data corrections over a 30-day window

    Out of Scope (v1)

    • INSERT (creating new rows) — too risky without a row-template convention
    • DELETE — too risky without a tombstone pattern
    • Bulk multi-row edit
    • Cross-table joins / custom queries
    • Any user role besides admin

    Safety Constraints (not separate stories — guarantees on this story)

    • Keycloak admin gate. Cookie SSR auth, JWKS validation per request, HttpOnly token cookie, public funnel safe per feedback_funnel_requires_auth.
    • Tenant scoping. Query helper auto-injects WHERE tenant_id = ?; v1 hardcoded TENANT_ID=1; direct unscoped db not exported.
    • Schema introspection. Drizzle schema generated from live Postgres via drizzle-kit pull; CI fails on drift.

    What This Story Replaces

    Today After westside-admin
    Lucas sshs to archbox, kubectl exec into postgres pod, runs UPDATE statements by hand Lucas opens westside-admin, edits row, saves
    Marcus texts Lucas to fix a player record Marcus opens westside-admin, fixes it himself
    Building a custom /admin/X/edit route in westside-app every time a new admin need arises Generic row editor handles it for free
    • arch-domain-westside-admin — entities being read/written
    • arch-dataflow-westside-admin — auth + page load + mutation flows
    • arch-deployment-westside-admin — deployment topology
    • project-westside-admin — parent project page
    • board-westside-admin — project board
    • feedback_never_write_prod_db — driving constraint (no more raw psql UPDATEs)
    • feedback_funnel_requires_auth — driving constraint (admin gate must be airtight)
Board 1
Repos 1
  • westside-admin
    active