westside-admin
Notes
Project Page 1
-
westside-admin
project-westside-adminwestside-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-nodeapp so Drizzle queries run server-side and rows flow into components as typed data. Replaces direct psql access (banned perfeedback_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-crudstory-westside-admin-admin-row-crudAdmin (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.- Keycloak admin gate. Cookie SSR auth via
westside-basketballrealm. JWKS validation per request. HttpOnly + Secure + SameSite=Lax token cookie — no Bearer in browser. Public Tailscale funnel permitted only because gate is airtight perfeedback_funnel_requires_auth. - Tenant scoping. Query helper wraps Drizzle
db, auto-injectsWHERE tenant_id = ?for tenant-scoped tables. v1: hardcodedTENANT_ID = 1. Direct unscopeddbnot exported. Lint or CI rule prevents bypass. - 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
arch-domain-westside-admin— Domain Model (22 entities, relationships, sensitive columns)arch-dataflow-westside-admin— Data Flow (auth + page load + mutation sequences)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
playerslist+edit; introspection-driven generic browser is the second iteration after the players seam ships
Board
Primary kanban:
board-westside-adminColumns: Backlog → Todo (review gate) → Next Up → In Progress → QA → Done. Every ticket carries
story:admin-row-crud+ anarch:label + atype:label pertemplate-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-admincreated (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 (PSAsecurityContext,pal-e-deployments#141) → Layer 3 (harbor-creds source-of-truth migration from SOPS-overlay to terraform,pal-e-deployments#145+ targetedtofu apply). Deployment 1/1 ready. ArgoCD Synced + Healthy. External funnelhttps://westside-admin.tail5b443a.ts.netreturns 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. Keycloakwestside-adminconfidential OIDC client created inwestside-basketballrealm via Playwright-driven admin console flow persop-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 perreview-1132-2026-05-03(5-min rule violation):#14keycloak.ts lib,#15hooks.server.ts,#16auth endpoints,#17403 page.#14merged 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):#2CLOSED — auth foundation 4/4 shipped. All 4 decomposed sub-tasks landed onmain:#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(extendKeycloakTokenswithrefresh_expso cookie Max-Age tracks refresh window instead of access window). Standing scope:arch-keycloak+arch-westside-admin+arch-dev-overlaynotes 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 fromvalidationtodoneperfeedback_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 onboard-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 livewestside-adminclient under tofu management (was created out-of-band in the realm) and addedhttps://westside-admin-dev.tail5b443a.ts.netas a second redirect URI + web origin; required schema extensionuse_refresh_tokens(optional, defaultfalse, additive — verified zero-drift on 4 sibling clients) because the live client carries the Keycloak defaulttruewhile the existing schema hardcodedfalse; targetedtofu applyran post-merge — admin-API GET confirms both prod + dev URIs/origins live.pal-e-deployments#148/PR #149 addedoverlays/westside-admin/dev/mirroringpal-e-dictionary/dev/with three intentional deviations (port 5179, no funnel, PSA-restricted securityContext); 3 review passes caught a port collision withpal-e-projects-dev, the PSA admission requirement, and the ArgoCD path mismatch (the existing Application sourcesprod/only; dev is intentionally manual-apply). Pending: manualkubectl apply -k overlays/westside-admin/dev/from archbox + Lucas e2e validation asdraneylucas@gmail.com.
2026-05-05:#1CLOSED — Drizzle ORM foundation merged (PR #30). Drizzle ORM setup complete: schema definitions, db client, drizzle.config.ts, migration infrastructure. Issuewestside-admin#1closed. 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:#19CLOSED — CI vitest gate merged (PR #32). Woodpecker test step now runsnpm ci && npm testbefore kaniko build on every push. All future PRs are test-gated.
2026-05-05:#3CLOSED — 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:#4CLOSED — 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, Postgresadmin_appuser + grants on basketball dbYes — 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 - Keycloak admin gate. Cookie SSR auth via
Doc 24
-
Review: westside-admin#29 (board 1146) — Edit route + audit — 2026-05-04 (v2, APPROVED)
review-1146-2026-05-04-v2Scope 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
- 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
- AC line:
old_state/new_stateshape. Pinned in Constraints: "FULL ROW snapshots, not diffs. jsonb of every column onplayers(excludingcontract_token) at the moment of UPDATE — both before and after." Removes ambiguity for the dev agent.arch-page-serverbacking note gap. Documented in Related asKNOWN 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-crudlabel — story notestory-westside-admin-admin-row-crudverified onproject-westside-admin#user-storiestable. - [x]
arch:page-serverlabel — backing notearch-page-serverstill missing in pal-e-docs (shared gap with #4, board item 1092). Behavior IS documented inarch-dataflow-westside-adminFlow 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:5labels present.
File Targets
- [x]
~/westside-admin/src/routes/players/[id]/+page.server.ts— verified greenfield (noplayers/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 onlykeycloak.ts+keycloak.test.ts). - [x] Modify
~/westside-admin/src/routes/players/+page.svelte— file ships in #4 (board item 1092, currently intodo); 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### Repodeclaration 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.sveltewe mutate to make rows clickable) - #28 (1145) — backlog (input components — sibling, parallel)
Cannot advance past
todountil 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_tokengrep AC is testable via integration test (response body assertion) — both AC and test are stated.monthly_feedollars-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
playerstable. Audit log targetcontract_audit_logis 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 signaturewriteAudit({ 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.tswith no+page.svelteis 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 → todomove. Cannot advance tonext_up/in_progressuntil #1, #3, #4 (and ideally #28) clear — that's a downstream concern, not a refinement gap.Related
- 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
- GET-side leak check. Body now contains two matching artifacts:
-
Review: westside-admin#4 (board 1092) — Players list (v2) — 2026-05-04 APPROVED
review-1092-2026-05-04-v2Verdict: 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 livepublic.playerstable. No further refinements required — clear to advance fromtodotonext_up.Headline
v2 reconciliation is complete. All ten allowlist column names exist on the live schema with the data types the ticket claims;
monthly_feerendering 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.columnsviakubectl -n basketball-api exec postgres-9b5b87b5-5nccx -- psql -U basketball -d basketball. Result:public.playershas 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 idinteger NO integer, NOT NULL OK 2 namecharacter varying NO varchar, NOT NULL OK 3 divisionUSER-DEFINED YES USER-DEFINED enum, nullable OK 4 positioncharacter varying YES varchar, nullable OK 5 jersey_numbercharacter varying YES varchar, nullable OK 6 jersey_sizeUSER-DEFINED YES USER-DEFINED enum, nullable OK 7 contract_statusUSER-DEFINED NO USER-DEFINED enum, NOT NULL OK 8 subscription_statusUSER-DEFINED NO USER-DEFINED enum, NOT NULL OK 9 monthly_feeinteger YES integer DOLLARS, nullable OK 10 created_attimestamp 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 frompublic.playersand 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-crudlabel — present on board item 1092 - [x] Story note verified —
story-westside-admin-admin-row-crudexists, 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-adminuser-stories section (table-4000) - [x]
arch:page-serverlabel — present on board item 1092 - [~] Arch note — no standalone
arch-page-servernote exists, but this is consistent with the rest of board-westside-admin (no per-componentarch-Xnotes forscoped-db,hooks-server,keycloak,harboreither). Thearch:page-serverlabel references the page-server component withinarch-dataflow-westside-adminFlow 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— parentsrc/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— parentsrc/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 toarch: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 toin_progressuntil #1 + #3 reach at minimumqa. 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 (nocontract_tokenin 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 toparentsorteamsin v1, which keeps the auth/scoping seam single-table and matches thescoped-dbdesign (#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 fromtodotonext_uponce its hard-dep chain (#1, #3) clears.Related
- 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
-
Review: westside-admin#28 (board 1145) — Input components — 2026-05-04 v2 (APPROVED)
review-1145-2026-05-04-v2Scope 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)
- Test approach pivoted to pure helpers. Body now extracts
parseJsonOnBlur,isoToDateInput,dateInputToIsointosrc/lib/components/inputs/helpers.tsand tests them inhelpers.test.ts. Verified achievable:vitest.config.tsat~/westside-admin/vitest.config.tssetsenvironment: 'node';package.jsonhas neither@testing-library/sveltenorjsdom/happy-dom. The existingsrc/lib/server/keycloak.test.tsis 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). - #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 (noschema.tsordbimports). - Contradictory
/dev/inputschecklist 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-adminuser-stories table (block 35117) - [x] story note —
story-westside-admin-admin-row-crudentry present - [ ] arch:svelte-components — label present, but
arch-svelte-componentsnote 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 vials ~/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 matchessrc/lib/server/keycloak.test.ts(vitest, node env) - [x] Convention checks:
+layout.svelteuseslet { 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]Createarch-svelte-componentsarchitecture note in pal-e-docs as a separate ticket — shared gap with #29.Related
- 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
- Test approach pivoted to pure helpers. Body now extracts
-
Review: westside-admin#29 (board 1146) — Edit route + audit — 2026-05-04 (v1, NEEDS_REFINEMENT minor)
review-1146-2026-05-04Scope 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
[BODY]GET-side leak check. Added explicit AC + integration test thatcontract_tokenvalue never appears in/players/:idHTML body. Existing WRITE-side guard (form excludes the column) was correct but didn't enforce READ-side.[BODY]old_state/new_stateshape. Pinned as full-row snapshots, not diffs, in Constraints. Removes ambiguity for the dev agent.[SCOPE — non-blocker]arch-page-serverbacking note doesn't exist in pal-e-docs. Shared gap with #4 (board item 1092). Filed as a separate Ava task; thin pointer toarch-dataflow-westside-adminFlow 2 should suffice.
Other findings
- Template completeness: all 11
template-issue-featuresections present. - Traceability:
story:admin-row-crudbacking note exists.arch:page-serverbacking absent (gap above). Behavior IS documented inarch-dataflow-westside-adminFlow 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
todountil 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-ticketagent for confirmation; expect quick turnaround to APPROVED. Once APPROVED, filereview-1146-2026-05-04-v2note.Related
- Forgejo issue:
forgejo_admin/westside-admin#29 - Board item: 1146 on
board-westside-admin
-
Review: westside-admin#28 (board 1145) — Input components — 2026-05-04 (v1, NEEDS_REFINEMENT)
review-1145-2026-05-04Scope 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
- 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, nojsdom/happy-dominpackage.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. - 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. - Self-contradicting checklist. Body said "Do NOT create routes" but Checklist asked for a
/dev/inputsdev 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-componentsnot present — shared gap, see #29 review). - Label-vs-slug nit (
story:admin-row-crudvsstory-westside-admin-admin-row-crud) — not a refinement, both forms valid perfeedback_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-ticketagent. Once APPROVED, filereview-1145-2026-05-04-v2note.Related
- Forgejo issue:
forgejo_admin/westside-admin#28 - Board item: 1145 on
board-westside-admin
- Test infrastructure gap (the dominant scope risk). Repo's vitest stack today is node env only (
-
Review: westside-admin#4 (board 1092) — Players list — 2026-05-04 (v1, NEEDS_REFINEMENT)
review-1092-2026-05-04Scope 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
playersschema in three places. Three of the ten allowlisted columns (email,team_name,updated_at) do not exist as columns on theplayerstable.Three columns broken in v1 allowlist
email— lives onparents.email, notplayers. Rendering requires JOIN, contradicts the constraintscopedDb.select(allowlistedColumns).from(players).team_name— does not exist onplayers. Teams link viaplayer_teamsM2M toteams.name. Needs JOIN + semantic decision for multi-team players.updated_at— does not exist onplayers. Onlycreated_at.
Other findings
- Live DB verified: 37 columns confirmed (37th is
jersey_number_preferred), 58 rows,contract_tokenpresent (sensitive),division+contract_statusare USER-DEFINED enums,monthly_feeis 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: droppedemail/team_name, swappedupdated_at→created_at, addedposition+subscription_status.[BODY]monthly_feeprecision: AC said$NN.NNbut 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-ticketagent. Once APPROVED, filereview-1092-2026-05-04-v2note.Related
- Forgejo issue:
forgejo_admin/westside-admin#4 - Board item: 1092 on
board-westside-admin
-
Review: westside-admin#3 (board 1091) — Tenant-scoped query helper — 2026-05-04
review-1091-2026-05-04Scope 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-featuresections present. - File targets verified:
tenant.ts,scopedDb.tsnot yet in~/westside-admin/src/lib/server/. Repo scaffolding (#6) closed. - Traceability:
story:admin-row-crud+arch:scoped-dblabels 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
[BODY]Anchor AC #3 (TypeScript distinguishes tenant vs. system tables) to a concreteexpectTypeOftest againstalembicVersion— prevents the yak-shaving risk PM flagged.[BODY]Addeslint.config.jsto File Targets > Modify alongsidepackage.json— canonical enforcement for the AC #4 lint check isno-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.
Related
- Forgejo issue:
forgejo_admin/westside-admin#3 - Board item: 1091 on
board-westside-admin
- Template completeness: all
-
Review: westside-admin#1 (board 1089) — Drizzle setup — 2026-05-04
review-1089-2026-05-04Scope Review: westside-admin#1 (board item 1089) — Drizzle setup + drizzle-kit pull schema.ts
Verdict
READY (equivalent to APPROVED for the
todo → next_upgate). 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.tsall absent;src/lib/server/contains onlykeycloak.ts+keycloak.test.ts(per #6);scripts/exists with.mjshelpers. - Live DB shape matches ticket claims: 22 tables, 18 enums, 1 row in
tenants, 58 rows inplayers, 37 columns onplayers,contract_tokenpresent,tenant_idon 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 inwestside-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.
Related
- Forgejo issue:
forgejo_admin/westside-admin#1 - Board item: 1089 on
board-westside-admin - Skill:
skill-review-ticket - Template:
template-review
-
Validation: westside-admin Keycloak client + SOP (pal-e-platform#301) — 2026-05-04
validation-301-2026-05-04Validation: 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-adminid=1096 - Shipped: OIDC client
westside-adminexists in realmwestside-basketball; SOPsop-keycloak-client-creationexists in pal-e-docs.
Environment
- Keycloak:
https://keycloak.tail5b443a.ts.net, realmwestside-basketball - Consuming app:
https://westside-admin.tail5b443a.ts.net(running off imageharbor.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-adminexists in realmwestside-basketballHit Keycloak admin API GET /admin/realms/westside-basketball/clients?clientId=westside-adminPASS API returned 1 client with id 19bfe0df-7fbc-463c-97df-59d77901421e2 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 withunauthorized_client | Client not allowed for direct access grants3 Valid Redirect URI https://westside-admin.tail5b443a.ts.net/auth/callbackInspect 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_SECRETwired into deploymentkubectl -n westside-admin get secret westside-admin-secrets -o jsonPASS Secret has 5 keys including KEYCLOAK_CLIENT_SECRET; pod env consumes it; live login proves cluster-side credential is valid5 SOP note sop-keycloak-client-creationexists in pal-e-docsget_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,activeSOP 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-apppattern). 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
doneon this board will go through the validation column live to avoid retroactive filings. - Forgejo:
-
Validation: westside-admin#24 — cookie size fix
validation-24-2026-05-03Ticket
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-03check #4 — Keycloak/logoutredirect still fires;id_token_hintparam 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 invalidation-15). - URL:
-
Validation: westside-admin#17 — (unauthorized) 403 page
validation-17-2026-05-03Ticket
forgejo_admin/westside-admin#17(PR #23, merged). The (unauthorized) route group renders a 403 page when an authenticated user lacks theadminrealm 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.jspresent 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.
- URL:
-
Validation: westside-admin#16 — /auth/login + /auth/callback + /auth/logout
validation-16-2026-05-03Ticket
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/callback7 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.
- URL:
-
Validation: westside-admin#15 — hooks.server.ts admin role gate
validation-15-2026-05-03Ticket
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, populatesevent.locals.user, and enforces theadminrealm 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 includeadmin)
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 cookieVerdict
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.
- URL:
-
Validation: Migrate westside-admin harbor-creds SOPS to terraform (#143)
validation-143-2026-05-03Verdict: 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 targetedtofu applyagainstpal-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 commita6a9913) - Harbor robot:
robot$westside-admin+westside-admin-pull(id=384, project-scoped towestside-adminid=38) - Validation run: 2026-05-03 by Dottie
Tier
Per
sop-validation, ticket has labelsarch: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.yamlno longer referencesharbor-creds.enc.yamlinresources:Forgejo API GET on kustomization.yaml@mainPASS resources:now lists onlynamespace.yaml,../../../bases/standard,ingress.yaml,westside-admin-secrets.enc.yaml— no harbor-creds reference2 overlays/westside-admin/prod/harbor-creds.enc.yamldeleted from repoForgejo API GET on the file path at ref=mainPASS 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/384PASS Returns name=robot$westside-admin+westside-admin-pull,kind=project,namespace=westside-admin, permissionsread artifact+pull repository,disable=false, no expiry4 kubectl get secret harbor-creds -n westside-admindockerconfigjson parses cleanly with valid credskubectl get secret harbor-creds -o jsonpath='{.data.\.dockerconfigjson}' | base64 -d | python3 -c json.toolPASS Parses as valid JSON. auths."harbor.tail5b443a.ts.net"containsusername=robot$westside-admin+westside-admin-pull,passwordlength=32 (real, not placeholder),authlength=100 (base64-encodedusername:password)5 kubectl get deployment westside-admin -n westside-adminshows 1/1 readykubectl get deployment westside-admin -n westside-admin -o widePASS READY=1/1, UP-TO-DATE=1, AVAILABLE=1, image=harbor.tail5b443a.ts.net/westside-admin/app:803e9439...6 Pod Running, no CreateContainerErrororImagePullBackOffon current podkubectl get pods+kubectl get events --sort-by=.lastTimestampPASS Pod westside-admin-5c96ffff7f-rhz84Status=Running, Ready=true, Restarts=0. Latest events showPulled successfully in 1.522s— the pre-fix events showingillegal base64/FailedToRetrieveImagePullSecretare on superseded ReplicaSets, not current pod7 /healthendpoint responds 200 inside clusterkubectl exec ... -- wget -qO- http://10.42.0.237:3000/healthPASS Returns {"status":"ok"}. Note: Alpine wget onlocalhosthits 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.netreturns app (not 502)curl -skI https://westside-admin.tail5b443a.ts.net/PASS HTTP 200in 11.7ms (was 502 since 2026-04-28)9 No-drift: targeted tofu planafter apply returns 0 changes for the harbor resourcescd ~/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=falsePASS Plan: 0 to add, 1 to change, 0 to destroy. The 1 change iskubernetes_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 met10 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 invar.servicesbut 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 strayargocd.argoproj.io/instancelabel. 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 inqapending 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:
- Layer 1 — CI validate step couldn't find
build/index.html(adapter-node, not adapter-static). Fixed bywestside-admin#13. Surfaced 40-zeros image tag. - Layer 2 — PSA admission rejected pod (no securityContext). Fixed by
pal-e-deployments#141. - 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.
-
Validation: Add PSA-restricted securityContext to westside-admin overlay (#137)
validation-137-2026-05-01Verdict: 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.yamlso the deployment is admitted underpod-security.kubernetes.io/enforce: restricted.
Board item:1118onboard-westside-admin(columnqa).
Merged PR: pal-e-deployments#141 — squash-merged 2026-05-01, merge commit2e10af964e473d342366ac1a76454613bb04dfc4.Environment
Cluster:
archbox(k3s, prod).
Namespace:westside-admin.
ArgoCD app:westside-admin(sync revision2e10af96, statusSynced, healthProgressing).
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 576eacackubectl 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)at2026-05-01T14:36:05Z.2 Rendered Deployment in cluster carries all 4 PSA-required fields at the pod level ( runAsNonRoot,runAsUser,runAsGroup,fsGroup) plusseccompProfile.type=RuntimeDefaultkubectl 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 referencedappwhile base usedwestside-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 phantomappsidecar.5 Post-merge ReplicaSet has zero FailedCreateevents from PSA admissionkubectl describe rs westside-admin-8786bffd6 -n westside-adminPASS Events on the new RS contain only SuccessfulCreate. All historicalFailedCreateevents from PSA (on stale RS566d4cc96d,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,/healthreturns 200kubectl get deployment;curl https://westside-admin.tail5b443a.ts.net/healthFAIL (out-of-scope) Deployment is 0/1, funnel returnsHTTP 502. Pod is stuck inWaitingwith reasonillegal base64 data at input byte 11— kubelet can't decode theauthfield in theharbor-credsdockerconfigjson because it's the literal placeholderPLACEHOLDER_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
restrictedenforcement 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
FailedCreatefrom 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
Syncedafter 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.yamlonly. 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-credsSecret still contains the literal placeholder string from pal-e-deployments bootstrap.- Symptom: kubelet rejects pod creation with
illegal base64 data at input byte 11when attempting to construct the docker auth header for image pull. - Root cause: The Secret's
.dockerconfigjsoncontains"auth":"PLACEHOLDER_REPLACE_AFTER_PAL_E_SERVICES_HARBOR_PROJECT_PROVISIONED". Theauthfield of a dockerconfigjson must be base64-encodedusername: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.tfvarsbut 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 thewestside-adminnamespace. - 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
authtoken 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-credsstep 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,/healthreturns 200, and Lucas verifies first admin login athttps://westside-admin.tail5b443a.ts.net.
-
Re-Review v2: Add PSA-restricted securityContext to westside-admin overlay (#137)
review-1118-2026-05-01-v2Verdict: 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 namewestside-admin→appin 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
appnotwestside-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'returns1— CONFIRMED. Listed as AC #5: "kustomize build overlays/westside-admin/prod/ | yq '.spec.template.spec.containers | length'returns1(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: 1000at the pod-levelsecurityContext. Cross-checked againstoverlays/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@mainline 31 hasUSER nodeper PR #10 — the image actually runs as non-root, so the manifest assertion will hold at runtime." Independently verified: fetchedforgejo_admin/westside-admin@mainDockerfile via API, line 31 readsUSER 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.yamlexclusively. "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 citesreview-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 citesreview-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 exists —
overlays/westside-admin/prod/deployment-patch.yamlverified at exact path onforgejo_admin/pal-e-deployments@mainvia API. Returns 200, valid YAML, container nameapp, no securityContext block currently. Confirms the fix is needed and the file is the right target. - [x] Kustomize patch order —
overlays/westside-admin/prod/kustomization.yamlverified:patches:list appliesdeployment-patch.yaml(strategic merge) FIRST, then JSON patches thatreplace /spec/template/spec/containers/0/namefromapp→westside-admin. Inline comment line 9 confirms: "targets base name 'app' before rename". Snippet's- name: appwill merge correctly. - [x] Streamlit reference pattern —
overlays/westside-streamlit/prod/deployment-patch.yamluses identical pod-level superset. Snippet's pod-level fields match exactly. Container-level fields match the security-relevant subset (snippet additionally addsrunAsNonRoot: trueat container-level, which is harmless redundancy with the pod-level setting). - [x] Base unchanged —
bases/standard/deployment.yamlremains 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
kubectlcommands) - [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-crudverified onproject-westside-adminuser-stories table (carried from prior reviews). - [x] arch:k8s-deploy label —
arch-k8s-deploynote 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 nameappmatches 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 readyafter ArgoCD sync. - AC3 — zero
FailedCreatefrom PSA admission viakubectl describe rs. - AC4 — zero
CreateContainerConfigErrorevents (catches Dockerfile-runs-as-root failure mode). - AC5 —
kustomize build | yq '.spec.template.spec.containers | length'returns1(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. - [x] 1. Container name in snippet is
-
Re-Review: Add PSA-restricted securityContext to westside-admin overlay (#137)
review-1118-2026-05-01Verdict: 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
CreateContainerConfigErrorzero-events check. Verified at AC #4: "ZeroCreateContainerConfigErrorevents 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 exists —
overlays/westside-admin/prod/deployment-patch.yamlverified at that exact path onforgejo_admin/pal-e-deployments@mainviacurl /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@mainDockerfile containsUSER 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 correctness — FAILS. See "New Blocker" below. The base has a
containersarray with one container namedapp. Strategic merge keys on container name. The issue body's snippet uses- name: westside-admin, which will NOT merge with- name: appin 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:
- Kustomize processes
patches:in order. Theoverlays/westside-admin/prod/kustomization.yamlappliesdeployment-patch.yaml(strategic merge) FIRST, then JSON patches that renamecontainers/0/namefromapp→westside-admin. - The existing
deployment-patch.yamlcorrectly uses- name: appat the strategic-merge stage. The kustomization comment even calls this out: "targets base name 'app' before rename." - 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 namedwestside-adminin the base, kustomize appends a second container. The pod manifest will then have:- Container 0: name
app(later renamed towestside-adminby 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.
- Container 0: name
- 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: appat 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: - ALLSecondary Finding: Snippet vs. Streamlit Canonical
The issue snippet's pod-level securityContext omits
runAsUser: 1000,runAsGroup: 1000, andfsGroup: 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 explicitrunAsUser: 1000would 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-crudverified onproject-westside-adminuser-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 namedapp, 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 readyafter ArgoCD sync. Standard rollout check. - AC3 — zero
FailedCreatefrom PSA admission viakubectl describe rs. Standard PSA failure-mode check. - AC4 — zero
CreateContainerConfigErrorevents. 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-adminto- name: app. The strategic-merge patch must target the base container name (app); the kustomization's JSON patches will rename it towestside-adminAFTER 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-admin→app) plus a clarifying comment. Re-review on the corrected body will be a fast pass. - [x] Original Blocker 1 (root crash on 9 sibling services) — RESOLVED. New "Expected Behavior" targets only
-
Review: Add PSA-restricted securityContext to bases/standard (#137)
review-1118-2026-04-30Verdict: 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-crudverified onproject-westside-adminuser-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 futurearch-k8s-deploynote. - [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 zerosecurityContextblocks. Container is namedapp, port 8000, imageapp-image. - [x]
overlays/westside-admin/prod/deployment-patch.yaml— verified, no securityContext. - [ ]
overlays/westside-streamlit/prod/deployment-patch.yaml— ISSUE: contains the issue body's claim is wrong. Streamlit overlay already defines BOTH pod-levelsecurityContext(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 atcontainers, 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: trueat 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
CreateContainerConfigErrorwhenrunAsNonRoot: trueis 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-validationDockerfile 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.yamldefines aninitContainer(copy-gmail-oauthusing 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.yamlusescontainers: [{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
appto 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.
- Option A — Per-overlay patch (LOW RISK): Add the 4 securityContext fields ONLY to
[BODY]If Option B is chosen, decompose into a parent ticket + 9 child Dockerfile PRs viaskill-decompose-ticket. Block the base change until all child PRs land.[BODY]Pod-level vs. container-level: PSA-restricted treatsrunAsNonRootandseccompProfile.typeas satisfied if set at EITHER level (per upstream k8s PSA docs — admission checks pod-level fallback).allowPrivilegeEscalationandcapabilities.dropare 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: truerejects 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 -Aafter merge shows zeroCreateContainerConfigErrorevents 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.
-
Validation: ci adapter-node validate smoke check (#12)
validation-12-2026-04-30Verdict: PARTIAL
Ticket
forgejo_admin/westside-admin#12 — PR #13 (squash-merged 2026-04-30 12:26:39 UTC, merge SHA
803e9439). Replacedtest -f build/index.htmlwithtest -f build/index.js+test -f build/handler.jsin the.woodpecker.yamlvalidate step so it asserts on artifacts@sveltejs/adapter-nodeactually 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.yamlvalidate 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.jsthen+ test -f build/handler.jsexit 0. Pipeline #2 (PR) shows the same.2 AC2: a fresh pipeline against main reaches build-and-push Woodpecker MCP get_pipeline_statusfor #3PASS Pipeline #3 (event=push) all 4 steps success: clone, validate, build-and-push, update-kustomize-tag. 3 AC3: no regression in pull_requestruns (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/artifactsPASS Tag 803e9439806cb644cc824fa8fbd87cc5d0f567e2, digestsha256:d350d5c5c05fea, pushed 2026-04-30T12:27:11Z (28 sec after merge).5 pal-e-deployments has auto-commit bumping overlays/westside-admin/prodnewTagForgejo commits API for overlays/westside-adminpathPASS Commit 576eacacde:[skip ci] westside-admin: update image tag to 803e9439806cb644cc824fa8fbd87cc5d0f567e2. Replaced the 40-zeros placeholder. Visible inkustomization.yamlnewTag.6 ArgoCD app westside-adminSynced + Healthykubectl get application -n argocd westside-admin -o jsonpathPARTIAL Sync status: Syncedat revision576eacac. Health status:Progressing(waiting on rollout). Operation phase:Succeeded. ArgoCD did its job; pod creation is what's failing downstream.7 Deployment westside-admin1/1 readykubectl get deploy -n westside-adminFAIL 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-adminand eventsFAIL Zero pods exist. Three replicasets ( 566d4cc96d,6fb5db9c6,bcb595854) all show 0/1 created. Events:FailedCreate—pods "..." is forbidden: violates PodSecurity "restricted:latest"(allowPrivilegeEscalation, capabilities.drop, runAsNonRoot, seccompProfile not set).9 /healthendpoint returns 200curl -s -o /dev/null -w "%{http_code}" https://westside-admin.tail5b443a.ts.net/healthFAIL Returns 000(connection drops). Root URL returns502from 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: maingate. 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 inImagePullBackOffrather than reaching the PSA admission controller.Discovered Issues
Pod creation blocked by PodSecurity admission "restricted:latest". The
westside-adminnamespace is labeledpod-security.kubernetes.io/enforce: restricted+enforce-version: latestinoverlays/westside-admin/prod/namespace.yaml, but neither the base manifest atbases/standard/deployment.yamlnor the overlay'sdeployment-patch.yamlsets the four fields PSA-restricted requires:spec.template.spec.containers[].securityContext.allowPrivilegeEscalation: falsespec.template.spec.containers[].securityContext.capabilities.drop: ["ALL"]spec.template.spec.containers[].securityContext.runAsNonRoot: truespec.template.spec.containers[].securityContext.seccompProfile.type: RuntimeDefault
Sister namespace
westside-streamlithas 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 manifestbases/standard/deployment.yamldoesn't set securityContext, so any overlay that adopts PSA-restricted will hit this. Recommend a follow-up Forgejo issue againstpal-e-deploymentsto either add securityContext tobases/standard/deployment.yaml(preferred — benefits every consumer) or patch it in theoverlays/westside-admin/prodoverlay specifically. The same fix unblockswestside-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.
- Cluster: archbox k3s, namespace
-
Review: pal-e-deployments#135 — Cross-namespace admin-app-db-url surfacing (2026-04-25)
review-135-2026-04-25Scope
Ticket
pal-e-deployments#135— surface theadmin-app-db-urlSecret produced in thebasketball-apinamespace (PR pal-e-platform#304) into thewestside-adminnamespace so the westside-admin pod (PR pal-e-deployments#134) can mountDATABASE_URLviasecretKeyRef.Producer: pal-e-platform terraform
module/databasecreateskubernetes_secret_v1.admin_app_db_urlinbasketball-api.
Consumer: westside-admin Deployment inwestside-adminnamespace expects a Secret namedwestside-admin-secretswith keyDATABASE_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#134body + diff — consumer expects Secretwestside-admin-secretsin thewestside-adminns withDATABASE_URL. Currently the SOPS-encryptedwestside-admin-secrets.enc.yamlin the overlay carries a placeholderDATABASE_URL. - Read PR
pal-e-platform#304body + diff — producer createskubernetes_secret_v1.admin_app_db_urlinbasketball-apins. 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#135body could not be retrieved directly (noget_issueMCP 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.yamlhas no replicator hook. Nokubernetes-replicator/external-secretsHelm release exists in the visiblepal-e-platform/terraform/main.tfmodule 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-crudand archarch:k8s-deployare 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_dispatchandfeedback_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 secondkubernetes_secret_v1in 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-platformas 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_v1in thewestside-adminnamespace — adds ~10 lines tomodule/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-adminns), and downscoping the ticket to either (a) a westside-admin-overlay-only fix that simply switches the env varsecretKeyRef.namefromwestside-admin-secrets→admin-app-db-urland 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/+ possiblyservices/_shared/. The actual repo layout isoverlays/westside-admin/prod/(noservices/prefix, no_shared/dir). Perfeedback_verify_repo_layout_before_ticketing, File Targets must match the live repo or the dev agent burns cycles. Fix before promoting totodo.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 restartof 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-platformmodule/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
- Pre-decide the mechanism with Lucas/Ava before dispatch. Recommended: terraform mirrors the Secret into
westside-adminns from the samemodule/databaseresource. Lowest blast radius, no new dependency, single source of truth. - 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#135becomes a tiny overlay-only change: rewiredeployment-patch.yamlenvs to source DATABASE_URL from the mirrored Secret name, drop DATABASE_URL from the SOPS-encryptedwestside-admin-secrets.enc.yaml. - Fix File Targets to match real repo paths (
overlays/westside-admin/prod/deployment-patch.yaml,overlays/westside-admin/prod/westside-admin-secrets.enc.yaml). - Add explicit rotation-propagation step to AC.
- Add the three constraints in finding #5.
- Once the above are in the ticket body, ticket can move
backlog→todoperfeedback_todo_means_reviewed.
- Read PR
-
Review v2: k8s Job provision admin_app Postgres user + grants
review-1097-2026-04-25-v2Verdict: READY (with one [SCOPE] carryover)
Re-review of board item #1097 — Forgejo
forgejo_admin/pal-e-platform#302. Issue body fully rewritten sincereview-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-grants→arch: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-crudexists inproject-westside-adminuser-stories table - [x] arch:postgres label — present (changed from arch:postgres-grants)
- [ ] arch note MISSING — neither
arch-postgresnorarch-deployment-westside-adminexist as backing notes. [SCOPE] Carryover from v1: createarch-deployment-westside-adminand a stubarch-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 noALTER ROLEbranch. 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()orlastval(), sequences also needSELECT.USAGEalone coversnextval()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 againstpostgres.yaml: Service name ispostgresin namespacebasketball-api, port 5432, DB namebasketball. 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, containskustomization.yaml+postgres.yaml. Newadmin-app-user-job.yamlgoes here. - [x]
pal-e-deployments/overlays/basketball-api/prod/postgres.yaml— verified: plainpostgres:16-alpineDeployment, Service namedpostgres, namespacebasketball-api, DBbasketball, userbasketball, password from Secretbasketball-api-secretskeypostgres-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
basketballis the DB owner (created viaPOSTGRES_USERenv), not formallySUPERUSER. 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-secretsfor 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: createarch-deployment-westside-adminand stubarch-postgresbacking 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 rolebasketballis 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 holdingadmin_apppassword should be a NEW Secret (notbasketball-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.
-
Review v2: Players list view: SSR table at /players
review-1092-2026-04-25-v2Verdict: APPROVED
Re-review of board item #1092 after refinement. Both [BODY] gaps from
review-1092-2026-04-25are 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 onforgejo_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
#6establishes (likelyvitest)." 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-crudexists, listed onproject-westside-adminuser-stories - [x] arch:page-server label -- backing artifact is
arch-dataflow-westside-adminFlow 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-adminKey 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
todoafter backlog -> todo move. Do not promote tonext_upuntil 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. - [x] [BODY] explicit blocker issue numbers -- Lineage now reads: "Hard depends on
-
Review: Postgres admin_app user + grants on basketball db
review-1097-2026-04-25Verdict: NEEDS_REFINEMENT
Board item #1097 — Forgejo
forgejo_admin/pal-e-platform#302. Type: Task. Reviewed againsttemplate-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-crudrow exists inproject-westside-adminuser-stories table - [x] arch:postgres-grants label — present
- [ ] arch note MISSING —
arch-postgres-grantsnote does not exist in pal-e-docs. Parentarch-deployment-westside-admin(referenced from project-westside-admin Architecture section) also does not exist as a backing note. [SCOPE] Createarch-deployment-westside-admin(parent) and eitherarch-postgres-grantsas 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/postgresqlprovider is registered.terraform/providers.tf+terraform/versions.tfdeclare 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-alpineDeployment+ PVC inpal-e-deployments/overlays/basketball-api/prod/postgres.yaml, namespacebasketball-api. CNPG-managed-user pattern (used for paledocs interraform/modules/database/main.tf:69-78) does not apply here. - The closest existing precedent is the paledocs pattern: terraform creates a
kubernetes_secret_v1in the consumer namespace (pal-e-app) holdingDATABASE_URLreferencing 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_appor 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-secretsfor 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/postgresqlnot 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 terraformkubernetes_job_v1) — pick one explicitly.[BODY]Clarify the secret-handoff: does pal-e-platform terraform create thekubernetes_secret_v1"westside-admin-secrets" directly in thewestside-adminnamespace (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 notearch-deployment-westside-admin(referenced by project-westside-admin but missing). Either include grants there or create separatearch-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.
-
Review: Players list view: SSR table at /players
review-1092-2026-04-25Verdict: 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-crudverified, entry exists onproject-westside-adminuser-stories section - [x] arch:page-server label - generic component label, not a backing-note expectation. Backing arch artifact is
arch-dataflow-westside-adminFlow 1 (referenced in Related). Noarch-page-servernote 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-adminKey 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 inbacklog. 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.tslevel 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 pastnext_upuntil all 6 blockers are merged. Ava should hold this intodoafter refinement and not promote tonext_upuntil the dependency chain clears.[BODY]Optional: name the test runner explicitly (likelynpm 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
validation-73-2026-05-04Validation: 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 viaCloses #73)Verification steps
- Local FF:
git -C ~/pal-e-services pull --ff-only forgejo main→ fast-forwarded toa80dc54 feat(keycloak): bring westside-admin client under tofu + add dev URI (#73) (#74). - tfvars edit: appended the canonical
westside-adminentry fromk3s.tfvars.exampleinto localterraform/k3s.tfvars(gitignored).tofu fmtclean.tofu validateSuccess. - Import:
tofu import -var-file=k3s.tfvars -lock=false 'keycloak_openid_client.this["westside-admin"]' westside-basketball/c5749fa6-4d1e-4b07-bdc0-e371bf65e1e5→Import successful. - Plan:
tofu plan -var-file=k3s.tfvars -lock=false -target='keycloak_openid_client.this["westside-admin"]'→0 to add, 1 to change, 0 to destroyexactly matching PR #74's documented Step 2 plan (URI + origin add only). - Apply:
tofu apply -var-file=k3s.tfvars -lock=false -target='keycloak_openid_client.this["westside-admin"]' -auto-approve→ succeeded. - 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. - Prod regression check:
curl -I https://westside-admin.tail5b443a.ts.net/returns302to/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 bypal-e-deployments#150due to a PSA/namespace structural defect uncovered at apply time.)Discovered scope (tracked separately)
- Schema extension
use_refresh_tokenswas applied toterraform/keycloak.tf+terraform/variables.tfdespite the issue's "do not touchkeycloak.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 defaultfalse; 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)
- Local FF:
Review 23
-
Review: B: westside-admin dev overlay (hostPath + tailnet)
review-1143-2026-05-03Verdict: 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 totodo.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:featurelabel — present on board item - [x]
arch:dev-overlaylabel — present on board item - [ ]
arch:dev-overlaybacking note MISSING —search_notes("arch-dev-overlay")returns empty.[SCOPE]Createarch-dev-overlaynote (or reusearch-deployment-westside-adminwith a Dev section appended). - [x]
story:marcus-admin-dev-accesslabel — present on board item - [ ]
story:marcus-admin-dev-accessbacking entry MISSING —project-westside-admin#user-storiescontains onlyadmin-row-crud.[SCOPE]Addmarcus-admin-dev-accessrow toproject-westside-adminUser 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 onorigin/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 onarchboxat/home/ldraney/pal-e-deployments/overlays/pal-e-dictionary/dev/. NOTE: it does not exist onorigin/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',isAuthExcludedexcludes/auth/*+/health, role check at line 264). Auth model claim is accurate: dev pod inherits the gate automatically. - [x] Local
~/westside-adminis 3 commits behindorigin/mainat review time — Constraints section already calls this out (git pull origin main). - [x] Local
~/pal-e-deploymentsis on stale branch75-rename-pal-e-production— Constraints already require FF main first. - [x]
westside-admin-secretsSecret — verified to exist inwestside-adminnamespace. 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 onpal-e-servicesis a separate issue (#73), correctly split.Dependencies
- Blocking: pal-e-services#73 — Keycloak client must list
https://westside-admin-dev.tail5b443a.ts.net/auth/callbackas a valid redirect URI. Without it, OIDC callback returns 400. Issue body documents this clearly. Board item A onboard-westside-admin#1142tracks it. - Implicit dependency NOT documented: ArgoCD Application registration. The existing
westside-adminArgoCD Application is sourced atoverlays/westside-admin/prod— NOT the parent directory. Addingoverlays/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 Applicationwestside-admin-devpointing atoverlays/westside-admin/dev(mirror the pattern used forpal-e-dictionary-dev) — OR — explicitly state that this overlay is applied viakubectl apply -kby 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.netfrom 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.
- [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-devstood up 6 min ago withcontainerPort=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 every5177in the issue body (Context, deployment.yaml sketch, service.yaml sketch) with5178. Also update the kustomization.yaml header comment to "Vite port 5178". - [BLOCKER 2 — PSA restricted will reject the dev pod] The
westside-adminnamespace haspod-security.kubernetes.io/enforce: restrictedapplied (verified live + present inoverlays/westside-admin/prod/namespace.yamlon origin/main). The referenceoverlays/pal-e-dictionary/dev/deployment.yamlhas no securityContext — and thepal-e-dictionarynamespace has no PSA enforcement, which is why the reference works there. Mirroring "exactly" intowestside-adminwill fail admission with `seccompProfile not set / runAsNonRoot not set`.[BODY]Add to deployment.yaml sketch: pod-levelsecurityContextwithrunAsNonRoot: true,runAsUser: 1000,runAsGroup: 1000,fsGroup: 1000,seccompProfile.type: RuntimeDefault; and container-levelsecurityContextwithallowPrivilegeEscalation: false,readOnlyRootFilesystem: false(Vite needs writes),capabilities.drop: ["ALL"]. Also note:node:22default user is root — image will need an explicit non-root user. Two viable options: (a) setimage: node:22-bookworm-slimand pre-create a writable workdir under/tmp/app, or (b) use the existing precedent: prod'sdeployment-patch.yamlsetsrunAsUser: 1000on the standard base — replicate that. The simpler path (b): copy the prod patch'ssecurityContextstanza verbatim into the new dev deployment. - [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 referenceingress.yamlhastailscale.com/funnel: "true". The Context section says "Do not addtailscale.com/funnel: \"true\"". These two instructions directly conflict — a literal mirror produces a public funnel, which the issue explicitly forbids perfeedback_funnel_requires_auth.[BODY]Reword the Constraint to "Mirroroverlays/pal-e-dictionary/dev/structure (filenames + general yaml shape), with two intentional deviations: (a) port 5178 not 5175; (b)ingress.yamlomitstailscale.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. - [BLOCKER 4 — DATABASE_URL claim is wrong] Issue says: "
DATABASE_URLis NOT needed for dev — it lives in a separate Secret (admin-app-db-url) used by prod only." Live check:westside-admin-secretscontains 6 keys includingDATABASE_URL. Both Secrets contain it (the prod patch sources DATABASE_URL fromadmin-app-db-url, but the legacy mirrored copy inwestside-admin-secretsstill exists). Practical effect ofenvFrom: [secretRef: westside-admin-secrets]: the dev pod WILL receive a realDATABASE_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_URLis present inwestside-admin-secrets(legacy mirror). The dev pod will inherit it viaenvFrom; 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)." - [BLOCKER 5 — Selector collision risk] Issue says "deployment named distinctly from prod (suggest
westside-admin-dev)". Good. But the prod deployment selector isapp: 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'sspec.selector. The referencepal-e-dictionaryoverlay usesapp: pal-e-dictionary-dev(distinct from a non-existent prod) — the dev agent could accidentally just useapp: westside-adminif they copy too literally.[BODY]Add an explicit Constraint: "All three label fields (deployment selector, deployment pod template, service selector) must beapp: westside-admin-dev— NOTapp: westside-admin— to avoid selector collision with the prod deployment in the same namespace. Verify withkubectl -n westside-admin get pods -l app=westside-admin-devreturning only the dev pod andkubectl -n westside-admin get pods -l app=westside-adminreturning 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]Replace5177with5178everywhere in the issue (Context port table, deployment containerPort, service targetPort, kustomization comment).[BODY]Add securityContext requirement (pod + container level) per Blocker 2; reference prod'sdeployment-patch.yamlsecurityContext 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 requiringapp: westside-admin-devacross 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 ArgoCDwestside-admin-devApplication resource pointing at the new path, OR explicitly state the dev overlay is hand-applied withkubectl apply -kand lives outside ArgoCD. Reword AC #2 accordingly.[SCOPE]Addmarcus-admin-dev-accessrow toproject-westside-admin#user-storiestable. (Dottie can do this on Ava's instruction once Ava confirms wording.)[SCOPE]Decide whetherarch:dev-overlaydeserves a standalonearch-dev-overlaynote or appends a Dev section toarch-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. -
Review: hooks.server.ts + app.d.ts — westside-admin#15
review-1135-2026-05-03Verdict: 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 ofwestside-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 onmain. 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-crudwith 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-crudlabel — matches project Safety Constraints contract - [x] story note verified — entry present in
project-westside-adminuser-stories table (story-westside-admin-admin-row-crud) - [x]
arch:westside-adminlabel — umbrella satisfied byarch-dataflow-westside-adminFlow 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:keycloaklabel has NO backing note — same gap as siblings #14 (review-1134), parent #1132, dep #1131.arch-keycloakstill does not exist;arch-dataflow-westside-adminFlow 1 jointly covers the Keycloak surface for this ticket. Per the carry-over[SCOPE]recommendation in review-1134, Ava's call whether to authorarch-keycloaknow 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 onmain(HEAD982df5b, 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). AddingApp.Locals.userhere 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 | nullat 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.jsontestscripts 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 inpal-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 invalidationcolumn. 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 throwMissingEnvError. For dev validation againstnpm run devon archbox, the agent will need a local.envshim — 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.tsupdate; agent cannot half-finish either side.
One small gap (NOT blocking): the issue body does not specify what happens if
refreshTokensIfNeededthrowsTokenRefreshErrormid-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_authapplies 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/healthalready 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'ssecuredefault is only true in HTTPS contexts;httpOnlydefaults to true but explicit is better. AC #6 covers this conceptually. - Locals shape lock-in: the
App.Locals.usertype defined in this PR becomes a load-bearing interface for every+page.server.tsin the project. RenamingrolestorealmRoleslater 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-ticketmachine consumption:[BODY](should-fix, not blocker) Resolve the/healthroute 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/healthas 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: 'IfrefreshTokensIfNeededthrowsTokenRefreshError, 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 exactlywestside_admin_session(matches #14's expectation and the spec in #2)' to 'Cookie name MUST be exactlywestside_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 createarch-keycloaknote now or continue deferring. Same status as before:arch-dataflow-westside-adminFlow 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/keycloakis 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/healthroute 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/healthquestion to the dev agent's prompt rather than blocking the ticket on a body refinement, since it is a 30-second clarification. -
Review: /auth/login + /auth/callback + /auth/logout endpoints (westside-admin#16)
review-1136-2026-05-03Verdict: APPROVED
Both
READYandAPPROVEDare accepted as passing verdicts by thecheck-board-advancehook. 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-crudwith 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-crudlabel — verified, single v1 story perproject-westside-adminSafety Constraints contract - [x] story note verified —
story-westside-admin-admin-row-crud(id 1616) exists, listed inproject-westside-admin#user-storiestable - [~]
arch:westside-adminlabel — no umbrellaarch-westside-adminnote, but project page Architecture section links three concrete arch notes (arch-domain-westside-admin,arch-dataflow-westside-admin,arch-deployment-westside-admin) andarch-scoped-db-westside-admin. The label is treated as a satisfied umbrella per the same precedent set inreview-1132-2026-05-03. Acceptable. - [!]
arch:keycloakhas NO backing note —search_notes("arch-keycloak")returns empty. This gap was already flagged inreview-1132-2026-05-03for 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 backfillarch-keycloakor treat the dataflow note as sufficient documentation. - [x] Forgejo issue —
forgejo_admin/westside-admin#16is 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"). Parentsrc/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,KeycloakTokensinterface, 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 viapal-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
mainat982df5b, board item #1134 invalidation). All three named primitives exist onmain. - 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 fromrefresh_expires_inin 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/logoutlink 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 inwestside-adminconsumes 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/callbacksub-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/loginas 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 PKCEcode_verifierstorage but never says where thecode_verifieris generated. Add one line: "Use Node'scrypto.randomBytes+ base64url + SHA-256 to derivecode_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 fromreview-1132-2026-05-03:arch-keycloakbacking 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 totodoand trust the dev to handle the implicit guidance. Either path is consistent with the convention. -
Review: Keycloak cookie SSR auth + admin role gate (westside-admin#2)
review-1132-2026-05-03Verdict: NEEDS_REFINEMENT
Board item: #1132 on
board-pal-e-platform(mirror ofboard-westside-adminitem #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
#6scaffolding +pal-e-platform#301Keycloak 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, butproject-westside-adminSafety Constraints section explicitly states "Every ticket on the board carriesstory:admin-row-crud." The issue body correctly referencesstory-westside-admin-admin-row-crud. Mirrored items onboard-westside-admin(item #1090) already usestory:admin-row-crud.[LABEL]Replacestory:superuser-onboard-servicewithstory:admin-row-crudon board item #1132. - [x] story note verified —
story-westside-admin-admin-row-crud(note 1616) exists; entry present inproject-westside-adminuser-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 citesarch-dataflow-westside-adminFlow 1 specifically. Treat the label as an umbrella satisfied by those three. Acceptable. - [!] arch:keycloak has NO backing note —
list_notes(tags="arch", project="westside-admin")returns noarch-keycloak, and globalsearch_notes("arch-keycloak")is empty. Same gap exists on board item1096(#301) which already lives intodo.[SCOPE]Createarch-keycloakarchitecture note covering realmwestside-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-adminverified. 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 insvelte.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.ts—src/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 emptyAppnamespace stubs;App.Localsis commented out, ready to extend. - [x]
package.json— exists.josenot currently a dependency (verified viahead -50of package.json);cookiealso not present. Both need adding. Acceptable. - [ ] "Prior art" hint about
westside-contractsSSR auth helpers — misleading.grep -rn -i "keycloak\|jose\|JWKS" /home/ldraney/westside-contracts/srcreturns zero hits. westside-contracts uses signed-token URLs (no Keycloak). westside-app uses browser-sidekeycloak-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 usingjoseper 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 intopal-e-platform#301(closed-equivalent — client created in admin console today per prompt) andpal-e-deployments#147(open — client_secret + 4 other env vars).Dependencies
Three external dependencies, ordering matters:
- westside-admin#6 (scaffolding) — CLOSED. Source tree confirms scaffold is in place.
- pal-e-platform#301 (Keycloak
westside-adminOIDC client creation + SOP) — per prompt context, the client was created today (2026-05-03) persop-keycloak-client-creation. Client is confidential (validated by the existence of aKEYCLOAK_CLIENT_SECRETin the secret). Effectively done. - 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 inoverlays/westside-admin/prod/westside-admin-secrets.enc.yamlon thekeycloak-westside-admin-secretbranch. 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 carrystory:admin-row-crudand depend on this hooks.server.ts ticket landing first because all data routes needevent.locals.userpopulated. This ticket is correctly first-in-line; recommend pulling it directly intonext_upafter refinement.Acceptance Criteria
Strong. 10 criteria, every one verifiable by an agent:
- Anonymous redirect path verifiable via Playwright follow-redirects=false.
stateCSRF check verifiable via crafted callback request with mismatched state.- Cookie attribute set verifiable via curl
-i+ grep forHttpOnly; 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
grepover 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 consumeevent.locals.user— that contract must remain stable. TheApp.Locals.usershape ({ sub, email, name, tenantId }) becomes a load-bearing interface for every subsequent feature; freeze it carefully.tenantIdhardcoded to1matches 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:
- keycloak.ts lib —
src/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. - hooks.server.ts — consumes
keycloak.ts; reads cookie, validates JWT, populatesevent.locals.user, redirects anonymous to Keycloak withstate. Also extendssrc/app.d.tswithApp.Locals.user. - auth/callback + auth/logout endpoints —
+server.tshandlers for OIDC code exchange, cookie set, logout + Keycloak SLO redirect. Depends on (1) and (2). - (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-ticketwith the 4-way split.Recommendation
[LABEL]Fix on board item #1132: replacestory:superuser-onboard-servicewithstory:admin-row-crudto match project Safety Constraints contract.[SCOPE]Decide whether to createarch-keycloaknote 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 toskill-decompose-ticketif 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_progressuntilpal-e-deployments#147is 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.
-
Review: Migrate westside-admin harbor-creds SOPS to terraform (Option A)
review-1120-2026-05-02Verdict: APPROVED
Note: Both
READYandAPPROVEDare accepted as passing verdicts by thecheck-board-advancehook.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-crudlabel — verified entry inproject-westside-adminuser-stories table (story notestory-westside-admin-admin-row-crud) - [x]
arch:harborlabel — verified row "Harbor registry" inarch-deployment-westside-adminComponents table (column "arch: label" =arch:harbor) - [x]
type:buglabel — matches refined Type (Bug) in body - [x]
blocks:deploy-chainlabel — 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/mainof pal-e-deployments and the live~/pal-e-services/terraform/tree:- [x]
overlays/westside-admin/prod/kustomization.yaml— line 8 ofresources:list contains- harbor-creds.enc.yaml. Confirmed. - [x]
overlays/westside-admin/prod/harbor-creds.enc.yaml— file exists, SOPS-encrypted with age recipientage15ct78fr4scv4vxzj3k6q76wshywzlu0mdc64a624e264dst7zfaq6tjzjr, lastmodified 2026-04-25. Confirmed. - [x]
~/pal-e-services/terraform/services.tf—resource "kubernetes_secret_v1" "harbor_creds"at lines 94-115 withfor_each = var.services. Resource composes auths fromharbor_robot_account.service_pull[each.key].secret. Confirmed. - [x]
~/pal-e-services/terraform/services.tf—resource "harbor_robot_account" "service_pull"at lines 57-77, name"${each.key}-pull", project-scoped. Resolves to robotrobot$westside-admin+westside-admin-pull. Matches issue body. - [x]
~/pal-e-services/terraform/k3s.tfvars—servicesmap at lines 199-308 includeswestside-adminat 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.env—HARBOR_ADMIN_PASSWORDpresent (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 againstforgejo_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 inbacklogon 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.yamlor 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 (perpal-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=falsereturns "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 meanskubernetes_secret_v1.harbor_creds["westside-admin"]andharbor_robot_account.service_pull["westside-admin"]only. terraform's-targetfollows transitive dependencies, so it will ALSO read/touchharbor_project.service["westside-admin"]andkubernetes_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=50returned onlyrobot$image-updater). This will be the FIRSTharbor_robot_account.service_pullto 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.yamlis 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=falseafter the apply returns 0 changes (no drift)."[BODY]Add AC #10: "ArgoCD showsApplication westside-adminasSyncedwith noOutOfSyncresources after both changes settle."[BODY]Constraint section: replace "Terraform state access (likely remote backend; verify before apply)" with "Terraform state is LOCAL (perpal-e-platform/CLAUDE.md); operator runstofu applyfrom~/pal-e-services/terraform/with the local state file."[BODY]Add to Constraint: "Operator must review thetofu plandiff before applying —-target=follows transitive dependencies, soharbor_project.service[\"westside-admin\"]andkubernetes_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:
-targetfollows transitive deps. The two named resources pull inharbor_project.service["westside-admin"]andkubernetes_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.yamlall 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'sharbor_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
#143as "first concrete consumer" and "first overlay to migrate."
- [x]
-
Review: Validate step fails: build/index.html doesn't exist for adapter-node
review-1117-2026-04-30Verdict: 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-crudlisted inproject-westside-adminuser-stories section (sole story for v1) - [x] arch:ci-pipeline label -- Woodpecker CI per
convention-architecture-idsDeployment Components table - [x] arch note verified --
arch-ci-pipelineexists 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 SHA82ac5255a9. Line 33 reads exactly- test -f build/index.htmlinside thevalidatestep (lines 27-35). Bug confirmed in the live tree. - [x]
~/westside-admin/svelte.config.js-- verified@sveltejs/adapter-nodewithout: 'build'(line 1, 8-10). Confirmsbuild/index.htmlis never produced;build/index.jsis the canonical Node entry. - [x]
~/westside-admin/package.json-- verified"start": "node build/index.js"(line 10). Production runtime already targetsbuild/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 upstreamupdate-kustomize-tag.shchange required (the script reference in.woodpecker.yamlis unaffected).Dependencies
None active. The originating commits (#10 Dockerfile, #11 .woodpecker.yaml) are merged. Sibling CI item #1094 is in
qaalready. Noin_progressblockers; 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.yamlforbuild/index.jsafter merge - AC2 (pipeline reaches build-and-push) -- visible in Woodpecker UI on next push to main
- AC3 (PR runs still gate Kaniko) -- the
validatestep still runs onpull_requestper thewhenblock (line 34-35), and Kaniko'swhenblock already excludespull_requestviaevent: [push, manual](line 53). No structural change required to preserve gating.
Optional: dev agent may add
build/handler.jsassertion 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.htmlis 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.htmlto 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.
Related
review-1094-2026-04-25-v2-- adjacent CI ticket APPROVED with the same arch:ci-pipeline label and story:admin-row-crud axesarch-ci-pipeline-- backing architecture note for this workstory-westside-admin-admin-row-crud-- sole user story for westside-admin v1convention-architecture-ids-- confirms arch:ci-pipeline as canonical for Woodpecker CIfeedback_yaml_parse_validation-- dev agent should yaml.safe_load the file before commit; QA must verify
-
Review v2: Onboard westside-admin: Harbor project + CI robot account via terraform
review-1098-2026-04-25-v2Verdict: 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-deploy→arch:harbor— confirmed vialist_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-admindoc drift fixed — verified directly:- Components table now has explicit
arch: labelcolumn. - Harbor registry row:
arch:harborlabel, 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, NOTforgejo_admin.
- Components table now has explicit
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-crudlabel — admin row CRUD story - [x] story note verified —
story-westside-admin-admin-row-crudexists; listed inproject-westside-adminuser-stories table (single v1 story) - [x]
arch:harborlabel — Harbor registry component, backed by the "Harbor registry" row inarch-deployment-westside-adminComponents table (per fractal arch convention used by sibling #1088) - [~] No standalone
arch-harbornote 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 labelsarch:harborwith the same backing). If platform-wide standalonearch-harbornote 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 entrywestsidekingsandqueens(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 overvar.services, key drives Harbor project name + robot accounts (line 7 comment, line 13name = 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 istodoorbacklog. 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-adminresolved — downstream tickets (#1094 .woodpecker.yaml, #1095 overlay) will now read consistentharbor.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-harbornote 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.
- [x] [LABEL] Label changed
-
Review: Tenant-scoped Drizzle query helper (v3)
review-1091-2026-04-25-v3Verdict: 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 forarch:scoped-db. That recommendation is now resolved.Changes Since v2 Review
- [x]
[SCOPE]RESOLVED —arch-scoped-db-westside-adminnote created (note id 1639, note_typearchitecture, projectwestside-admin, taggedarchitecture,active). Verified directly viaget_note(slug="arch-scoped-db-westside-admin"). The note describesscopedDbas a sub-component nested insidearch-dataflow-westside-admin, which matches v2's recommended option (a). - [x] Components table maps
arch:scoped-dbto two real components:scopedDb(tenant-scoped query builder, lives insrc/lib/server/scopedDb.ts) andsystemDb(unscoped builder for system tables). Plus internaldbandtenant.tsrows. The label is no longer phantom. - [x] Implementation Tickets section in the arch note explicitly lists
forgejo_admin/westside-admin#1(board #1089) andforgejo_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 newarch-scoped-db-westside-adminnests 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-crudinproject-westside-adminuser-stories section (verified in v2, unchanged) - [x] arch:scoped-db label — present
- [x] arch note verified —
arch-scoped-db-westside-adminexists 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-adminstill contains onlyREADME.mdon 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.tsandschema.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 consumescopedDb. 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
expectTypeOfortsc --noEmit - AC4: CI lint check fails on direct
dbimport — 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 = 1in 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-adminalongsidearch-dataflow-westside-admin, but the parent-arch link is sufficient since the new note nests under it. - [x]
-
Review v3: Drizzle setup + drizzle-kit pull schema.ts from basketball db
review-1089-2026-04-25-v3Verdict: 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 thearch:scoped-dblabel — has been resolved. Issue body is unchanged from v2 (already approved on body grounds). Ticket is dispatch-ready.v2 Recommendation Resolution
- [x]
[SCOPE]Resolvearch:scoped-dbbacking note — RESOLVED via Path A (preferred path). New architecture notearch-scoped-db-westside-admincreated (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 —scopedDbandsystemDbboth map toarch:scoped-db;db(internal),tenant.ts, andschema.tsdocumented 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).
- Mermaid diagram of the scopedDb/systemDb seam between page-server code and the unscoped Drizzle
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-adminuser-stories table. - [x] arch:scoped-db label — verified.
- [x] arch note verified —
arch-scoped-db-westside-adminexists in pal-e-docs with a components table that explicitly mapsarch:scoped-dbto 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-adminprovides the documented anchor thatarch:scoped-dblabels on this ticket and #1091 now point to.Advance #1089 backlog → todo.
Related
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 blockerproject-westside-admin— project pagestory-westside-admin-admin-row-crud— driving user story
- [x]
-
Review: Onboard westside-admin: Harbor project + CI robot account via terraform
review-1098-2026-04-25Verdict: NEEDS_REFINEMENT
Scope is fundamentally correct: it describes adding a
westside-adminentry toterraform/k3s.tfvars, mirroring existing service entries, to provision a Harbor project + CI robot account. Critically, the ticket gets the Harbor naming convention right (correctly invokingfeedback_harbor_project_namingas: service key = Harbor project = image_repo prefix; explicitly NOTforgejo_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 inarch-deployment-westside-adminmust 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-crudexists; listed inproject-westside-adminuser-stories section - [~] arch:k8s-deploy label — misaligned. This ticket provisions Harbor projects + robot accounts (registry/IAM concern), not k8s deployment topology.
arch:k8s-deployis correctly used on #1095 (kustomize overlay). Recommendarch:harbor(matches sibling #1088) orarch:deployment-westside-admin(matches the arch note explicitly cited in Related). [LABEL] - [ ] arch note MISSING — no
arch-k8s-deploynote 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 aservices = { ... }map with 11 active entries. Schema confirmed:forgejo_repo,image_repo,port,funnel,source_repo,source_path. Reference entrywestsidekingsandqueens(lines 224-231) uses keywestsidekingsandqueens,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.keyonharbor_project.service. Lines 24, 60: robot accounts named${each.key}-ciand${each.key}-pull. Module is for_each overvar.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.tfvarsbelongs inforgejo_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 onlyservicesmap 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=falseoutput 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 thekubernetes_secret_v1.harbor_credsresource in services.tf (image pull secretharbor-credsin the namespace). For Woodpecker push secrets specifically, need to verify whether existingharbor_username/harbor_passwordglobal 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:7comment: "service key = Harbor project name = image_repo prefix"services.tf:13:harbor_project.serviceusesname = 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(NOTforgejo_admin/westside-admin— owner namespace is irrelevant perfeedback_harbor_project_naming)." This is exactly right.
However: doc drift in
arch-deployment-westside-admin. The arch note (cited in Related) says: "Project:forgejo_adminperfeedback_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 copyforgejo_admin/westside-adminas 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-appis the closest analog (frontend SvelteKit app, separate from API). Its k3s.tfvars entry uses keymcd-tracker-app,image_repo = "mcd-tracker-app/app",port = 80,funnel = true. westside-admin should mirror this exactly — and the ticket already proposesimage_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]Replacearch:k8s-deploywitharch:harbor(matches #1088) orarch:deployment-westside-admin(matches the arch note in Related).arch:k8s-deploybelongs to #1095 (overlay), not this Harbor-provisioning ticket.[SCOPE]Either createarch-k8s-deploynote (if keeping the label) or rely on existingarch-deployment-westside-admin. Recommend the latter for consistency with sibling tickets.[SCOPE]Fix doc drift inarch-deployment-westside-admin: the "Harbor registry" row says project=forgejo_adminand 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 chosenportfor 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 newwestside-admin-cirobot 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.
-
Review v2: .woodpecker.yaml: kaniko build → Harbor → kustomize tag bump
review-1094-2026-04-25-v2Verdict: 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 statesrepo: westside-admin/appwith rationale "service key = Harbor project = image_repo prefix" matchingpal-e-services/terraform/services.tf:7convention. Acceptance Criteria includes "project =westside-admin, NOTforgejo_admin". Constraints section reinforces.[BODY]failure: ignore on update-kustomize-tag — FIXED. Step 4 in Scope explicitly says "Addfailure: ignoreper 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 namesforgejo_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 fromarch:woodpecker-citoarch:ci-pipeline, which matchesconvention-architecture-ids"Deployment Components" table.[SCOPE]Story + project page — RESOLVED.project-westside-adminexists with user-stories section listingadmin-row-crud→story-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-crudexists, listed inproject-westside-adminuser-stories section - [x] arch:ci-pipeline label — Woodpecker CI per
convention-architecture-idsDeployment Components table - [x] arch entry verified —
arch:ci-pipelinedocumented 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 withrepo: westsidekingsandqueens/app, update-kustomize-tag withOVERLAY: 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). Newwestside-admin/appentry 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.yamlbelongs inforgejo_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: ignorecovers 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: ignoreon 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.
Related
review-1094-2026-04-25— previous verdict (NEEDS_REFINEMENT)convention-architecture-ids— confirmsarch:ci-pipelinecanonicalfeedback_harbor_project_naming— Harbor naming convention enforcedfeedback_yaml_parse_validation— YAML parse-validation enforced in body
-
Review: Tenant-scoped Drizzle query helper (v2)
review-1091-2026-04-25-v2Verdict: 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 onforgejo_admin/westside-admin#1(Drizzle setup ...). Hard depends onforgejo_admin/westside-admin#6(scaffolding)." Related section also adds "DEPENDS ON:#6,#1". Resolved. - [~]
[SCOPE]Arch label changed fromarch:scopeddb-helpertoarch:scoped-db. Sibling ticket #1089 (Drizzle setup) also carriesarch: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 backingarch-scoped-dbnote 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-adminuser-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-helperin v1; now matches sibling #1089) - [ ] arch note MISSING —
[SCOPE]Noarch-scoped-dbnote exists in pal-e-docs (verified viasearch_notesqueries "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 toarch-dataflow-westside-admin. The arch label and the issue's Related link disagree. Two clean resolutions: (a) createarch-scoped-db-westside-adminas a sub-component note nested underarch-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 toarch:dataflow-westside-adminand dropscoped-dbas a phantom component. Recommend (a):scoped-dbis 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-adminstill contains onlyREADME.mdon 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.tsandschema.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 intodocolumn despite depending on this ticket which is stillbacklog— 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
expectTypeOfortsc --noEmit - AC4: CI lint check fails on direct
dbimport — 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 = 1in 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]Createarch-scoped-db-westside-adminnote (or chosen slug) underarch-dataflow-westside-admin. Document: helper API surface (scopedDb+systemDb), tenant-table vs system-table split, lint-enforced single entry point,TENANT_IDswap 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. - [x]
-
Review v2: Create westside-admin Keycloak client + document SOP
review-1096-2026-04-25-v2Verdict: APPROVED
Re-review of board item #1096 — forgejo_admin/pal-e-platform#301. Original verdict
review-1096-2026-04-25was 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 assop-keycloak-client-creationSOP. Re-reviewed 2026-04-25.Resolution of Original Findings
- [x] False premise removed — ticket no longer claims a terraform
westside-appclient to "clone." Lineage section explicitly documents the discovery and linksreview-1096-2026-04-25. - [x] Outage risk acknowledged and avoided — Constraints explicitly prohibit declaring
keycloak_realmor 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-adminuser-stories section - [x] story note
story-westside-admin-admin-row-crudexists - [x] arch:keycloak label present
- [ ] arch note
arch-keycloakstill MISSING — search returned no results. [SCOPE] Carryover from v1 review: create architecture notearch-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-platformis 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 notearch-keycloakas 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.
- [x] False premise removed — ticket no longer claims a terraform
-
Review v2: Scaffold SvelteKit adapter-node + TypeScript + ESLint
review-1087-2026-04-25-v2-finalVerdict: READY
Re-review of board item #1087 —
forgejo_admin/westside-admin#6— supersedesreview-1087-2026-04-25(NEEDS_REFINEMENT). All v1 blockers resolved.Changes Since v1
- Arch label fixed:
arch:sveltekit-server→arch:sveltekit-ssr. The new label is a canonical example inconvention-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@latestscaffold 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-idsData Flow Components table. No per-label arch note required (component sub-element ofarch-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.tsadded 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 ofconvention-architecture-ids(labels = component IDs from diagrams, no per-label note required), these are acceptable as-is provided each maps to a node inarch-dataflow-westside-adminorarch-deployment-westside-admin. Recommend a separate audit ticket to spot-check the 7 labels resolve to real diagram nodes — not blocking any individual ticket. - Arch label fixed:
-
Review v2: Drizzle setup + drizzle-kit pull schema.ts from basketball db
review-1089-2026-04-25-v2Verdict: 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 onforgejo_admin/westside-admin#6... Hard depends onforgejo_admin/pal-e-platform#302". Explicit issue numbers present. - [x]
[BODY]CI drift AC narrowed — RESOLVED. AC now reads "scripts/check-schema-drift.shruns locally withDATABASE_URLset 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 renamedarch:scopeddb-helper→arch:scoped-dbon board item #1089 (and #1091). However, noarch-scoped-dbnote 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-dbnote exists; project page Architecture section does not list it.[SCOPE]Either (a) createarch-scoped-dbnote documenting the tenant-scoped query helper component and add it to the project page Architecture list, OR (b) re-label this ticket and #1091 toarch: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]Resolvearch:scoped-dbbacking note. Two viable paths:- Path A (preferred if scoped-db is a real component): Create
arch-scoped-dbnote describing the tenant-scoping query helper (the constraint Safety #2 enforces) and add it toproject-westside-adminarchitecture 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.
- Path A (preferred if scoped-db is a real component): Create
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.
- [x]
-
Review: Scaffold SvelteKit adapter-node + TypeScript + ESLint (v2)
review-1087-2026-04-25-v2Verdict: READY
Re-review of board item #1087 —
forgejo_admin/westside-admin#6— column: backlog (gate: backlog→todo). Supersedesreview-1087-2026-04-25(NEEDS_REFINEMENT).Changes Since v1
- Arch label changed from
arch:sveltekit-server→arch:sveltekit-ssr(canonical entry inconvention-architecture-idsData 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
/healthroute 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 branchmain, 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 touchpal-e-deployments,pal-e-platform, orbasketball-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.
Related
review-1087-2026-04-25— v1 (NEEDS_REFINEMENT)story-westside-admin-admin-row-crudconvention-architecture-idsboard-westside-admin
- Arch label changed from
-
Review: .woodpecker.yaml: kaniko build → Harbor → kustomize tag bump
review-1094-2026-04-25Verdict: 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-adminpage found in pal-e-docs (search returned no results). [SCOPE] Create user story entry onproject-westside-adminuser-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-cinote found in pal-e-docs. [SCOPE] Create architecture notearch-woodpecker-cifor the CI component (or rename label to match an existing arch note such asarch-deployment-westside-adminreferenced 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 defaultprod) - [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/prodin 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.yamlbelongs 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.yamlwith anewTagfield exists. - Blocked by: service-registry onboarding for
westside-admininpal-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_tokenexist 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 perfeedback_harbor_project_naming)" — this is wrong. See Blast Radius below. Convention is "service key = Harbor project = image_repo prefix" (perpal-e-services/terraform/services.tfline 7), NOT repo owner. The reference westside-app usesrepo: westsidekingsandqueens/app(Harbor project =westsidekingsandqueens), notforgejo_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 explicitfailure: ignorein the reference. The cited precedent commit1d54939("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 needsfailure: ignoreon the step) and not assume it is implicit. [BODY]
Blast Radius
Harbor naming convention conflict (high severity). The ticket cites
feedback_harbor_project_namingto justify Harbor project =forgejo_admin, but that memory says Harbor projects derive from image_repo prefix, which inpal-e-services/terraform/services.tfis 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-adminentry withimage_repo = "westside-admin/app"(or similar) to provision awestside-adminHarbor project. - If kaniko pushes to
harbor.harbor.svc.cluster.localwithrepo: forgejo_admin/westside-admin, push will fail (noforgejo_adminproject) OR create aforgejo_adminHarbor 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-adminmatching the repo name, mirroringmcd-tracker-app), setimage_repo = "westside-admin/app", and use kanikorepo: 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 (likelywestside-admin, matchingimage_repoprefix per services.tf convention). Update kanikorepo:setting in Scope to match.[BODY]Add explicitfailure: 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 inpal-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 onproject-westside-adminuser-stories section (or create the project page if missing).[SCOPE]Create architecture notearch-woodpecker-ci(or relabel the board item toarch:deployment-westside-adminmatching the Related section in the issue body).[LABEL]Confirm the board's arch label resolves to a real arch note before todo→next_up.
-
Review: Players row edit: form action UPDATE + audit log in same transaction
review-1093-2026-04-25Verdict: 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'sdb.transaction(async (tx) => { ... })requires usingtx(not the outerdb) 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.
-
Review: Terraform Keycloak OIDC client westside-admin
review-1096-2026-04-25Verdict: 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.tfdeploys only the Keycloak server (namespace, secret, PVC, deployment, service, theme configmap). Thekeycloakterraform provider is not configured interraform/providers.tf, nokeycloak_realmresource exists, and nokeycloak_openid_clientresource exists. Thewestside-basketballrealm and existingwestside-appclient 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-crudexists; user-stories section onproject-westside-adminexists - [x] arch:keycloak label
- [ ] arch note MISSING — search for
arch-keycloakreturned no results. Existing arch notes for the project arearch-domain-westside-admin,arch-dataflow-westside-admin,arch-deployment-westside-admin. No keycloak-component arch note exists in pal-e-docs. [SCOPE] Create architecture notearch-keycloakdocumenting 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 atterraform/modules/keycloak/main.tf. - [ ] "westside-app client definition" — DOES NOT EXIST in terraform. Grepped
terraform/forkeycloak_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
keycloakterraform provider inproviders.tf(with admin credentials sourced from existingkeycloak-adminsecret or a new tfvar). - Importing or declaring the
westside-basketballrealm as a terraform resource (or marking it as data-only via adata "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-appclient into terraform (import), or leave it manual and only add the newwestside-adminclient? 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-basketballrealm via a freshkeycloak_realmresource 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 viatofu importor 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:
- Add
keycloakterraform provider + import (or data lookup) thewestside-basketballrealm. PR showstofu planwith zero diff after import. - Add
keycloak_openid_clientresource forwestside-adminwith the spec'd settings. - 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 notearch-keycloakdocumenting the component and the IaC boundary (what's terraform-managed vs admin-console-managed).[BODY]Fix file path reference:~/pal-e-platform/keycloak.tf→terraform/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-basketballrealm referenced viadata "keycloak_realm"lookup OR imported viatofu import, with no realm-level drift intofu plan."[DECOMPOSE]After SCOPE answered: split into 2-3 sub-tickets per Decomposition Assessment. Route toskill-decompose-ticket.
-
Review: Tenant-scoped Drizzle query helper
review-1091-2026-04-25Verdict: NEEDS_REFINEMENT
Note: Both
READYandAPPROVEDare accepted as passing verdicts by thecheck-board-advancehook. 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-adminuser-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]Noarch-scopeddb-helpernote 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 toarch-dataflow-westside-admin— that is the actual backing arch note. Either (a) update the board label toarch:dataflow-westside-adminto match an existing arch note, or (b) create a dedicatedarch-scopeddb-helpersub-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-admincurrently contains onlyREADME.mdon main — nosrc/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 createssrc/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.tsandschema.tsthat 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-servertickets (#1092 issue #4, #1093 issue #5) consumescopedDb, 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
expectTypeOfortsc --noEmiton a negative-fixture file. - AC4: CI lint check fails on direct
dbimport — 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 / insertrather than a query-string helper is correct (composes with arbitrarywhere/and/orper 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 = 1in 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]Resolvearch:scopeddb-helperlabel mismatch. Either (preferred) relabel the board item toarch:dataflow-westside-adminto match the existing arch note that the issue's Related section already points to, or create a newarch-scopeddb-helpersub-component note underarch-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.
-
Review: Drizzle setup + drizzle-kit pull schema.ts from basketball db
review-1089-2026-04-25Verdict: 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.
-
Review: Dockerfile: multi-stage node:22-alpine build
review-1088-2026-04-25Verdict: 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 abuild/index.jsoutput — all produced by #6. #1087 is alsobacklog; 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 inarch-deployment-westside-admin. - Documented in the ticket's
### Lineagesection.
Acceptance Criteria
All AC are agent-verifiable:
docker buildsucceeds — runnabledocker run -p 3000:3000serves on 3000 — runnable + curlable- Image size < 250MB —
docker imagesoutput - No build secrets —
docker historyoutput - 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/Dockerfileis 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
/healthroute to actually exist in the SvelteKit app. Scaffolding ticket #6 does not include a/healthroute. Either #6 needs to add it, or this Dockerfile's HEALTHCHECK will fail until a later ticket adds the route. Recommend adding a/healthroute 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 withdocker run --rm westside-admin:test id -ureturns ≥ 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/healthroute 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.
-
Review: Scaffold SvelteKit adapter-node + TypeScript + ESLint
review-1087-2026-04-25Verdict: NEEDS_REFINEMENT
Board item #1087 —
forgejo_admin/westside-admin#6— column: backlog (review gate: backlog→todo).Template Completeness
Issue declares
### Type: Task, so reviewed againsttemplate-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-crudexists, listed inproject-westside-adminuser-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 labelarch:sveltekit-serverdoes not correspond to any of them. The issue body's Related section actually points toarch-deployment-westside-adminandarch-dataflow-westside-admin, which suggests the correct label isarch:deployment-westside-admin(orarch: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 touchpal-e-deployments,pal-e-platform, orbasketball-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.yamlto 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.jsresponds 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]Changearch:sveltekit-servertoarch:deployment-westside-admin(matchesarch-deployment-westside-adminnote 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 usesadapter-staticand 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-adminDeployment: 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 .-> KCPodComponents
Component arch: label Purpose Notes Tailscale Funnel arch:tailscale-funnelPublic hostname + TLS Annotation tailscale.com/funnel: "true". Hostnamewestside-admin.tail5b443a.ts.net.Ingress (tailscale) arch:k8s-deployRoutes traffic to service ingressClassName: tailscale. tls.hosts[0]: westside-admin. Service arch:k8s-deployClusterIP for the pod Port 3000 → containerPort 3000. Deployment arch:k8s-deploySvelteKit adapter-node container Image: harbor.tail5b443a.ts.net/westside-admin/app:<sha>(Harbor project = service key perfeedback_harbor_project_naming; NOTforgejo_admin/...). Single replica v1.Postgres (basketball-api/postgres) arch:postgresData source Cross-namespace via service DNS: postgres.basketball-api.svc.cluster.local:5432. Pod connects as dedicated admin_appuser.Keycloak arch:keycloakIdentity provider Same realm/client used by westside-app + westside-contracts. New OIDC client westside-admin.k8s Secret: westside-admin-secrets arch:k8s-deployHolds DATABASE_URL, KEYCLOAK_CLIENT_SECRET, COOKIE_SIGNING_KEY SOPS-encrypted in pal-e-deployments. Harbor registry arch:harborContainer image storage Project name = westside-admin(matches service key perfeedback_harbor_project_naming). Provisioned by pal-e-services terraform onboarding (forgejo_admin/pal-e-services#64).Woodpecker CI arch:ci-pipelineBuild + push pipeline Builds on push to main, tags by commit sha, updates kustomize image tag. ArgoCD arch:k8s-deployContinuous 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_appPostgres role with read+write grants only (no CREATE/ALTER/DROP). Limits blast radius if pod is compromised. Provisioned byforgejo_admin/pal-e-platform#302via 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 projectwestside-admin, image pathharbor.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 infeedback_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-levelsecurityContextperforgejo_admin/pal-e-deployments#141. Dockerfile usesUSER node(UID 1000) so the manifest assertion holds at runtime. System-wide PSA hardening tracked inforgejo_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 persop-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).
Related
arch-domain-westside-admin— sibling: what entitiesarch-dataflow-westside-admin— sibling: how data flowsarch-scoped-db-westside-admin— sub-component: tenant-scoping query layerproject-westside-admin— parent project pagesop-network-security— network policy rulesfeedback_harbor_project_naming— image registry convention (Harbor project = service key, NOT Forgejo owner)feedback_funnel_requires_auth— funnel safety rule
- Public funnel chosen over tailnet-only. User accessibility (Lucas + Marcus from any device, no Tailscale install required for Marcus). Per
-
scopedDb helper: westside-admin
arch-scoped-db-westside-adminscopedDb helper: westside-admin
Sub-component focus on the
scopedDb/systemDbquery layer. Sits insidearch-dataflow-westside-adminas the Postgres access seam — every server route reads/writes through this helper, never directly through the unscoped Drizzledb.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 --> PGComponents
Component arch: label Purpose Notes scopedDbarch:scoped-dbTenant-scoped query builder. Auto-injects WHERE tenant_id = TENANT_IDfor everyselect/update/insert.The ONLY exported DB entry point for tenant-scoped tables. Lives in src/lib/server/scopedDb.ts.systemDbarch:scoped-dbUnscoped builder for system tables that have no tenant_idcolumn.Compile-time error if used on a tenant-scoped table. Tables: alembic_version,player_teams.db(internal)— Raw Drizzle client. Marked @internal; onlyscopedDb.tsmay 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 tellscopedDbwhich tables havetenant_id.Key Decisions
- Single entry point, lint-enforced. Any
import { db } from '$lib/server/db'outsidescopedDb.tsfails 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)orsystemDb.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 viaand(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 carryarch:scoped-db.
Implementation Tickets
forgejo_admin/westside-admin#1(board #1089) — creates the unscopeddb, generatesschema.tsforgejo_admin/westside-admin#3(board #1091) — createsscopedDb+systemDb+ lint rule +tenant.ts
Related
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 decisionsproject-westside-admin— project pagestory-westside-admin-admin-row-crud— driving user story (this helper is a Safety Constraint guarantee)convention-architecture-ids— labeling convention
- Single entry point, lint-enforced. Any
-
Data Flow: westside-admin
arch-dataflow-westside-adminData 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 HTMLFlow 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.
Related
arch-domain-westside-admin— sibling: what entities flowarch-deployment-westside-admin— sibling: where the flow livesproject-westside-admin— parent project pagefeedback_funnel_requires_auth— drives the cookie SSR choice
-
Domain Model: westside-admin
arch-domain-westside-adminDomain 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 : hasComponents
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.
Related
arch-dataflow-westside-admin— sibling: how data movesarch-deployment-westside-admin— sibling: where it runsproject-westside-admin— parent project pagearch-domain-westside-basketball— same DB, modeled from basketball-api side
- Live-Postgres-as-source-of-truth. Domain comes from
User Story 1
-
Story: Admin Row CRUD
story-westside-admin-admin-row-crudstory: 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_dbafter 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 touchingAcceptance Criteria
- [ ] SSO from westside-app — opening westside-admin in same browser does not require second login form
- [ ] Non-admins (no
adminrealm 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 ... psqlsessions 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 unscopeddbnot 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/editroute in westside-app every time a new admin need arisesGeneric row editor handles it for free Related Architecture
arch-domain-westside-admin— entities being read/writtenarch-dataflow-westside-admin— auth + page load + mutation flowsarch-deployment-westside-admin— deployment topology
Related
project-westside-admin— parent project pageboard-westside-admin— project boardfeedback_never_write_prod_db— driving constraint (no more raw psql UPDATEs)feedback_funnel_requires_auth— driving constraint (admin gate must be airtight)
Board 1
-
westside-admin
board-westside-adminNo content
Repos 1
-
westside-adminactive