westside-streamlit
Notes
Doc 18
-
Review: S1 — pal-e-services: add westside-streamlit to var.services (round 2)
review-1069-2026-04-25-r2Verdict: APPROVED
Board item
#1069· Forgejo issue forgejo_admin/pal-e-services#60 · Reviewer: Dottie · Date: 2026-04-25 · Round: 2 (re-review ofreview-1069-2026-04-22)Round 1 Findings Resolution
The prior review (id 1603) returned
NEEDS_REFINEMENTwith three[BODY]findings and one[SCOPE]finding. All three body findings are now resolved in the issue body:- [x] Reference services fixed — body now references
westsidekingsandqueens(k3s.tfvars line ~225, the livefunnel = falseprecedent) andbasketball-api(k3s.tfvars line ~209, alternate shape) with explicit warning "do NOT copy that field from here" for basketball-api'sfunnel = true. The non-existentwestside-contractsandpal-e-dora-exporterreferences are gone. - [x]
funnel = falseAC pinned — AC bullet 3 now reads: "funnel = false— the overlay'singress.yamlalready provides the Tailscale ingress; settingfunnel = truewould create a duplicatekubernetes_ingress_v1.service_funnel(dead-address class warned atservices.tf:171-175) AND violatefeedback_funnel_requires_auth." AC bullet 6 verifiestofu plandoes NOT show creation ofkubernetes_ingress_v1.service_funnel["westside-streamlit"]. - [x]
forgejo_repoAC pinned — AC bullet 4 now reads: "forgejo_repo = "forgejo_admin/westside-streamlit"— the schema requires this field with no default (variables.tf:168); copy-from-westsidekingsandqueens MUST override this (that points atforgejo_admin/westside-app)." - [ ] [SCOPE] arch-services-tf note still missing — search returned no results for
arch-services-tfin pal-e-docs. Body's Related section now explicitly calls out this discovered scope: "createarch-services-tfnote documenting thevar.servicesfan-out pattern at platform scope." Per skill-review-ticket guidance, missing backing notes are[SCOPE]recommendations and do NOT block advance. Prior review explicitly concluded: "After the three [BODY] fixes land via skill-refine-ticket, this ticket is APPROVED for advance to todo." Carry-forward as a separate ticket.
Template Completeness
- [x] Type — Feature
- [x] Lineage — explicit (audit-discovered, links siblings + supersession)
- [x] Repo —
forgejo_admin/pal-e-services - [x] User Story — well-formed As/I want/So that
- [x] Context — substrate audit summary, schema verified, hostname declared, funnel-collision risk callout added
- [x] File Targets — files to modify (with corrected reference services) and files NOT to touch
- [x] Acceptance Criteria — 9 items (was 7, added
funnel = false+forgejo_repo+ plan-output-no-funnel-create) - [x] Test Expectations — fmt/validate/plan with explicit run commands
- [x] Constraints — present, includes
-lock=falseand no-funnel reminders - [x] Checklist — present
- [x] Related — present, includes new
feedback_funnel_requires_auth+incident-2026-04-10-pal-e-streamlit-public-funnelreferences
Traceability
- [x]
story:reachablelabel — Dashboard Reachable from Phone - [x] story note verified —
story-westside-streamlit-reachableexists (id 1401, confirmed in round 1) - [x]
arch:services-tflabel present - [ ] arch note MISSING —
arch-services-tfstill not in pal-e-docs (search returned no results). [SCOPE] Carry forward as separate ticket; body already documents this as discovered scope. - [x] Forgejo issue — #60 open, body well-structured
File Targets
- [x]
~/pal-e-services/terraform/k3s.tfvars— verified, exists.grep westside-streamlitreturns zero hits, so the additive change is correctly scoped. - [x] Reference services verified live in k3s.tfvars:
westsidekingsandqueensat lines 224-231 — confirmedfunnel = false,source_repo = "forgejo_admin/pal-e-deployments",source_path = "overlays/westsidekingsandqueens/prod". Exactly the shape the body recommends copying.basketball-apiat lines 208-215 — confirmedfunnel = true. Body's warning to NOT copy this field is correct.
- [x]
~/pal-e-services/terraform/variables.tflines 165-177 — schema verified, supportssource_repo+source_pathas optional fields.forgejo_reporequired with no default (line 168). Body's "no schema extension needed" claim is accurate. - [x]
~/pal-e-services/terraform/services.tflines 168-180 — funnel resource gatedfor_each = { for k, v in var.services : k => v if v.funnel }. Comment block at 171-175 explicitly warns about the dead-address pattern. AC plan-output check matches resource namekubernetes_ingress_v1.service_funnel["westside-streamlit"]. - [x] Overlay path
~/pal-e-deployments/overlays/westside-streamlit/prod/verified — 6 files (deployment-patch, harbor-creds.enc, ingress, kustomization, namespace, secret.enc). Body's claim that the overlay already provides the Tailscale ingress is correct.
Repo Placement
OK. Issue is correctly filed on
forgejo_admin/pal-e-services. Single-repo change.Dependencies
Unchanged from round 1. Documented:
- Lineage block calls out sibling
pal-e-services#61(Keycloak client) and noteswestside-streamlit#3/#6are blocked by this ticket. - Lineage notes that
westside-streamlit#10was closed-but-incomplete and is superseded by sibling #61. - No board blockers — this ticket can advance to
todoimmediately on approval.
Acceptance Criteria
9 criteria, all individually verifiable:
- AC1 (key =
westside-streamlit) — verifiable via grep - AC2 (
image_repoprefix matches key perfeedback_harbor_project_naming) — verifiable via diff - AC3 (new)
funnel = falsewith cited reasons — exact match to round 1 [BODY] recommendation - AC4 (new)
forgejo_repo = "forgejo_admin/westside-streamlit"with override-on-copy warning — exact match to round 1 [BODY] recommendation - AC5
source_path = "overlays/westside-streamlit/prod"with pre-flight directory check — pre-verified, directory exists - AC6 (5 specific resource creates) — exact match to
services.tfresource names - AC7 (new)
tofu plandoes NOT show creation of funnel ingress — provesfunnel = falseis set - AC8 (ArgoCD targets namespace
westside-streamlit) — guaranteed by services.tf line 155 - AC9 (namespace creates cleanly or no-ops) — handles pre-existing-namespace edge case
- AC10 (no
tofu applyin PR) — explicit Lucas-applies gate
All ACs verifiable post-implementation.
Blast Radius
All round-1 risks now mitigated by explicit ACs:
- Funnel collision risk — AC3 + AC7 pin
funnel = falseAND verify the funnel resource is NOT created in plan output. Theservices.tf:171-175warning is now cited in the AC. Resolved. forgejo_repofield — AC4 pins the value with the explicit override-on-copy warning. Resolved.portfield — body Constraints section cites thewestsidekingsandqueensshape (which usesport = 80). Withfunnel = falsethe port is short-circuited; defensible.- Harbor naming bug — AC2 enforces image_repo prefix match per
feedback_harbor_project_naming. Carry-forward from round 1.
Decomposition Assessment
5-minute rule: 1 file, 1 repo, ~10 lines added to k3s.tfvars, 9 ACs (10 if counting the no-apply gate). Well within a single agent pass. No decomposition needed.
Recommendation
All three round-1
[BODY]fixes are present and accurate. The body now correctly references services that exist ink3s.tfvars, pinsfunnel = falsewith both the configuration AC and a plan-output verification AC, and pinsforgejo_repowith the verbatim-copy override warning.The single remaining finding (
arch-services-tfnote missing) is a[SCOPE]recommendation that the body already calls out as discovered scope. Per the skill-review-ticket guidance and the round-1 review's own conclusion, it does not block advance.APPROVED for advance to
todo. No further refinement required.Carry-forward (separate ticket):
- [SCOPE] Create
arch-services-tfarchitecture note at platform scope documenting thevar.servicesfan-out pattern (Harbor project + robot accounts + Harbor creds + namespace + ArgoCD app + optional funnel). Already cited in body's Related → Discovered scope.
- [x] Reference services fixed — body now references
-
Review: S1 — pal-e-services: add westside-streamlit to var.services
review-1069-2026-04-22Verdict: NEEDS_REFINEMENT
Board item
#1069· Forgejo issue forgejo_admin/pal-e-services#60 · Reviewer: Dottie · Date: 2026-04-22Template Completeness
- [x] Type — Feature
- [x] Lineage — explicit (audit-discovered, links siblings + supersession)
- [x] Repo —
forgejo_admin/pal-e-services - [x] User Story — well-formed As/I want/So that
- [x] Context — substrate audit summary, schema verified, hostname declared
- [x] File Targets — files to modify and NOT to touch both listed
- [x] Acceptance Criteria — 7 items
- [x] Test Expectations — fmt/validate/plan with explicit run commands
- [x] Constraints — present
- [x] Checklist — present
- [x] Related — present
Traceability
- [x]
story:reachablelabel — Dashboard Reachable from Phone - [x] story note verified —
story-westside-streamlit-reachableexists at id 1401 - [x]
arch:services-tflabel present - [ ] arch note MISSING — no
arch-services-tfnote in pal-e-docs (search returned no results). Theproject-westside-streamlitArchitecture section listsarch-domain-westside-streamlit,arch-dataflow-westside-streamlit,arch-deployment-westside-streamlit,arch-ci-pipeline— but nothing for theservices-tfcomponent. [SCOPE] Create architecture notearch-services-tfdocumenting thevar.servicesfan-out pattern (Harbor project + robot accounts + Harbor creds + namespace + ArgoCD app + optional funnel). This pattern serves the entire platform, not just westside-streamlit, so the note belongs at platform scope, not project scope. - [x] Forgejo issue — #60 open, body well-structured
File Targets
- [x]
~/pal-e-services/terraform/k3s.tfvars— verified, exists, contains theservicesmap block. Confirmedwestside-streamlitkey is NOT yet present (no false-positive), so the change is correctly scoped as additive. - [x]
~/pal-e-services/terraform/variables.tf— verified, schema (lines 165-177) supports optionalsource_repo+source_pathfor kustomize overlays. Required fields:forgejo_repo,image_repo,port,funnel. Optional:target_revision,source_repo,source_path. Issue's "no schema extension needed" claim is correct. - [x]
~/pal-e-services/terraform/services.tf— verified, fans out viafor_each = var.servicesacrossharbor_project.service,harbor_robot_account.service_ci,harbor_robot_account.service_pull,kubernetes_namespace_v1.service,kubernetes_secret_v1.harbor_creds,argocd_application.service. AC plan-output checks match the resource names exactly. - [x] Overlay path —
~/pal-e-deployments/overlays/westside-streamlit/prod/verified (6 files: namespace, kustomization, ingress, secret.enc, harbor-creds.enc, deployment-patch). Namespace name innamespace.yaml=westside-streamlit✓ matches AC. - [!] Issue body says: "Reference the shape used by an existing kustomize-overlay service (e.g.
westside-contracts,pal-e-dora-exporter)." — BOTH OF THESE REFERENCE SERVICES DO NOT EXIST in~/pal-e-services/terraform/k3s.tfvars.grep westside-contracts ~/pal-e-services/terraform/k3s.tfvarsreturns zero hits; same forpal-e-dora-exporter. A dev grepping for the suggested examples will waste time. Existing kustomize-overlay services in k3s.tfvars:platform-validation,basketball-api,pal-e-docs,westsidekingsandqueens,pal-e-app,gcal-scheduler,mcd-tracker,mcd-tracker-app,pal-e-mail,westside-ai-assistant,playme2k. [BODY] Replace the example list with services that actually exist — recommendbasketball-api(close shape: same file lives in pal-e-deployments overlay, image_repo prefix matches service key) ANDpal-e-mail(matches thefunnel = falsepattern this ticket needs, see next finding).
Repo Placement
OK. Issue is correctly filed on
forgejo_admin/pal-e-services. Single-repo change, no cross-repo concerns.Dependencies
Documented:
- Lineage block calls out sibling
pal-e-services#61(Keycloak client) and notes thatwestside-streamlit#3/#6 are blocked by this ticket. - Lineage notes that
westside-streamlit#10was closed-but-incomplete and is superseded by sibling #61. Verified by reading #61 body.
Board state cross-check (board-westside-streamlit):
- #1069 (this ticket, S1) and #1070 (S2/Keycloak) both in
backlog. - #1071 (C1 Monthly), #1073 (C3 Tournament, blocked by C2), #1074 (O1 Marcus onboarding), #1072 (C2 migration 048) all in
backlog. - QA column has #960 (Keycloak client #10), #961 (streamlit-keycloak in-app), #933 (CI pipeline #2), #936 (Postgres role #5), #935 (k8s deploy #4) — none of these are blocking this ticket.
- #938 (Tailscale funnel #7) and #962 (#12 secret/env) in
next_up— the ticket does not block on these.
Recommendation: dependencies are correctly mapped; no missing blockers.
Acceptance Criteria
7 criteria, all individually verifiable. Notes:
- AC1 (key =
westside-streamlit) — verifiable viagrep - AC2 (
image_repoprefix) — verifiable via diff and Harbor project name post-apply - AC3 (
source_path = "overlays/westside-streamlit/prod") — pre-verified, directory exists - AC4 (5 specific resource creates) — exact match to
services.tfresource names; agent can grep plan output - AC5 (ArgoCD targets namespace
westside-streamlit) — guaranteed bydestination.namespace = each.keyin services.tf line 155 - AC6 (namespace creates cleanly or no-ops) — handles the pre-existing-namespace edge case correctly
- AC7 (no
tofu apply) — explicit Lucas-applies-after-merge gate
Missing AC — see Blast Radius / Decomposition for required additions.
Blast Radius
Critical risk: funnel collision. The kustomize overlay at
~/pal-e-deployments/overlays/westside-streamlit/prod/ingress.yamlalready declares its own Tailscale ingress:kind: Ingress metadata: name: westside-streamlit spec: ingressClassName: tailscale defaultBackend: { service: { name: westside-streamlit, port: { number: 80 } } }If the new tfvars entry sets
funnel = true,kubernetes_ingress_v1.service_funnel["westside-streamlit"]inservices.tf(line 177) will create a SECONDwestside-streamlit-funnelingress in the same namespace. Both will register with Tailscale; only one will win the funnel hostname; the loser becomes a dead address. The comment block right abovekubernetes_ingress_v1.service_funnel(services.tf:171-175) explicitly warns about this pattern — it's the exact lesson learned fromwestside-landing-funnel.The issue's File Targets section says "Copy the shape from an existing kustomize-overlay service entry verbatim" — but most kustomize-overlay services in k3s.tfvars use
funnel = true(e.g.basketball-api,pal-e-docs,pal-e-app,gcal-scheduler,mcd-tracker,mcd-tracker-app,westside-ai-assistant,playme2k,platform-validation). Onlywestsidekingsandqueensandpal-e-mailusefunnel = false. A literal "copy verbatim" instruction will likely select the wrong template. [BODY] Add an explicit AC: "funnel = false— the kustomize overlay atoverlays/westside-streamlit/prod/ingress.yamlalready manages the Tailscale ingress; a Terraform-managed funnel would create a duplicate ingress and cause the same dead-address class of bug warned about inservices.tf:171-175."Secondary risk:
forgejo_repofield omission. The schema requiresforgejo_repo(line 168 of variables.tf, no default). The issue's AC list does not name it. A dev who copieswestsidekingsandqueensverbatim will end up withforgejo_repo = "forgejo_admin/westside-app"— wrong. The correct value isforgejo_admin/westside-streamlit. [BODY] Add to AC: "forgejo_repo = "forgejo_admin/westside-streamlit"(the source repo for ArgoCD Image Updater write-back; distinct fromsource_repowhich points to pal-e-deployments)."Tertiary risk:
portfield. The funnel block useseach.value.portas the upstream port. Withfunnel = falsethis is harmless (the funnel resource is gatedif v.funnel), so any sane number works. Recommend documentingport = 80to match the overlay's Service port (overlay mapsport: 80 → targetPort: 8501in kustomization.yaml lines 47-51). Not a hard blocker sincefunnel = falseshort-circuits the field, but it pre-empts a future toggle bug.Pattern-similar bugs to consider: Per
feedback_harbor_project_naming, theimage_repoprefix and service key MUST match exactly to avoid the 36-hour-outage class of bug. AC2 enforces this. ✓Decomposition Assessment
5-minute rule: 1 file, 1 repo, ~10 lines added to k3s.tfvars, 7 ACs. Well within a single agent pass. No decomposition needed.
Recommendation
Three findings; two are body-fix, one is scope. None are
BLOCK-class.- [BODY] Replace the non-existent reference services in File Targets (
westside-contracts,pal-e-dora-exporter) with services that actually exist in k3s.tfvars. Suggested:basketball-apifor the kustomize-overlay shape andpal-e-mailfor thefunnel = falseprecedent. Without this, a dev grepping for the suggested examples wastes time and may copy a wrong-shape template. - [BODY] Add explicit AC:
funnel = falsewith a one-line reason citing the existing overlay-managed ingress and theservices.tf:171-175warning. This is the highest-impact fix — without it the dev is one mis-copy away from a dead-ingress incident. - [BODY] Add explicit AC:
forgejo_repo = "forgejo_admin/westside-streamlit"so a verbatim copy fromwestsidekingsandqueensdoesn't carry the wrong source repo through. - [SCOPE] Create architecture note
arch-services-tfat platform scope documenting thevar.servicesfan-out pattern (label-traceability requirement; the labelarch:services-tfcurrently points to nothing). Lower urgency than the body fixes — the ticket can advance once the body is fixed; the arch note can be filed as a sibling story-tracking ticket.
After the three [BODY] fixes land via
skill-refine-ticket, this ticket is APPROVED for advance totodo. -
Review: C3 — app: Tournament registration section (blocked by C2)
review-1073-2026-04-22Verdict: APPROVED
Note: Both
READYandAPPROVEDare accepted as passing verdicts by thecheck-board-advancehook. Item passed two prior informal review rounds; the second was APPROVED after rewriting the schema premise fromregistrationstoorders → tournament_products → tournaments. This formal review confirms that revised scope.Template Completeness
- [x] Type — Feature
- [x] Lineage — Standalone, schema premise correction noted
- [x] Repo — forgejo_admin/westside-streamlit
- [x] User Story — Marcus operator role, want, so-that all present
- [x] Context — full join chain documented + provenance (review-994/998/1013, models.py)
- [x] File Targets — single file (app.py) plus explicit do-not-touch list
- [x] Acceptance Criteria — 8 criteria including dropdown query, status semantics, filters, totals, empty-state
- [x] Test Expectations — ruff check + format with run commands
- [x] Constraints — explicit "DO NOT use registrations", read-before-write list, tenant-scoping
- [x] Checklist — present
- [x] Related — links story note, parent project, blocking dependencies
Traceability
- [x] story:tournament label — matches new story note
- [x] story note verified —
story-westside-streamlit-tournament(id 1594) exists, status active, has Data Shape section confirmingorders → tournament_products → tournamentsjoin chain - [ ] story note NOT YET in project page user-stories table —
[SCOPE]Add atournamentrow to the User Stories table onproject-westside-streamlit(current rows: triage, blast, clusters, jerseys, audit, reachable). Sidecar hygiene; does not block this ticket. - [x] arch:streamlit-app label — present
- [ ] arch note MISSING —
[SCOPE]Create architecture notearch-streamlit-app. Pre-existing gap shared by every ticket on this board (jerseys/blast/triage all use the same label without a backing note); not introduced by #1073. Recommend a single hygiene ticket on board-pal-e-docs rather than gating C3. - [x] Forgejo issue —
forgejo_admin/westside-streamlit#15, state open, body matches APPROVED-round-2 content
File Targets
- [x]
~/westside-streamlit/app.py— verified: file exists, 211 lines total, three section headers found at lines 51, 114, 147 (Offered, Declined, Jerseys). Jerseys section spans 147-211 exactly as ticket claims, ending withst.dataframe(jview, ...). Pattern is the right reference for a new dropdown + filter + dataframe + cohort-textarea section. - [x]
~/basketball-api/src/basketball_api/models.py— verified Order (line 437), Tournament (595), TournamentProduct (613), Product (419), Player (225), Parent (204) all exist. Order hasplayer_id,parent_id,product_id,status(OrderStatus enum),amount_cents,tenant_id— exactly what the ticket queries assume. Tournament hasid,name,event_date(Date, nullable),tenant_id— the dropdown querySELECT id, name, event_date FROM tournaments WHERE tenant_id = 1 ORDER BY event_date DESC NULLS LASTis valid against this schema. - [x]
~/basketball-api/alembic/versions/044_add_westside_streamlit_ro_role.py— verifiedordersis inGRANT_TABLEStuple. C3's queries againstorderswill work as soon as the role is in the connection string. - [x] Migration 045 (
045_add_tournament_tables.py) — verified present; createstournamentsandtournament_products. - [x] Migration 048 — does NOT exist yet; this is correct. C2 (basketball-api#510) is the open ticket that will create it. Latest migration in tree is 047.
- [x] Files NOT to touch list — accurate:
requirements.txtandDockerfileexist but no new deps are needed (st, q() helper already imported);registrationstable correctly excluded.
Repo Placement
Correct. App-layer change (Streamlit dashboard) lives in
westside-streamlit; the corresponding GRANT migration is correctly placed inbasketball-apias a separate ticket (C2 / #510). Perfeedback_basketball_hands_off, this ticket does not touch basketball-api code — it only consumes via the existingq()helper.Dependencies
Hard blockers:
- C2 — basketball-api#510 (board item #1072, backlog): must merge AND apply migration 048 to prod before C3 PR merges. Without 048, the ro role cannot SELECT from
tournamentsortournament_products; C3 will fail at runtime withpermission denied. Issue body is explicit about this expected failure mode being correct. - C1 — westside-streamlit#14 (board item #1071, backlog): C3 says it appends "after the Monthly section (C1 appends Monthly first; this goes after it)". If C1 hasn't landed when C3 enters in_progress, the dev agent will need to choose: (a) wait for C1, or (b) place the Tournament section in the same relative position regardless of Monthly's presence and rebase. Recommend serializing C1 → C3, not parallel, to avoid line-number drift on the same file. This matches
feedback_migration_slot_coordinationapplied at the file level.
Soft blockers (substrate, already noted in ticket Related):
- S1 — pal-e-services#60 (Harbor + ArgoCD Application for westside-streamlit, board item #1069, backlog)
- S2 — pal-e-services#61 (Keycloak client, board item #1070, backlog)
Both are marked "substrate live" prerequisites. Reachability tickets (#7, #11, #12) are in next_up/qa; once they clear, the substrate is ready.
Acceptance Criteria
All 8 ACs are agent-verifiable:
- Dropdown query is given verbatim (executable check)
- Query shape spelled out with named tables and exact join path
- Status semantics defined unambiguously (paid = at least one paid order; unpaid = orders exist but none paid; "not signed up" explicitly out of scope for v1)
- Filter set enumerated (Program: All/Kings/Queens; Payment Status: All/Paid/Unpaid)
- Totals caption format given verbatim
- Empty-state path defined ("No tournaments configured yet")
- Read-only contract reaffirmed
Test commands (
ruff check app.py,ruff format --check app.py) are real and runnable. Empty-state validation against a dev db is the correct local gate.Blast Radius
Low. Single-file additive change in a Streamlit dashboard with no other consumers.
app.pyis 211 lines; new section appends after line 211 (post-Monthly per C1). No shared helpers modified —q()already exists and is cached. No basketball-api changes. No schema changes. TheordersSELECT pattern is identical to the existing Jerseys section (147-211), so the same query semantics that have been validated in prod apply here. Risk note: if migration 048 GRANTs are not live when C3 deploys, the section throwspermission deniedon first render — caught immediately, no data corruption, recoverable by applying 048. Ticket explicitly calls this out as the correct failure mode.Decomposition Assessment
1 file, 1 repo, 8 ACs, mirroring an existing 64-line section. Estimated agent work: 5–10 minutes (read Jerseys section → write SQL → wire filters → cohort textarea → ruff). No decomposition needed. Sits inside the 5-minute rule envelope when the agent has the join chain handed to them, which the ticket does.
Recommendation
[SCOPE]Add atournamentrow to the User Stories table onproject-westside-streamlit. Story note exists (id 1594) but is not linked from the project page yet. Hygiene; does not block this ticket.[SCOPE]Create architecture notearch-streamlit-app. This is a board-wide gap (every ticket on board-westside-streamlit uses this label without a backing note). Recommend filing a single hygiene ticket on board-pal-e-docs rather than gating C3.[BODY](optional, non-blocking) Consider adding an explicit ordering rule to AC for the tournament dropdown whenevent_dateis NULL across multiple rows — current ORDER BY handles it (NULLS LAST), but a tie-breaker onid DESCwould make agent output deterministic. Skip if Marcus's data has only one current upcoming tournament.- Sequencing note for the dispatcher: Move C1 (#1071) → todo and dispatch first; hold C3 (#1073) until C1 PR merges to avoid line-number drift on app.py. C2 (#1072) can run in parallel with C1 — different repo, different file. C3 enters todo only after C1 merges AND C2 migration 048 is applied in prod.
Scope is solid. Ready to advance backlog → todo.
-
Review: C2 — basketball-api migration 048 GRANT tournament tables
review-1072-2026-04-22Verdict: APPROVED
Note: Both
READYandAPPROVEDare accepted as passing verdicts by thecheck-board-advancehook. Use either keyword interchangeably.Board item #1072 — Forgejo issue
forgejo_admin/basketball-api#510. Third review round; scope corrected on 2026-04-24 to dropregistrationsafter C3 cascading review confirmed tournament payments flow throughorders(already in 044's GRANT allowlist). Two minor follow-ups flagged but neither blocks moving to todo.Template Completeness
Issue type: Feature — checked against
template-issue-feature:- [x] Type
- [x] Lineage (calls out 044 extension + 2026-04-24 scope correction)
- [x] Repo
- [x] User Story
- [x] Context (explains the orders-vs-registrations join chain)
- [x] File Targets (single new migration file + explicit do-not-touch list)
- [x] Acceptance Criteria (7 items, all binary-verifiable)
- [x] Test Expectations (with run commands)
- [x] Constraints (slot rule, op.execute style, no model edits, NO registrations)
- [x] Checklist
- [x] Related (links migrations 044 + 045, story note, feedback memories, consumer ticket)
Traceability
- [x]
story:tournamentlabel — Story: Tournament Signup + Payment Visibility - [x] story note verified —
story-westside-streamlit-tournament(id 1594) exists in pal-e-docs and explicitly documents the orders-not-registrations join chain that this migration enables - [ ] story not yet listed in
project-westside-streamlituser-stories table — current rows: triage, blast, clusters, jerseys, audit, reachable. Thetournamentkey needs a row added. - [x]
arch:postgres-rolelabel — postgres-role component (the read-only role created in 044 and being extended here) - [ ] no dedicated
arch-postgres-rolenote found viasearch_notes. Pattern is documented inproject-westside-streamlitArchitecture section +review-936-2026-04-10, which is sufficient context, but a standalone arch note would harden traceability. - [x] Forgejo issue —
forgejo_admin/basketball-api#510, state: open
File Targets
- [x]
alembic/versions/048_grant_tournament_tables_to_ro_role.py— slot is free; current head is047_add_stripe_checkout_url_to_orders.py, slots 030–047 occupied + one legacy hash-named file (e09c9e678004_add_division_column_to_players.py). down_revision047is correct. - [x] Reference migration
044_add_westside_streamlit_ro_role.pyexists and defines rolewestside_streamlit_rowith the canonical ACL-only style. - [x] Reference migration
045_add_tournament_tables.pyexists and creates exactly the two tables being granted (tournaments,tournament_products). - [x]
src/basketball_api/models.pydefinesTournament(line 596, tabletournaments) andTournamentProduct(line 614, tabletournament_products) — names match exactly. No edits required (correctly listed under "Files NOT to touch"). - [x]
registrationstable exists in models.py (line 303,Registrationbound to tryouts/practices) — correctly excluded from this migration. Confirms the C3-driven scope correction is right.
Repo Placement
OK. Migration belongs in
basketball-api(where alembic + the role + the tables live). Consumer dashboard work is correctly split intowestside-streamlit#15(board item #1073, C3).Dependencies
Documented and consistent with the board:
- Upstream: migrations 044 (role) and 045 (tournament tables) — both already merged, both at HEAD.
- Downstream: board item #1073 (C3 — westside-streamlit Tournament section) is explicitly blocked by C2. C3's title carries "(blocked by C2)" — consistent.
- Slot risk: slot 048 is currently free in
~/basketball-apimain. Perfeedback_migration_slot_coordination, if any other PR claims 048 between approval and PR open, dev must rebase and renumber. The issue body already calls this out under Constraints.
Acceptance Criteria
Seven AC, all binary-verifiable:
- Revision strings are exact strings — verifiable by inspection.
- Role name is verifiable by reading 044.
- upgrade()/downgrade() statements are exact text — verifiable by inspection.
- Round-trip is verifiable via
alembic upgrade head+alembic downgrade 047. - Style match to migration 044 — see Blast Radius below.
Test commands (pytest, ruff, alembic) are real and runnable. No missing criteria.
Blast Radius
Minimal — ACL-only, two tables, single role. No schema mutations. No model edits. No app code changes.
Style note (informational, not blocking): The AC #3 prescribes
op.execute("GRANT SELECT ON tournaments, tournament_products TO westside_streamlit_ro")— comma-separated form. Migration 044 itself uses a per-table loop withGRANT SELECT ON TABLE public.{table} TO {ROLE_NAME};(lines 82–83) — explicitTABLEkeyword and explicitpublic.schema. Both forms are valid PostgreSQL; the comma-separated form will execute correctly. This is purely a stylistic mismatch with the "match migration 044's SQL style" constraint. Dev agent should be aware they have two valid paths: (a) honor AC #3 verbatim, or (b) match 044's per-table loop style. Either is fine — flagging so the QA reviewer doesn't bounce the PR for style during code review.No similar pattern needs changing in sibling services.
westside-opsuses the same read-only-role pattern but with its own role and its own allowlist; out of scope here.Decomposition Assessment
1 file target, 7 AC, 1 repo. Estimated agent work: 5–10 minutes (write migration, run upgrade/downgrade locally, ruff, pytest, open PR). Well within the 5-minute rule for the actual code change; the longest portion is the local round-trip test which is mechanical. No decomposition needed.
Recommendation
Move from backlog → todo. Two non-blocking follow-ups for Ava to decide whether to track separately:
[SCOPE]Add a row for thetournamentkey to the User Stories table onproject-westside-streamlit(anchoruser-stories). Story notestory-westside-streamlit-tournamentalready exists (id 1594) but isn't linked from the project page table.[SCOPE]Consider creating a standalonearch-postgres-rolenote documenting the read-only-role pattern (defense-in-depth, allowlist-only, password from env). The pattern is currently documented acrossreview-936-2026-04-10, migration 044's docstring, andproject-westside-streamlitArchitecture — consolidation would harden thearch:postgres-rolelabel's traceability target.
Neither follow-up blocks ticket #1072 from advancing. Migration 048 is well-scoped, correctly bounded, and ready for an agent.
-
Story: Tournament Signup + Payment Visibility
story-westside-streamlit-tournamentStory: Tournament Signup + Payment Visibility
Role
Marcus — Lucas's brother, head coach at Westside Basketball.
Key
tournamentWant
As Marcus, I want to pick the upcoming tournament from a dropdown and see every player who's signed up, with their payment status (paid / unpaid) and parent contact.
So That
I can see which players are in for the tournament, which families haven't paid the entry fee yet, and copy the unpaid parents' emails into Gmail to chase the fee — without asking anyone to query the database.
Acceptance Criteria
- [ ] Tournament dropdown lists tournaments ordered by
event_date DESC NULLS LAST, defaulting to the most recent - [ ] Grid for the selected tournament: player name, program (Kings/Queens), team(s), tournament fee amount, payment status (paid / unpaid), parent email, parent phone
- [ ] Filters: Program, Payment Status
- [ ] Totals caption: signups, paid, unpaid
- [ ] Email cohort export textarea
- [ ] Empty-state handles "no tournaments yet" gracefully
- [ ] Read-only
Data Shape
Tournament payments flow through
orders, notregistrations:tournaments(id, name, event_date) — added in migration 045tournament_products(tournament_id, product_id, team_id) — links a tournament to specific Stripe productsproducts— Stripe productsorders(player_id, product_id, status, amount_cents) — who paid for what;ordersis already in migration 044's GRANT allowlist
Join chain for "who paid for tournament X":
tournaments→tournament_products→products→orders→players→parents.Related
project-westside-streamlit- Migration
045_add_tournament_tables— creates tournaments + tournament_products - Section pattern:
app.py:147-211Jerseys section
- [ ] Tournament dropdown lists tournaments ordered by
-
Story: Monthly Subscription Visibility
story-westside-streamlit-monthlyStory: Monthly Subscription Visibility
Role
Marcus — Lucas's brother, head coach at Westside Basketball.
Key
monthlyWant
As Marcus, I want to see every player's monthly subscription state —
active,past_due,canceled, ornone— with each parent's contact info, in a filterable grid on the dashboard.So That
I know who's current on monthly dues, who's stalled, and who never started — and I can copy the unpaid parents' emails into Gmail to chase payment without asking anyone to query the database for me.
Acceptance Criteria
- [ ] One row per player, joined to parent (email, phone) and team(s)
- [ ] Metric cards: count per
SubscriptionStatusvalue (4 cards) - [ ] Filters: Program (All / Kings / Queens), Status (each enum value)
- [ ] Totals caption
- [ ] Parent-cluster surfacing: parents with multiple non-paying players auto-highlighted
- [ ] Email cohort export textarea for the current filtered view
- [ ] Read-only — no buttons, no writes, no edits
Related
project-westside-streamlit- Source enum:
~/basketball-api/src/basketball_api/models.py→SubscriptionStatus - Section pattern:
app.py:51-112Offered pool
-
Review: Create read-only Postgres role for westside-streamlit (R2)
review-936-2026-04-10-r2Verdict: READY
Round 2 review of board item #936 on
board-westside-streamlit. Forgejo issue: forgejo_admin/westside-streamlit#5 (state: open). All three R1 findings fromreview-936-2026-04-10are addressed in the revised body.R1 Findings Disposition
- [x] Filename convention — R1 flagged timestamp vs sequential prefix. R2 body now specifies
030_add_westside_streamlit_ro_role.pywith explicit guidance: "sequential prefix (NOT a timestamp) ... verify at PR time and adjust if 030 is taken." Resolved. - [x] DELETE denial AC — R1 flagged missing DELETE denial. R2 body now adds DELETE, TRUNCATE, and DROP denial acceptance criteria (plus existing INSERT/UPDATE/CREATE TABLE), giving full symmetry across all mutation and DDL verbs. Constraints section also lists TRUNCATE + DDL denials explicitly. Resolved.
- [x] Cross-repo scope clarification — R1 flagged implicit cross-repo pattern. R2 body now has an explicit
### Repoheader stating PR lands inbasketball-apiwhile the issue is filed onwestside-streamlitfor project-tracking. Lineage and context also call out the split. Resolved.
Template Completeness
Type: Feature. All required sections per
template-issue-feature:- [x] Type, Lineage, Repo, User Story, Context, File Targets, Acceptance Criteria, Test Expectations, Constraints, Checklist, Related
- [x] Revision history captured in Lineage (cites
review-936-2026-04-10) - [x] Incident cross-reference added:
incident-2026-04-10-pal-e-streamlit-public-funnelas defense-in-depth rationale
Traceability
- [x]
story:reachablelabel — verifiedstory-westside-streamlit-reachablelisted inproject-westside-streamlituser-stories (confirmed R1). - [x]
arch:postgreslabel — present on board item. - [!] arch note missing —
arch-postgresdoes not exist in pal-e-docs. Waiver applied per precedent established for #933 (arch-ci-pipeline) and #938 (arch-keycloak-oidc): platform-wide docs gap, not a ticket defect. Ticket body explicitly acknowledges the gap and queues follow-up. Does not block dispatch. - [x] Forgejo issue — westside-streamlit#5, open, correctly filed.
File Targets
- [!]
alembic/versions/030_add_westside_streamlit_ro_role.py— directory verified. However, slot030_is now occupied by030_add_registration_type_to_registrations.py(landed between R1 and R2). Next free slot is031_. The ticket body preemptively handles this: "verify at PR time and adjust if 030 is taken." Dev agent will pick the correct prefix at branch time. Not a blocker. - [x]
tests/test_westside_streamlit_ro_role.py— clean add into existing~/basketball-api/tests/. - [x] "Files NOT to touch" section explicit.
Repo Placement
Explicit in the
### Repoheader — PR lands inbasketball-api, issue tracked onwestside-streamlit. Acceptable cross-repo pattern, now documented.Dependencies
Independent. No
in_progressblockers onboard-westside-streamlit. Downstream consumers (#4 kustomize overlay) wait on this but are not blocked on each other.Acceptance Criteria
10 criteria, all machine-verifiable. Full denial symmetry across INSERT / UPDATE / DELETE / TRUNCATE / CREATE / DROP. Positive path (all app.py queries succeed) and reversibility (
alembic downgrade -1) also asserted. Test command is real and runnable.Blast Radius
Establishes the per-consumer read-only role pattern. westside-ops should follow later with
westside_ops_ro. Additive, non-breaking. No live consumers yet.Decomposition
5-minute rule: 2 file targets, 1 repo, 10 AC (all same domain — Postgres grants). Estimated <5 min. No decomposition needed.
Recommendation
No action needed. Ticket is READY for
todoand dispatch. All R1 findings resolved; arch-postgres gap waived per precedent. The only residual nuance — migration slot 030 is now taken — is already handled by the ticket's "verify at PR time and adjust" instruction. - [x] Filename convention — R1 flagged timestamp vs sequential prefix. R2 body now specifies
-
Review: Create read-only Postgres role for westside-streamlit
review-936-2026-04-10Verdict: NEEDS_REFINEMENT
Board item #936 on
board-westside-streamlit. Forgejo issue: forgejo_admin/westside-streamlit#5 (state: open). Issue body declares target repo asforgejo_admin/basketball-api— cross-repo pattern: tracked on the consumer board, code lands in the schema owner.Template Completeness
Type: Feature. All required sections present per
template-issue-feature:- [x] Type
- [x] Lineage
- [x] Repo (explicit:
forgejo_admin/basketball-api) - [x] User Story
- [x] Context (cites
arch-dataflow-westside-streamlitandfeedback_never_alter_prod_directly) - [x] File Targets (creations + explicit "do not touch")
- [x] Acceptance Criteria (6 items, positive + negative)
- [x] Test Expectations (unit + manual, run command)
- [x] Constraints (role name, grants, denials, password handling, reversibility)
- [x] Checklist
- [x] Related
Traceability
- [x]
story:reachablelabel — verified:story-westside-streamlit-reachableexists and is listed inproject-westside-streamlituser-stories section (Platform + Head Coach role). - [x]
arch:postgreslabel — present on board item. - [ ] arch note MISSING — no
arch-postgresnote found in pal-e-docs viasearch_notes. The issue also referencesarch-dataflow-westside-streamlitandarch-deployment-westside-streamlit; neither exist yet (project page links them as planned architecture artifacts). [SCOPE] Create architecture notearch-postgres(or scope down toarch-deployment-westside-streamlitwhich would cover the DB connection). - [x] Forgejo issue — westside-streamlit#5, open, correctly filed.
File Targets
- [x]
alembic/versions/<timestamp>_add_westside_streamlit_ro_role.py— to be created. Verified~/basketball-api/alembic/versions/directory exists with 001–029 migrations. Naming convention in the repo uses numeric prefixes (e.g.029_add_schedule_tables.py), not timestamps. [BODY] Recommend clarifying filename convention: next sequential number (likely030_add_westside_streamlit_ro_role.py) rather than timestamp, to match repo convention. - [x] Tests directory
~/basketball-api/tests/exists. New filetest_westside_streamlit_ro_role.pyis a clean add. - [x] "Files NOT to touch" section correctly excludes retroactive migration edits and app code.
Repo Placement
Board lives on
board-westside-streamlit(consumer), code change lands inbasketball-api(schema owner). Issue body is explicit about this via the### Repoheader. Acceptable cross-repo pattern — the dashboard is the consumer, but the role and grants belong to the database the schema owner manages. Single PR on basketball-api closes this.Dependencies
Independent. Foundation work — no blocking tickets. The only blast-dependent ticket on the board (#939, arch:streamlit-app) will eventually consume
WESTSIDE_STREAMLIT_RO_PASSWORD, but that consumption is not required for this migration to land. No items inin_progresson board-westside-streamlit.Tables verified against app.py queries
Grepped
~/westside-streamlit/app.py. Real tables referenced:players,parents,teams,player_teams,orders,products. The stringjersey_ordersappears at line 123 but is aWITH jersey_orders AS (...)CTE, not a real table — it reads fromorders+productsinternally. The 6-table list in Constraints is exact and complete.Table existence confirmed in migrations:
parents/players(001),teams(008),player_teams(019),orders/products(013).Acceptance Criteria
6 criteria, all machine-verifiable:
- Role creation with LOGIN — assertable via
pg_roles. - All app.py queries succeed under new role — testable by capturing queries and replaying.
- INSERT / UPDATE / CREATE TABLE all fail — negative assertions with
pytest.raises. - Password stored in
~/secrets/pal-e-services/westside-streamlit.env— verified absent on host, clean add.
One implicit AC gap: no criterion for DELETE / TRUNCATE denial, even though Constraints list them. [BODY] Recommend adding one AC line: "When I connect as
westside_streamlit_roand runDELETE FROM players, then the query fails." Symmetry with the INSERT/UPDATE cases.Blast Radius
Sibling pattern exists:
westside-opsproject uses the same "read-only Postgres role" architectural invariant (seestory-westside-ops-spreadsheet-access,arch-deployment-westside-ops). This migration establishes the pattern; westside-ops should follow with its ownwestside_ops_rorole migration later. Not blocking — the pattern is additive per-consumer.No downstream runtime consumers yet — the role is provisioned before any deployed service uses it. Password rotation story is out of scope (appropriate).
Decomposition
5-minute rule check:
- File targets: 2 (1 migration + 1 test) — within limit.
- Repos touched: 1 (basketball-api) — within limit.
- AC count: 6 (plus 1 recommended DELETE addition = 7) — at limit but all same domain (Postgres grants).
- Estimated agent work: <5 min — schema-only change, template-driven migration.
No decomposition needed.
Recommendation
[SCOPE]Create architecture note for Postgres or westside-streamlit deployment — eitherarch-postgresorarch-deployment-westside-streamlit. Current issue cites an arch doc that doesn't exist in pal-e-docs. Per prior precedent (review-836-r2) this is flagged but does not block dev dispatch if Ava accepts the gap.[BODY]Clarify migration filename convention in File Targets: next sequential numeric prefix (e.g.030_add_westside_streamlit_ro_role.py), not a timestamp — to match the existing001_…029_numbering in~/basketball-api/alembic/versions/.[BODY]Add a DELETE denial acceptance criterion for symmetry with INSERT/UPDATE/CREATE TABLE.
No [LABEL] or [DECOMPOSE] fixes needed. Scope is solid in substance; the gaps are cosmetic + a missing backing architecture note.
-
Review: Add Woodpecker CI pipeline (westside-streamlit #3)
review-934-2026-04-10Verdict: APPROVED
Scope is solid. Ticket is well-structured, mirrors established basketball-api pipeline pattern, acknowledges dependencies, and embeds platform lessons (f17b49b, yaml parse validation). One [SCOPE] note: the backing arch note is missing but that is platform-wide — not a blocker for this ticket.
Template Completeness
- [x] Type (Feature)
- [x] Lineage — notes dependencies on Dockerfile and kustomize overlay tickets
- [x] Repo
- [x] User Story (As/I want/So that)
- [x] Context — references commit f17b49b lesson and yaml parse validation feedback
- [x] File Targets (create: .woodpecker.yaml; explicit do-not-touch list)
- [x] Acceptance Criteria (6, all When/Then, all verifiable)
- [x] Test Expectations
- [x] Constraints (mirror basketball-api, secrets hygiene, unconditional update-kustomize-tag, placeholder test step)
- [x] Checklist
- [x] Related
Traceability
- [x] story:reachable — maps to story-westside-streamlit-reachable (Platform + Head Coach, Keycloak + no Tailscale client). Verified in project-westside-streamlit user-stories section.
- [x] arch:ci-pipeline label present
- [ ] arch note MISSING — search_notes for arch-ci-pipeline returned 0 results. [SCOPE] Create architecture note arch-ci-pipeline for the shared Woodpecker → kaniko → Harbor → kustomize update pattern. NOT a blocker for this ticket (gap is platform-wide across all 7 services using this pattern).
- [x] Forgejo issue — https://forgejo.tail5b443a.ts.net/forgejo_admin/westside-streamlit/issues/3 (open)
File Targets
- [x]
.woodpecker.yaml(to be created) — consistent with reference implementation at/home/ldraney/basketball-api/.woodpecker.yamlwhich containsupdate-kustomize-tagstep at line 60 and downloads the sharedscripts/update-kustomize-tag.shfrom pal-e-platform. Pattern is verifiable and reusable. - [x] Explicit "do not touch" list (Dockerfile, pal-e-deployments overlay) correctly partitions scope to avoid overlap with #2 and #4.
Repo Placement
OK. Issue is filed on forgejo_admin/westside-streamlit where the .woodpecker.yaml will live. Dependencies (Dockerfile #2, kustomize overlay #4) correctly filed as separate tickets in the same repo and pal-e-deployments respectively.
Dependencies
Explicitly acknowledged in ticket body:
- #2 Dockerfile — needed for kaniko build step to have something to build
- #4 kustomize overlay — needed for update-kustomize-tag step to have a target file
- Harbor robot account — mentioned in "Blocks on" list
Per review directive: this is a scope review, not a readiness-to-dispatch review. Acknowledgement is sufficient; dispatch ordering is the kanban flow concern.
Acceptance Criteria
All 6 AC are When/Then format and independently verifiable. Concrete commands given (python yaml.safe_load, harbor UI check, git log check). AC #5 directly encodes the f17b49b fix pattern as a testable requirement. Excellent.
Blast Radius
Low risk. New file on a new repo — no existing workflows to break. Platform-wide implication: if the arch-ci-pipeline note is eventually created, all 7 services should be audited to confirm they share the f17b49b fix pattern (feedback_ci_pipeline_lessons already tracks 12 root-cause fixes, so this is likely covered).
Decomposition
No decomposition needed. 1 file target, 6 AC, estimated agent time < 5 min (copy basketball-api pattern, adjust repo/image names, yaml validate, PR). Fits the 5-minute rule cleanly.
Recommendation
[SCOPE]Create architecture notearch-ci-pipelinedocumenting the shared Woodpecker → kaniko → Harbor → kustomize-tag-update pattern used by all 7 platform services. Not a blocker for #934 — tracked as a separate platform docs ticket. Ava to decide whether to open it now or defer.
APPROVED for advance to todo. Dispatch ordering: wait for #2 (Dockerfile) and #4 (kustomize overlay) to reach done before this moves to in_progress.
-
Story: Dashboard Reachable from Phone
story-westside-streamlit-reachableStory: Dashboard Reachable from Phone
Key:
reachable· Label:story:reachable· Role: Platform operator (Lucas) + Head coach (Marcus)Story
As the platform operator and head coach, I need the westside-streamlit dashboard to be reachable on a stable HTTPS URL from any device — including Marcus's phone, with no Tailscale client installed — so that Marcus can check contract and jersey status at practice without calling Lucas, and so the dashboard moves from "a tool only Lucas can run" to "shared ops infrastructure."
Why
Today, the dashboard is a local Python process on Lucas's machine. Every time Marcus has a question, he either calls Lucas or waits until the next time they're together. That's the opposite of what an ops dashboard is for. Making it reachable is the single most important step toward the dashboard being useful, not just built.
"Reachable" is a bundle of infrastructure work, not one ticket. All the deployment-stack tickets (Dockerfile, Woodpecker pipeline, kustomize overlay, read-only DB role, Tailscale funnel, Keycloak OIDC) trace to this single story. Each ticket is a necessary step; this story is the user-facing outcome.
Acceptance criteria
- Marcus can open
https://westside-ops.tail5b443a.ts.net/on his phone, log in with his Keycloak credentials, and see the dashboard — without installing Tailscale. - The deployment is managed via the standard platform pipeline: git push → Woodpecker build → Harbor push → ArgoCD sync. Manual
kubectltouches are not part of the steady-state loop. - The database connection uses a dedicated read-only role, not the basketball-api credentials.
- Unauthenticated requests are redirected to Keycloak. Authenticated users without the
westside-opsrole see a 403. - Dashboard response time < 3 seconds from cold cache over 4G on Marcus's phone.
Supporting tickets
All of these trace to
story:reachable:- Add Dockerfile
- Add Woodpecker CI pipeline
- Add kustomize overlay in pal-e-deployments
- Create read-only Postgres role for westside-streamlit
- Expose via Tailscale funnel on dedicated hostname
- Wrap dashboard in Keycloak OIDC before public exposure
Out of scope
- Per-user audit logging (who viewed what, when). Nice to have, separate future story.
- Role-based feature gating (e.g., Marcus sees only Jerseys, Lucas sees everything). Start with all-or-nothing.
- Mobile-first UI redesign. Streamlit is responsive enough for v1; custom mobile styling can come later.
- Marcus can open
-
Story: Signed Contract Audit
story-westside-streamlit-auditStory: Signed Contract Audit
Key:
audit· Label:story:audit· Role: Ops (Lucas / Ava)Story
As ops, I need to audit the set of signed contracts — by date, version, and fee — so I can verify nothing was signed at the wrong price, spot anyone still on an outdated contract version, and have a defensible record if a dispute arises.
Why
Contract versions change mid-season (fee adjustments, language updates). When that happens, some players are grandfathered on the old version. Without a quick audit view, it's impossible to answer "who is on v2 vs v3?" without opening every player individually. And if a parent disputes a fee, ops needs to see the signed-at timestamp and version instantly.
Acceptance criteria
- Signed audit section lists every
contract_status = 'signed'player with: name, program, teams, fee,contract_signed_at,contract_version, parent email. - Sorted by signed-at timestamp descending (most recent first, nulls last).
- Section is collapsed by default (behind an expander) so it doesn't crowd the triage views, but expansion is a single click.
- Each row is unambiguous — no need to look anything up elsewhere.
Current implementation
Already built in
app.pylines 102–119 inside ast.expander. Matches acceptance criteria. Captured for traceability.Future enhancements
- Group by contract version — show counts per version to catch outliers.
- Highlight anyone on a version >2 revisions old.
- Click-through to the signed PDF artifact (stored where? — open question; coordinate with
westside-contracts).
- Signed audit section lists every
-
Story: Jersey Payment Tracking
story-westside-streamlit-jerseysStory: Jersey Payment Tracking
Key:
jerseys· Label:story:jerseys· Role: Head Coach (Marcus)Story
As head coach Marcus, I need to see which players have ordered jerseys and whether they've actually paid — including players who paid cash outside the Stripe flow — so I don't hand out a jersey to a kid whose parent never paid, and I don't hound a parent who already paid me in cash.
Why
Jersey handoffs happen at practice. In the moment, Marcus needs to answer "is this kid good to go?" without calling Lucas. The current system has two sources of truth for payment: Stripe (via
ordersrows created by basketball-api) and cash (no row anywhere — it exists in Marcus's head). Cash payments are frequent (Seydou is a current example). The dashboard needs to make both visible in one place.Current implementation
Built in
app.pylines 121–185. Shows players with ajersey_optionset, joined with their most recent jersey order to derivepay_statusfromorders.status. Filters: program (All / Kings / Queens), payment (All / Unpaid / Paid / Canceled).Known gap — cash payments
The current implementation treats missing = unpaid. A player who paid cash has no
ordersrow, so they show up aspay_status = 'none'in the "unpaid" filter. This is wrong — they ARE paid, just not through Stripe. See ticket: "Add cash jersey payment tracking" onboard-westside-streamlit.Acceptance criteria (current + post-cash-fix)
- Every player with
jersey_option IS NOT NULLappears in the Jerseys view. - Each row shows: name, program, teams, jersey option (home/away/both), size, number, preferred number, payment status, product name, amount, ordered-at timestamp.
- Sort order: unpaid first, then canceled, then paid (ops priority order).
- Filter totals shown: "paid: X, pending: Y, canceled: Z, no order: N".
- (After cash fix): cash-paid jerseys show
pay_status = 'paid-cash'(or similar), and are excluded from the "unpaid" filter by default.
Out of scope
- Recording the cash payment itself. That write goes through basketball-api (a new endpoint, or a manual DB row via the admin UI). The Streamlit dashboard just reads the result.
- Jersey fulfillment tracking (ordered from supplier? received? handed to player?) — that's a separate future story if needed.
- Every player with
-
Story: Parent Cluster Detection
story-westside-streamlit-clustersStory: Parent Cluster Detection
Key:
clusters· Label:story:clusters· Role: Ops (Lucas / Ava)Story
As ops, I need to automatically spot parents who have multiple unsigned kids, so I can have the "sibling discount" conversation with them as one call instead of calling the same parent twice.
Why
A parent with two unsigned kids is a very different conversation than a parent with one. The business decision (apply a sibling discount? bundle the contracts? waive a fee?) depends on knowing they're a cluster. Calling them twice and having two separate half-discussions is the failure mode. The dashboard should surface these clusters automatically so ops doesn't have to hand-scan for duplicate emails.
Acceptance criteria
- Below the filtered Offered dataframe, a warning banner appears only when there are parent clusters in the current filter.
- Banner lists each parent email with the count of unsigned kids: e.g., "jane@example.com (3), bob@example.com (2)".
- Clusters are computed from the current filtered view, not the full offered pool, so filtering to a specific team shows team-scoped clusters.
- Parents with a single unsigned kid never show up in the cluster list.
Current implementation
Already built in
app.pylines 77–83. Matches acceptance criteria. Captured for traceability.Future enhancements (not yet tickets)
- Make the email clickable — one click filters the dataframe to just that parent's kids.
- Extend cluster detection to the Jerseys view (one parent buying 3 jerseys might qualify for a bulk discount).
- Surface which teams the cluster spans (e.g., "both kids on 17U Kings" vs "one on Kings, one on Queens").
-
Story: Email Blast Cohort Export
story-westside-streamlit-blastStory: Email Blast Cohort Export
Key:
blast· Label:story:blast· Role: Ops (Lucas / Ava)Story
As ops, I need to grab a scoped list of parent emails for a blast campaign (e.g., "all Queens who haven't signed") in one copy-paste, so I can move from "which parents?" to "send the email" without a data migration.
Why
Email blasts are gated on the 7-step approval flow per
feedback_email_blast_nuclear_gate. That gate assumes the recipient list is already defined. Building the recipient list is the slow step: someone has to filter by program, team, contract status, maybe jersey status, and pull emails. Doing that in SQL is expensive (new query per cohort); doing it in the admin UI is one-by-one and error-prone. Streamlit filters + a copy-paste text area collapses that to seconds.Acceptance criteria
- Any filter combination on the Offered section produces a matching comma-separated email list in a copy-paste text area below the dataframe.
- Email list is deduplicated (one parent → one email even if they have multiple kids in the cohort).
- Empty/null emails are excluded silently.
- Label on the text area explicitly says "Parent emails for this view (copy for blast)" so the user knows the list is scoped to the current filter.
Current implementation
Already built in
app.pylines 85–86. Matches acceptance criteria. Captured here for traceability.Related tickets (likely future work)
- Extend the export to the Declined pool (re-offer campaigns).
- Extend to the Jerseys unpaid pool (payment reminder campaigns).
- Optional: "Export to CSV" button for exports that need phone + name alongside email.
Out of scope
- Actually sending the email — that goes through
gmail-sdk/ basketball-api blast endpoint and the 7-step approval gate. The Streamlit app only produces the list.
-
Story: Contract Triage
story-westside-streamlit-triageStory: Contract Triage
Key:
triage· Label:story:triage· Role: Ops (Lucas / Ava)Story
As ops, I need to see at a glance which players have not yet signed their contract, so I can prioritize who to chase without drilling into individual player cards.
Why
The admin UI in
westside-appshows one player at a time. When 50+ players are mid-roster, one-at-a-time navigation buries the signal. Marcus and Lucas need the "who is still open" list to be the default view, not something you have to click through to find.Acceptance criteria
- Dashboard top shows 4 metrics: signed, offered, declined, none (the current counts from
players.contract_status). - An "Offered — the action pool" section lists every
contract_status = 'offered'player with: name, program (Kings/Queens), teams, fee, parent email, parent phone, jersey details, contract version, and custom notes. - Filters: program, team multi-select, fee tier, "has phone", "custom deal only".
- The filtered view shows a count: "Showing X of Y offered".
- Total page load time < 2s (cold cache), < 500ms (warm).
Current implementation
Already built in
app.pylines 15–75. Matches all acceptance criteria. This story is captured primarily for traceability — any future work touching the triage view should referencestory:triagein its ticket.Out of scope
- Mutating contract status from this view (all writes go through the admin UI).
- Sending outreach messages directly (see
story:blastfor the copy-paste export workflow). - Historical trend lines (how many were signed last week vs this week) — future story if requested.
- Dashboard top shows 4 metrics: signed, offered, declined, none (the current counts from
-
Deployment — westside-streamlit
arch-deployment-westside-streamlitDeployment — westside-streamlit
Where the service runs and how it connects to the basketball database. Current state is "local only" — the diagram below shows both the current reality and the target state.
Current — local dev loop
graph TB subgraph archbox["archbox host"] subgraph k3s["k3s cluster"] CNPG[("pal-e-postgres
CNPG cluster
ns: postgres")] API["basketball-api
ns: basketball-api"] end PF["kubectl port-forward
svc/pal-e-postgres-rw
5432 → 55432"] subgraph devloop["Local dev loop"] STR["streamlit run app.py
PGURL=postgres://...@127.0.0.1:55432/basketball"] BROWSER["browser
localhost:8501"] end end CNPG --- PF PF -.-> STR STR -.-> BROWSER API --> CNPGTarget — deployed via the standard platform pipeline
graph TB subgraph archbox2["archbox host"] subgraph k3s2["k3s cluster"] CNPG2[("pal-e-postgres
CNPG
ns: postgres")] POD["westside-streamlit pod
ns: westside-streamlit
image: harbor/westside-streamlit:sha"] SECRET["SOPS-encrypted secret
PGURL"] TS_INGRESS["Tailscale funnel
westside-ops.tail5b443a.ts.net"] end ARGO["ArgoCD"] DEPLOYMENTS["pal-e-deployments
kustomize overlay"] HARBOR["Harbor
harbor.tail5b443a.ts.net"] WP["Woodpecker CI"] end GIT["Forgejo
forgejo_admin/westside-streamlit"] GIT -->|push| WP WP -->|build + push| HARBOR HARBOR -.->|image updater| DEPLOYMENTS DEPLOYMENTS -->|sync| ARGO ARGO -->|apply| POD SECRET --> POD POD --> CNPG2 TS_INGRESS --> PODGap to target
The following pieces are missing and live as tickets on board-westside-streamlit:
- Dockerfile — multi-stage build producing a small Python 3.12 + streamlit image.
- Woodpecker pipeline —
.woodpecker.yamlmirroring the basketball-api pattern (test, build, push to Harbor, update pal-e-deployments overlay). - Harbor robot account — per-service credential for the Woodpecker build step.
- Kustomize overlay in
pal-e-deployments— Deployment + Service + SOPS-encrypted Secret forPGURL. - Tailscale funnel — new dedicated hostname (never stomp archbox per
feedback_never_stomp_archbox). Proposal:westside-ops.tail5b443a.ts.net. - ArgoCD Application pointing at the new overlay.
- Keycloak OIDC (optional, post-MVP) — even though the app is read-only, the data is sensitive (parent emails, phone numbers). Wrap it in Keycloak auth before exposing on a public funnel.
DB connection detail
In-cluster, the connection target is
pal-e-postgres-rw.postgres.svc.cluster.local:5432(the read-write endpoint). The database name isbasketball; credentials come from a dedicated read-only role (to be created in a backlog ticket — the basketball-api role has write privileges we don't need). -
Data Flow — westside-streamlit
arch-dataflow-westside-streamlitData Flow — westside-streamlit
How a page render moves data from the Postgres database to the user's browser. Streamlit re-runs the entire script on every interaction; the
@st.cache_data(ttl=30)decorator keeps repeat queries off the database for 30 seconds.sequenceDiagram actor User as Lucas / Marcus participant Browser participant ST as Streamlit runtime participant APP as app.py participant CACHE as st.cache_data (ttl=30s) participant PG as Postgres (basketball db) User->>Browser: loads / Browser->>ST: WebSocket connect ST->>APP: run script top-to-bottom rect rgb(240, 248, 255) Note over APP,CACHE: Each q(sql) call checks cache first APP->>CACHE: q("SELECT contract_status, COUNT(*) ...") CACHE-->>APP: miss (first load) APP->>PG: SELECT via psycopg3 PG-->>APP: rows → pandas DataFrame APP->>CACHE: store with TTL 30s end APP->>APP: build Offered pool, apply filters APP->>APP: detect parent clusters (group by email) APP->>APP: build Declined pool + Jerseys pool APP-->>ST: st.dataframe, st.metric, st.text_area ST-->>Browser: render widgets Browser-->>User: dashboard visible User->>Browser: change a filter (radio / checkbox) Browser->>ST: WebSocket event ST->>APP: re-run script rect rgb(240, 255, 240) Note over APP,CACHE: Within 30s TTL → cache hit, no DB round trip APP->>CACHE: q(...) CACHE-->>APP: hit end APP->>APP: re-apply filters (pure pandas, fast) APP-->>ST: render ST-->>Browser: patch UIQuery load estimate
Current
app.pyissues 4 SELECTs per cold render (counts, offered, declined, signed, jerseys — signed is behind an expander so it only runs when opened; jerseys and offered are the heavy ones). Withttl=30and an audience of 2, database load is effectively negligible: < 10 queries/minute at peak.Interaction model
Streamlit's mental model: every widget interaction re-runs the whole script. Cached queries return instantly on re-run; everything else (pandas filtering, rendering) is pure Python on a small DataFrame, so the loop feels immediate.
This is why caching is critical: without the 30s TTL, every filter click would hit the database 4 times.
Write path
There is none. The dashboard issues zero writes. All mutations go through:
basketball-api(contract signing, jersey orders, player admin)- The admin UI in
westside-app(manual player edits)
This is a deliberate architectural decision, not a limitation. It keeps the Streamlit app safe to iterate on without risk of corrupting production data.
-
Domain Model — westside-streamlit
arch-domain-westside-streamlitDomain Model — westside-streamlit
Entities that the Streamlit dashboard reads from the
basketballPostgres database. All schemas are owned bybasketball-api; westside-streamlit is a consumer, never a writer.erDiagram PLAYERS ||--o{ PLAYER_TEAMS : "roster" TEAMS ||--o{ PLAYER_TEAMS : "roster" PLAYERS }o--|| PARENTS : "belongs to" PLAYERS ||--o{ ORDERS : "placed by" PRODUCTS ||--o{ ORDERS : "ordered" PLAYERS { int id PK string name enum division "boys|girls" money monthly_fee int parent_id FK enum contract_status "signed|offered|declined|none" string contract_version datetime contract_signed_at enum jersey_option "home|away|both|none" enum jersey_size "S|M|L|XL|XXL" int jersey_number int jersey_number_preferred string custom_notes } PARENTS { int id PK string email string phone } TEAMS { int id PK string name } PLAYER_TEAMS { int player_id PK,FK int team_id PK,FK } ORDERS { int id PK int player_id FK int product_id FK enum status "paid|pending|canceled" int amount_cents datetime created_at } PRODUCTS { int id PK string name enum category "jersey|contract|merch" }Columns the dashboard reads
players.contract_statusdrives the counts cards, the Offered pool, the Declined pool, and the Signed audit.players.divisionis mapped to "Kings" / "Queens" in the UI (boys→ Kings,girls→ Queens).players.monthly_feedrives the fee tier filter (≤$100, $100–180, $180–200, >$200).players.custom_notessurfaces as a "custom deal" filter — any non-empty value means a bespoke contract.parents.emailis the dedup key for parent-cluster detection (one parent → multiple unsigned kids).orders.statusfiltered toproducts.category = 'jersey'drives the jersey payment view. Gap: cash-paid jerseys (e.g. Seydou) have noordersrow, so they show aspay_status = 'none'. Seestory-westside-streamlit-jerseys.
Data not yet surfaced
The database has more than the dashboard currently shows. Future tickets may pull in:
- Outreach log (when was a parent last contacted)
- Payment schedule / monthly dues tracking
- Tournament registration state
- Coach assignments per team
Review 9
-
Review: S2 — pal-e-services: Keycloak client for westside-streamlit (#1070)
review-1070-2026-04-22Verdict: APPROVED
Board item: #1070 — S2 — pal-e-services: Keycloak client for westside-streamlit (supersedes #10)
Forgejo issue: forgejo_admin/pal-e-services#61 (open)
Reviewer: Dottie
Date: 2026-04-22
Context: Third review pass. Two prior informal rounds completed; second round was APPROVED after lockingpublic_client = true+ PKCE S256. This formal review confirms the locked scope is sound for backlog→todo advancement.Template Completeness (Feature)
- [x] Type — Feature
- [x] Lineage — supersedes incorrectly-closed
forgejo_admin/westside-streamlit#10; sibling of pal-e-services#60 - [x] Repo —
forgejo_admin/pal-e-services - [x] User Story — Marcus loading dashboard in Safari, public client (PKCE), admin role claim flow
- [x] Context — accurately describes
app.py:13-18as browser-sidestreamlit-keycloakwrapper requiring public client - [x] File Targets — single file (
terraform/k3s.tfvars) with explicit "files NOT to touch" list - [x] Acceptance Criteria — 9 testable items, each grep/tofu-verifiable
- [x] Test Expectations — concrete
tofu fmt -check,tofu validate,tofu plan -lock=falsecommands - [x] Constraints — explicit "copy westside-spa shape verbatim",
-lock=falseperfeedback_tofu_lock_false, no unrelated client edits - [x] Checklist — present
- [x] Related — links to project page, story, three feedback memories, superseded issue
Traceability
- [x] story:reachable label — "Dashboard Reachable from Phone"
- [x] story note verified —
story-westside-streamlit-reachable(id 1401) exists; entry onproject-westside-streamlituser-stories table at row "reachable" - [x] arch:services-tf label — services terraform layer (Keycloak clients, services map)
- [~] arch note for
services-tf— no dedicatedarch-services-tfnote exists in pal-e-docs. The project'sarch-deployment-westside-streamlitcovers the consuming side but not the platform-level terraform module shape. Same gap exists on sibling #1069. This is a board-wide convention gap, not a #1070 blocker. - [x] Forgejo issue — #61 open, body well-formed, no blocking comments
- [x] Repo placement — fix is in
pal-e-servicesand the issue is filed there. Consistent.
File Targets
- [x]
terraform/k3s.tfvars— verified file exists.westside-spaentry occupies lines 92-111 (issue says "around line 92" — accurate). Shape includesrealm_key,client_id,name,public_client = true,pkce_code_challenge_method = "S256",backchannel_logout_session_required = false,valid_redirect_uris,web_origins. Copy-verbatim instruction is achievable. - [x]
terraform/keycloak.tf(NOT to touch) — verified the resource iteratesvar.keycloak_clients, wiresaccess_type = each.value.public_client ? "PUBLIC" : "CONFIDENTIAL"at line 142,pkce_code_challenge_methodat line 150,valid_redirect_uris/web_originsat 152-153, and gates the realm-roles mapper oninclude_realm_roles_mapperat line 169. No schema changes needed — confirmed. - [x]
terraform/variables.tf— schema forkeycloak_clientsmap (lines 137-163) accepts every attribute the issue requires:public_client,pkce_code_challenge_method,valid_redirect_uris,web_origins,include_realm_roles_mapper. All optional with safe defaults. - [x] Existing
westside-landing,westside-spa,pal-e-docs-landingentries — verified untouched scope per "files NOT to touch." - [x]
grep -rn streamlit terraform/— confirmed zero existing references; supersession lineage from #10 is accurate (no orphan terraform left behind).
Repo Placement
OK. Single repo, single file. No multi-repo coordination required. The Keycloak resource is in
pal-e-services/terraform; the consuming app is inwestside-streamlit; the issue correctly targets the producing repo.Dependencies
- Sibling, not blocker: #1069 (S1 — pal-e-services#60, add westside-streamlit to
var.services). Both touch the same repo (pal-e-services) but different terraform files (k3s.tfvarsKeycloak block vsk3s.tfvarsservices block). Migration-slot-style collision is possible if dispatched in parallel — recommend serializing dispatch (S1 first → tofu apply → S2) perfeedback_migration_slot_coordination. Flag for Ava when promoting totodo. - Downstream: O1 onboarding (#1074) and the existing in-progress streamlit-keycloak QA items (#960, #961, #962) all depend on this client landing in Keycloak.
- Supersedes: closed-without-code
westside-streamlit#10(board #960 in qa). Lineage explicitly documented in issue body.
Acceptance Criteria Assessment
Strong. All 9 ACs are programmatically verifiable:
tofu plan -lock=falseoutput is the canonical evidence — provides create-plan diff forkeycloak_openid_client.westside-streamlitclient_id,public_client,pkce_code_challenge_method, redirect URIs, web origins, andinclude_realm_roles_mapperare all observable in plan outputtofu validate+tofu fmtcommands are concrete and CI-runnable- "No changes to
westside-landingorwestside-spa" is verifiable by grepping the plan diff for those keys
Blast Radius
- No write side-effects on existing clients — the Keycloak Terraform provider creates new clients additively when keys are added to the map. Existing clients are untouched provided the dev agent does not modify their map entries.
- Realm-roles mapper:
include_realm_roles_mapper = trueemits a separatekeycloak_openid_user_realm_role_protocol_mapperresource (or equivalent in the terraform-keycloak provider). Addsrealm_access.rolesclaim to JWT — required byapp.py:26-31admin gate. - Funnel auth gate (
feedback_funnel_requires_auth): This Keycloak client is the auth verification mechanism for the eventual funnel exposure ofwestside-streamlit.tail5b443a.ts.net. Per the memory, any funnel ingress requires verified auth documented in the PR. This ticket lands the auth side; the corresponding funnel ingress lands separately (board #937 / westside-streamlit#6, currently backlog). - Ordering with funnel: Confirm Keycloak client merges and applies BEFORE the funnel ingress is exposed publicly — otherwise the canonical hostname returns the dashboard without auth for the gap window.
Decomposition Assessment
Single file, single map entry, 9 well-bounded ACs, copy-verbatim instruction. Estimated agent work: 5-15 minutes (write entry, run
tofu fmt/validate/plan -lock=false, capture plan output for PR body, open PR). No decomposition needed.Recommendation
Verdict: APPROVED. Ticket is ready to advance from
backlogtotodo. The two prior informal review rounds did the heavy lifting (lockingpublic_client = truePKCE S256). Live verification confirms file targets, line references, schema support, and absence of existing streamlit references in terraform.[SCOPE]Convention gap:arch-services-tfnote does not exist. Same gap on sibling #1069. Recommend Ava create a single platform-level architecture note for thepal-e-servicesterraform module shape (Keycloak clients map, services map, realms) to close the traceability triangle for allarch:services-tflabels. Do NOT block this ticket on it.[SCOPE]Dispatch ordering: serialize #1069 (S1) before #1070 (S2) to avoid simultaneous edits toterraform/k3s.tfvars. Same-file collision risk perfeedback_migration_slot_coordination.[SCOPE]Funnel sequencing: ensure board #937 (Tailscale funnel exposure) lands AFTER #1070 has merged and applied, so the public hostname is gated from first byte. Perfeedback_funnel_requires_auth.- No
[BODY]or[LABEL]changes required. Issue body and labels are clean.
-
Review: O1 — Marcus onboarding: browser-SSO + dashboard walkthrough (final ticket)
review-1074-2026-04-22Verdict: APPROVED
Final ticket in the
story:reachablearc for westside-streamlit. Validates Marcus can independently see his four data surfaces (contracts, jerseys, monthly, tournaments) via browser SSO. Two informal review rounds already passed; this is the canonical backlog→todo gate review.Template Completeness
Issue type: Feature (per
### Typeheader). Routed totemplate-issue-feature.- [x] Type — present
- [x] Lineage — present (final ticket in chain, supersedes orphan note)
- [x] Repo — present (
forgejo_admin/westside-streamlit) - [x] User Story — present (Marcus, head coach, 4 data surfaces)
- [x] Context — present (browser SSO, no Tailscale client, milestone framing)
- [x] File Targets — present, explicitly "No repo files touched" with external state change scoped (Keycloak admin role)
- [x] Acceptance Criteria — 7 items, mix of objective + observational
- [x] Test Expectations — present (screenshot, video/screenshot, retro tracking)
- [x] Constraints — present (no Tailscale client, no docs unless gap, no SQL training, scope-fenced)
- [x] Checklist — present
- [x] Related — present (project, story, sibling stories, feedback notes, blocked-by chain)
Traceability
- [x]
story:reachablelabel — Dashboard Reachable from Phone - [x] story note verified —
story-westside-streamlit-reachable(id 1401) exists; entry present inproject-westside-streamlituser-stories table (row "reachable" → Platform + Head Coach) - [x]
arch:keycloak-oidclabel — Keycloak OIDC component - [ ] arch note MISSING —
arch-keycloak-oidcnot found viasearch_notes. Architectural decisions ARE documented inline inproject-westside-streamlitkey-decisions (same-realm SSO, in-app integration via streamlit-keycloak, admin realm role gating, canonical hostname). [SCOPE] Createarch-keycloak-oidcnote covering realm topology, client registration, role mapping, JWT claim contract — shared across westside-streamlit children #10/#11/#12 and this onboarding ticket. Non-blocking; previously flagged on reviews 938, 961, 962. - [x] Forgejo issue — westside-streamlit#16, open, body matches ticket title
File Targets
Explicitly zero repo files. External state change only:
- [x] Keycloak realm
westside-basketball— assignadminrealm role to Marcus's user via Keycloak admin console (https://keycloak.tail5b443a.ts.net). Aligns with project arch key-decision: same-realm SSO + admin realm role gating. - [x] Files NOT to touch listed defensively (
app.py,Dockerfile,requirements.txt, terraform, pal-e-deployments).
Repo Placement
OK. Filed on
westside-streamliteven though the change is in Keycloak; this is correct because the ticket completes the project'sstory:reachablearc and validates the reachable user story's acceptance criteria. Keycloak realm/client provisioning is already covered separately bypal-e-services#61(S2). No second issue required.Dependencies
Cross-repo blocker chain explicitly enumerated in body:
- Substrate:
pal-e-services#60(S1, Harbor + ArgoCD Application — open, backlog) and#61(S2, Keycloak client tf — open, backlog) - CI/ingress:
westside-streamlit#3(Woodpecker pipeline — open, todo) and#6(Tailscale funnel — open, backlog) - Content surfaces:
westside-streamlit#14Monthly (open, backlog) and#15Tournament (open, backlog) - DB:
basketball-api#510migration 048 GRANT tournament tables (open, backlog)
All seven blockers verified open via Forgejo API. The body's claim "All four data surfaces must be live for this ticket to validate against its acceptance criteria" is accurate: contracts (already present in app), jerseys (already present), monthly (#14 + read-only role), tournaments (#15 + #510). Dependencies are appropriately documented; this ticket cannot move beyond
todountil the chain clears.Acceptance Criteria
Seven criteria, all verifiable:
- Objective + tool-checkable: Keycloak role mapping (admin console screenshot), URL load on phone Safari (visual), Keycloak login first-attempt success (visual), 4 data surfaces render with correct enum coverage.
- Observational: in-person sort/filter/copy-paste demo, 5 unassisted ops tasks over 1 week, retro backlog filing.
- Test commands: explicitly "none (no code; verification is observational)" — appropriate for an onboarding ticket.
AC #6 (5 unassisted tasks in week 1) and AC #7 (week-1 retro tickets) are time-deferred — they only resolve after a one-week observation window. That's expected for a final-arc ticket; the ticket can move to
doneonly after the retro is filed.Blast Radius
Bounded:
- Granting
adminrealm role to Marcus exposes the dashboard to him only — no spillover. Other parents/coaches in the realm continue to hit 403. - App-layer + auth-layer + DB-layer read-only enforcement (per project arch key-decisions: defense in depth) means Marcus cannot accidentally write data even with admin role.
- No third-party comms, no email send, no PII export. Safe per
feedback_funnel_requires_auth— funnel is Keycloak-gated. - Constraint "do not onboard other Westside staff" properly fences scope; future staff is a follow-up story.
Decomposition Assessment
5-minute rule: file targets = 0, repos touched = 0 (external state only), AC count = 7 but five are observation/retro flags rather than discrete work units. Active work = one Keycloak admin-console click (role assignment) + one in-person walkthrough + one date-tracking checklist update. Estimated agent + human time: well under 5 minutes for execution; week-long passive observation is not work-blocking. No decomposition needed.
Recommendation
[SCOPE]Createarch-keycloak-oidcarchitecture note (realm topology, client registration pattern, admin role mapping, streamlit-keycloak JWT claim contract). Non-blocking for this ticket — decisions already inlined onproject-westside-streamlit. Previously flagged on reviews 938/961/962. File as a separate doc-fix backlog item; do not block O1 advancement on it.[SCOPE](informational, per caller context) The misframed user-stories table onproject-westside-streamlitis a separate doc-fix item and is NOT a blocker for this ticket. Confirmed.
This ticket is APPROVED to advance from
backlog→todo. It will remaintodountil the upstream blocker chain (S1, S2, #3, #6, #14, #15, #510) clears. BothREADYandAPPROVEDare passing verdicts per thecheck-board-advancehook. -
Review: C1 — app: Monthly subscription section
review-1071-2026-04-22Verdict: APPROVED
Board item: #1071 — C1 — app: Monthly subscription section
Forgejo issue: forgejo_admin/westside-streamlit#14 (open)
Type: Feature
Reviewer: Dottie
Date: 2026-04-22
Round: 3 (informal rounds 1+2 already completed; round 2 was APPROVED after placeholder + enum prose fixes)Template Completeness
All Feature template sections present:
- [x] Type — Feature
- [x] Lineage — standalone, discovered 2026-04-23, blocked-by called out
- [x] Repo — forgejo_admin/westside-streamlit
- [x] User Story — full Marcus narrative with role/want/so-that
- [x] Context — schema source, enum source, GRANTs already in place, mirror pattern called out
- [x] File Targets — single file (app.py), explicit "files NOT to touch" list
- [x] Acceptance Criteria — 9 criteria, all observable
- [x] Test Expectations — ruff check + format, run commands listed
- [x] Constraints — mirror pattern, single-file, tenant filter, no deps, enum source-of-truth rule
- [x] Checklist — PR/lint/render/no-unrelated
- [x] Related — links to story note, project, parent project, hands-off feedback memory
Traceability
- [x] story:monthly label — Monthly Subscription Visibility
- [x] story note verified —
story-westside-streamlit-monthly(id 1593) exists, taguser-story, status active, project westside-streamlit. Role/want/so-that match issue body. - [~] story note NOT yet listed in
project-westside-streamlituser-stories table — table currently shows triage / blast / clusters / jerseys / audit / reachable. Missing:monthlyrow +tournamentrow (separate ticket #1073's story). - [x] arch:streamlit-app label — references the streamlit app component
- [~] arch note
arch-streamlit-appnot located via slug lookup. Architecture is described inproject-westside-streamlitArchitecture section, which is the convention for this small project. Acceptable for this round. - [x] Forgejo issue — open, body well-formed
- [x] type:feature, track:backend, scope:planned labels — all present
File Targets
- [x]
app.py— verified at/home/ldraney/westside-streamlit/app.py, 211 lines. "After line ~211" matches actual EOF. - [x]
app.py:36-39q()helper — verified, matches issue claim (cached psycopg + pandas read_sql). - [x]
app.py:51-112Offered pool — verified as the mirror pattern (header → query → filters → dataframe → cluster detection → cohort export). - [x]
app.py:103-109parent-cluster detection — verified, exact pattern referenced in AC #6. - [x]
app.py:111-112email cohort export — verified (st.text_area pattern). - [x]
~/basketball-api/src/basketball_api/models.pySubscriptionStatus— verified at lines 101-105. Four values exactly:active,past_due,canceled,none. Constraint to read enum from source (not hardcode) is sound. - [x] Migration 044 GRANTs — verified at
~/basketball-api/alembic/versions/044_add_westside_streamlit_ro_role.py.GRANT_TABLESincludesplayers,parents,teams,player_teams.subscription_status+monthly_feecolumns live on the already-grantedplayerstable — no new GRANT needed. Issue claim is accurate.
Repo Placement
OK. Issue is filed on
westside-streamlit; the only file change isapp.pyin that repo. Thebasketball-apireference is read-only (consult the enum file, no edits) — explicitly called out in "Files NOT to touch" and consistent withfeedback_basketball_hands_off.Dependencies
Blocked-by chain explicit in issue body:
forgejo_admin/pal-e-services#60— Harbor + ArgoCD Application (open, board item #1069 backlog)forgejo_admin/pal-e-services#61— Keycloak client (open, board item #1070 backlog, supersedes #10)
Substrate must be live before this dashboard change can deploy. Not a scope problem — correctly noted as a blocker, and #1071 is in
backlog(won't entertodo/next_upuntil substrate clears). Sibling related ticket #1074 (O1 onboarding) is the final downstream consumer.Acceptance Criteria
All 9 criteria are observable and verifiable by an agent:
- Header presence — DOM check after Jerseys section
- 4 metric cards — count + label match enum values
- Filterable grid columns — column list specified
- Filter widgets — Program (3-way) + Status (4-way radio)
- Totals caption — "Showing N of M" pattern matches existing
- Parent-cluster block — pattern referenced by line range
- Email cohort textarea — pattern referenced by line range
q()usage + read-only — testable by code review (no INSERT/UPDATE/DELETE)- No new dependency — testable by
git diff requirements.txt
Test commands (
ruff check app.py,ruff format --check app.py) are real and run cleanly today.Blast Radius
Low.
- Single file, append-only section.
- Read-only SQL — no writes to basketball-api or its DB.
- No schema/ACL changes — column already exposed via existing GRANT.
- No new dependency — uses existing
q()helper, existingst.dataframe+st.text_areapatterns. - Auth gate at
app.py:13-31already in place — Keycloak admin role required before any query runs. New section inherits this gate by virtue of being below it. - No similar buggy pattern to grep for — this is additive, not a fix.
- Downstream: Marcus dashboard view, no API consumers.
Decomposition Assessment
No decomposition needed. 5-minute rule:
- 1 file target across 1 repo — well under the >3/>2 threshold
- 9 AC, but they are all features of one section, not independent work — verifiable in one screenshot
- Estimated agent work: append ~50-60 lines mirroring the Offered pool pattern, ~3-5 minutes
- Single PR, single review
Recommendation
APPROVED — ready to advance to
todoonce Lucas confirms.One discovered scope item (does NOT block this ticket):
[SCOPE]Addmonthlyrow toproject-westside-streamlituser-stories table linking tostory-westside-streamlit-monthly(id 1593). Story note exists and is correct — only the project-page table needs the new row. Thetournamentstory for sibling ticket #1073 has the same gap. Perfeedback_discovered_scope_always_tracked, this should become its own backlog item rather than be folded into #1071.
No
[BODY],[LABEL], or[DECOMPOSE]recommendations — issue body, board labels, and ticket size are all correct.Verification Provenance
- Forgejo API: GET /repos/forgejo_admin/westside-streamlit/issues/14 (open, body matches input)
- Forgejo API: GET /repos/forgejo_admin/pal-e-services/issues/60 (open) and #61 (open) — blockers confirmed open
- Filesystem:
~/westside-streamlit/app.py(211 lines, all referenced lines match) - Filesystem:
~/basketball-api/src/basketball_api/models.py:101-105(SubscriptionStatus enum, 4 values) - Filesystem:
~/basketball-api/alembic/versions/044_add_westside_streamlit_ro_role.py(GRANT_TABLES verified) - pal-e-docs:
story-westside-streamlit-monthlyid 1593 (exists, project-bound, user-story tag) - pal-e-docs:
project-westside-streamlituser-stories section (no monthly row — recorded as discovered scope) - pal-e-docs:
board-westside-streamlitvia list_board_items (item #1071 in backlog, dependencies #1069/#1070 also in backlog)
-
Review: Add kustomize overlay in pal-e-deployments (westside-streamlit #4) — Round 2
review-935-2026-04-11-r2Verdict: APPROVED
Round-2 review of board item #935 / forgejo_admin/westside-streamlit#4. Body rewritten 2026-04-11 after round-1 NEEDS_REFINEMENT. All 5 round-1 findings addressed. Conventions verified against live
~/pal-e-deploymentsrepo. Ready to advance backlog → todo → next_up.Round-1 Findings Resolution
- [x] Finding 1 — Base pattern (invented per-service base): FIXED. Body now consumes shared
bases/standard+bases/servicemonitorviaresources: [../../../bases/standard, ../../../bases/servicemonitor], with JSON6902 rename patches turning baseapp→westside-streamlitfor Deployment/Service/ServiceMonitor name + labels + container name. Matchesoverlays/basketball-api/prod/kustomization.yamlexactly. - [x] Finding 2 — Overlay directory structure (flat, not /prod/): FIXED. All file targets live under
overlays/westside-streamlit/prod/. Matchesoverlays/basketball-api/prod/andoverlays/westsidekingsandqueens/prod/convention. - [x] Finding 3 — Hostname discrepancy: Ticket uses
westside-streamlit.tail5b443a.ts.netconsistently. Story/arch notes still saywestside-ops.tail5b443a.ts.net— flagged by reviewer as KNOWN DOC DRIFT to be fixed in a separate update pass, NOT a defect of this ticket body. Not blocking approval. Recommend doc update ticket on board-westside-streamlit to updatestory-westside-streamlit-reachable+arch-deployment-westside-streamlitto the new hostname. - [x] Finding 4 — ArgoCD Application location undefined: FIXED. Body now explicitly marks the ArgoCD Application declaration as Out of Scope: "pal-e-deployments has no Application YAML convention; Applications are managed externally in pal-e-platform terraform. A follow-up ticket will add the ArgoCD Application entry. Without it,
kubectl apply -k overlays/westside-streamlit/prod/is the manual sync path until the follow-up lands." Dev agent has an unambiguous path. - [x] Finding 5 — NetworkPolicy ambiguity (additive vs replacement): FIXED by omission. Body no longer lists a per-overlay
networkpolicy.yamlas a file target, and notes the base NetworkPolicy is "temporarily disabled per kube-router ipset bug." Matches how basketball-api + westsidekingsandqueens handle this today (commented-out patches). No ambiguity left for the agent.
Convention Verification (live repo grep 2026-04-11)
- [x]
bases/standard/andbases/servicemonitor/exist (only two shared bases) - [x]
overlays/basketball-api/prod/containsdeployment-patch.yaml,harbor-creds.enc.yaml,kustomization.yaml— matches body's proposed file set (minus app-specific extras) - [x]
overlays/westsidekingsandqueens/prod/containsingress.yamlalongside the standard files — confirms per-overlayingress.yamlis a valid pattern (ingress not in base) - [x]
overlays/westside-email/prod/namespace.yamlexists — confirms separatenamespace.yamlis a valid pattern (2 of ~15 services use it, rest inline or manage externally) - [x]
.sops.yamlcreation_rulesregex:\.enc\.yaml$— confirms body's requirement that SOPS files use.enc.yamlsuffix - [x] JSON6902 rename pattern in basketball-api/prod/kustomization.yaml matches body spec exactly (
kind: Deployment,name: app, replace ops on/metadata/name,/spec/selector/matchLabels/app,/spec/template/metadata/labels/app,/spec/template/spec/containers/0/name) - [x]
overlays/pal-e-streamlit/orphan confirmed present for deletion (trackeddev/ingress.yamlfrom incident PR #109)
Template Completeness
- [x] Type (Feature), Lineage, Repo, User Story, Context, File Targets (create + delete), Acceptance Criteria (11 testable), Test Expectations, Constraints, Checklist, Related — all present
- [x] Revision history noted in body ("Revised again 2026-04-11 post-scope-review-round-1")
- [x] Cluster-side manual cleanup documented for PR body (not agent-run)
- [x] Out-of-scope items explicitly enumerated (ArgoCD Application, Keycloak wiring)
Traceability
- [x] story:reachable — label present;
story-westside-streamlit-reachablenote exists and lists this work - [x] arch:k8s-deploy — label present;
arch-k8s-deploybacking note still missing (platform-wide gap, waived per round-1 precedent) - [x] Forgejo issue #4 — open, scoped to
forgejo_admin/pal-e-deployments - [x] Incident
incident-2026-04-10-pal-e-streamlit-public-funnellinked - [x] Parent/child relationships to #2 (merged), #3 (blocks-on-this), #5 (merged), #6 (hostname dep), #10/#11/#12 (Keycloak children) all enumerated
File Targets
- [x]
overlays/westside-streamlit/prod/kustomization.yaml— matches convention, shared bases, JSON6902 rename, image pinning by Woodpecker - [x]
overlays/westside-streamlit/prod/deployment-patch.yaml— additive env/probes/resources/securityContext/strategy, targets baseapp - [x]
overlays/westside-streamlit/prod/service-patch.yaml(or inline) — port/targetPort override acceptable either way - [x]
overlays/westside-streamlit/prod/ingress.yaml— tailnet-private, NO funnel annotation, hostnamewestside-streamlit.tail5b443a.ts.net - [x]
overlays/westside-streamlit/prod/harbor-creds.enc.yaml— SOPS-encrypted, convention match - [x]
overlays/westside-streamlit/prod/secret.enc.yaml— SOPS-encryptedwestside-streamlit-secretswithPGURLfromwestside_streamlit_ro(matches #5 migration) - [x]
overlays/westside-streamlit/prod/namespace.yaml— restricted PSA label, valid pattern (precedent: westside-email) - [x] DELETE
overlays/pal-e-streamlit/— in same PR, supersedes incident PR #109 partial
Repo Placement
OK. All file changes in
forgejo_admin/pal-e-deployments. Cluster-side namespace cleanup documented as post-merge manual commands for Lucas.Dependencies
- Upstream satisfied: #2 Dockerfile MERGED, #5 Postgres role MERGED
- Downstream: #3 Woodpecker CI blocks on this (needs
overlays/westside-streamlit/prod/kustomization.yamlas image-tag target); #12 Keycloak env wiring additive post-this; #6 funnel depends on #7 Keycloak chain landing first - Follow-up identified: ArgoCD Application in pal-e-platform terraform (separate ticket)
- Follow-up identified: Story/arch hostname doc drift (separate ticket, NOT blocking this one)
Acceptance Criteria
11 programmatically verifiable criteria:
kustomize buildexit 0,kubectl apply --dry-run=serverpasses, rendered names match, namespace PSA label, ingress class + no funnel, orphan deleted, SOPS decrypt, correct PG credentials, PR body includes build/dry-run output + manual cleanup commands. All testable in a local dev agent pass.Blast Radius
- Migration risk: LOW — no live ArgoCD app watches the orphan overlay, deletion is pure filetree cleanup. Cluster untouched until Lucas runs manual
kubectl delete namespacepost-merge. - Harbor project: body specifies
harbor.tail5b443a.ts.net/westside/westside-streamlit. Perfeedback_harbor_project_naming, agent should verifywestsideHarbor project exists before image pull works (acceptance-time check, not scoping-time). - Hostname doc drift (
westside-opsin story/arch vswestside-streamlitin ticket): known, flagged separately, not blocking. - No similar-pattern bugs detected elsewhere — conventions now match the 15 existing service overlays.
Decomposition Assessment
- Discrete file changes: 7 creates + 1 directory delete = 8 discrete changes, all in one directory tree
- Acceptance criteria: 11 — above the 5-AC rule of thumb, but all tightly coupled to a single overlay landing; splitting would create artificial seams
- Estimated agent time: 8-12 minutes (clone, copy basketball-api/prod as template, adapt, SOPS encrypt, kustomize build, dry-run, PR)
- Borderline on 5-min rule but no decomposition — all changes are one cohesive overlay bundle; splitting would force a dev agent to land half a service
Recommendation
APPROVED. No action needed on the ticket body.
- All 5 round-1 findings resolved in the 2026-04-11 rewrite.
- Conventions verified against live
~/pal-e-deploymentsrepo (bases, overlay structure, SOPS regex, JSON6902 patches, namespace pattern). - Out-of-scope items (ArgoCD Application, Keycloak) explicitly deferred with unambiguous paths.
- Known residual: story/arch hostname doc drift — tracked separately, not blocking this ticket.
- Ready for todo → next_up advance and dev dispatch.
- [x] Finding 1 — Base pattern (invented per-service base): FIXED. Body now consumes shared
-
Review: Deployment Secret + env wiring for Keycloak (child of #7)
review-962-2026-04-11Verdict: NEEDS_REFINEMENT
Board item #962 —
forgejo_admin/westside-streamlit#12. Child C of #7 Keycloak decomposition.Template Completeness
- [x] Type = Feature
- [x] Lineage / User Story / Context / Repo / File Targets / Acceptance Criteria / Test Expectations / Constraints / Checklist / Related — all present
Traceability
- [x] story:reachable label — "Marcus can open the dashboard on his phone with Keycloak SSO"
- [x] story note verified —
story-westside-streamlit-reachablelisted in project-westside-streamlit user-stories - [x] arch:keycloak-oidc label
- [ ] arch note MISSING — [SCOPE] Create architecture note
arch-keycloak-oidc(search_notes returns nothing). Affects all three #7 children. - [x] Forgejo issue — https://forgejo.tail5b443a.ts.net/forgejo_admin/westside-streamlit/issues/12 open
- [x] parent:7 label — umbrella linkage documented
File Targets
- [ ]
overlays/westside-streamlit/secret-keycloak.sops.yaml— ISSUE: repo SOPS convention is*.enc.yaml, not*.sops.yaml..sops.yamlcreation_rules regex is\.enc\.yaml$. A.sops.yamlfile will NOT be auto-encrypted by SOPS and will commit plaintext. Rename tosecret-keycloak.enc.yaml. Precedent:overlays/basketball-api/prod/harbor-creds.enc.yaml. - [ ]
overlays/westside-streamlit/deployment.yaml— ISSUE: existing overlays don't ship a literaldeployment.yaml. Pattern isbases/standard+ adeployment-patch.yaml(seeoverlays/basketball-api/prod/). Env additions should go in a deployment-patch patching the base Deployment, not a new deployment.yaml. - [ ] Overlay directory name — ISSUE: existing streamlit overlay is
overlays/pal-e-streamlit/and overlays follow{service}/{dev,prod}/layout. Ticket writesoverlays/westside-streamlit/with no env subdir. Needs alignment with #4's actual directory structure before this ticket runs. - [x]
kustomization.yaml— correct file to register new Secret resource
Repo Placement
Correct repo:
forgejo_admin/pal-e-deployments. Single-repo change.Dependencies
Sequencing documented and correct: #4 (creates overlay Deployment/patch) → #10 Child A (creates Keycloak client, produces secret value) → this ticket (Child C) → lands alongside #11 Child B (consumes env vars). Hard-blocks #6 Tailscale funnel. Board state: #4, #10, #11 all in backlog; #7 in next_up. This ticket cannot enter in_progress until #4 and #10 are merged.
Acceptance Criteria
7 ACs, all testable: SOPS round-trip (
sops -d), AC for single-key-only content, env wiring verified viakubectl exec ... env | grep KC_, rollout status, kustomize build. Real commands. Only gap: AC does not assert thegit statusplaintext-gate — it's in Constraints but not an AC. Minor.Blast Radius
Purely additive to an overlay that doesn't exist yet — near-zero blast radius for other services. The
KC_CLIENT_SECRETrotation story inherits from Child A. ArgoCD sync will pick up the new Secret + Deployment patch on merge. Watch: basketball-api uses the same age recipient; any SOPS tooling mistake here could theoretically affect that service's round-trip if decryption config is shared.Decomposition Assessment
3 files, 7 ACs, single repo, purely additive. Estimated <5 min agent work. No decomposition needed.
Recommendation
[BODY]Renamesecret-keycloak.sops.yaml→secret-keycloak.enc.yamlin File Targets, Acceptance Criteria, and Test Expectations. Match the repo's existing SOPScreation_rulesregex (\.enc\.yaml$). Cite precedentoverlays/basketball-api/prod/harbor-creds.enc.yaml.[BODY]Replaceoverlays/westside-streamlit/deployment.yamlwithoverlays/{overlay-name}/{env}/deployment-patch.yaml(additive patch to the base Deployment, not a literal deployment.yaml). Match the basketball-api overlay pattern.[BODY]Align overlay directory name + env subdir with #4 output. Eitheroverlays/pal-e-streamlit/{dev,prod}/or confirm #4 createsoverlays/westside-streamlit/{dev,prod}/. Add env subdir to all three file paths.[BODY]Promote the "git status before commit — no plaintext" gate from Constraints into an Acceptance Criterion so QA explicitly verifies.[SCOPE]Create architecture notearch-keycloak-oidccovering realm, client, redirect URIs, and the env var contract. Blocks arch traceability for #7, #10, #11, and #12.
-
Review: Add kustomize overlay in pal-e-deployments (westside-streamlit #4)
review-935-2026-04-11Verdict: NEEDS_REFINEMENT
Post-incident revision is thorough on intent (migration sequencing, Keycloak separation, no funnel annotation) but introduces several file-path and convention mismatches against the existing
pal-e-deploymentslayout that would force the dev agent to guess. Fix these in the body and this ticket is ready.Template Completeness
- [x] Type — Feature
- [x] Lineage — story:reachable | arch:k8s-deploy | type:infra
- [x] Repo — forgejo_admin/pal-e-deployments
- [x] User Story — platform operator + migration retirement
- [x] Context — current cluster state, relationship to Keycloak children
- [x] File Targets — create + delete lists
- [x] Acceptance Criteria — 12, all testable
- [x] Test Expectations — kustomize build / dry-run / SOPS / reachability
- [x] Constraints — funnel prohibition, migration coupling, no Lucas-bypass
- [x] Checklist — present
- [x] Related — #2, #5, #3, #7 children, incident note
Traceability
- [x] story:reachable — present on board item
- [x] story note verified —
story-westside-streamlit-reachableexists, explicitly lists "Add kustomize overlay in pal-e-deployments" as supporting ticket #3 - [x] arch:k8s-deploy — present on board item
- [ ] arch note MISSING — no
arch-k8s-deploynote in pal-e-docs. Platform-wide gap (same as #933 arch:ci-pipeline). Treat as waiver perreview-933-2026-04-10precedent — not a blocker unique to this ticket. - [x] Bonus —
arch-deployment-westside-streamlitexplicitly enumerates "Kustomize overlay in pal-e-deployments" as gap item #4 - [x] Forgejo issue — https://forgejo.tail5b443a.ts.net/forgejo_admin/westside-streamlit/issues/4 (open)
- [x] Incident linkage —
incident-2026-04-10-pal-e-streamlit-public-funnelcross-referenced
File Targets
- [ ]
bases/westside-streamlit/kustomization.yaml— CONVENTION MISMATCH. Every existing service (basketball-api, pal-e-docs, mcd-tracker, twitch-2k-wager, westside-ai-assistant, westsidekingsandqueens, gcal-scheduler, pal-e-production, platform-validation, pal-e-mail) uses the sharedbases/standardbase with per-overlay kustomize patches that rename the genericappDeployment/Service. Onlybases/standardandbases/servicemonitorexist today. Inventing a per-service base for westside-streamlit is a new pattern. - [ ]
bases/westside-streamlit/deployment.yaml— same convention mismatch. Seeoverlays/basketball-api/prod/kustomization.yamlfor the patch-the-shared-base pattern. - [ ]
bases/westside-streamlit/service.yaml— same. - [ ]
overlays/westside-streamlit/kustomization.yaml— DIRECTORY STRUCTURE MISMATCH. All existing overlays useoverlays/{service}/prod/(ordev/) subdirectory. Ticket specifies a flatoverlays/westside-streamlit/. ArgoCD Application paths will break if this convention is broken. - [ ]
overlays/westside-streamlit/namespace.yaml— same structure mismatch. Also, base Deployment inbases/standard/deployment.yamlalready handles namespace via overlay kustomization; confirm namespace.yaml is needed at all. - [x]
overlays/westside-streamlit/secret.enc.yaml— SOPS encrypted Secret is a valid pattern (used by all services; typically namedharbor-creds.enc.yamlfor Harbor creds — this one is app secret, name is fine). - [ ]
overlays/westside-streamlit/networkpolicy.yaml— PATH AMBIGUITY.bases/standard/networkpolicy.yamlalready exists with default ingress rules (from tailscale + monitoring namespaces). A per-overlay networkpolicy.yaml would add a second NetworkPolicy in parallel, not patch the base. If the intent is egress topostgres+ kube-dns, that's a new policy (valid) — ticket should clarify it's additive to the base, not a replacement. - [x]
overlays/westside-streamlit/ingress.yaml— no funnel annotation, tailnet-private. Correct. Supersedes orphan atoverlays/pal-e-streamlit/dev/ingress.yaml. - [x]
overlays/pal-e-streamlit/DELETE — verified: this directory contains 5 files, onlydev/ingress.yamlis tracked in git (committed via PR #109 at commit693721c). The other 4 files are untracked locally. Deletion in the same PR is clean. - [ ] ArgoCD Application file — UNKNOWN LOCATION. Ticket says "check existing services — likely
argocd-apps/or per-serviceapplication.yaml." Verified: neither exists in pal-e-deployments. Noargocd-apps/directory, noapplication.yamlfiles anywhere. ArgoCD Applications are declared externally (likelypal-e-platformtofu or managed via ArgoCD UI). Dev agent will get stuck here without clarification.
Repo Placement
OK. All changes live in
forgejo_admin/pal-e-deployments. Cluster-side cleanup is documented in PR body, not a repo change.Dependencies
- Upstream satisfied: #2 Dockerfile merged (image exists), #5 Postgres role merged (credentials exist).
- Downstream blocked by this ticket: #3 Woodpecker CI (needs overlay kustomization.yaml to image-update), #10/#11/#12 Keycloak children (additive to this Deployment), #6 Tailscale funnel (uses this hostname once Keycloak lands).
- Not blocked by anything in progress.
- Keycloak child sequencing is explicitly enumerated in ticket body — good.
Acceptance Criteria
Strong. All 12 AC are programmatically verifiable:
kustomize buildexit 0, ArgoCD namespace creation, probe rollout, in-cluster postgres reachability + default-deny test,_stcore/health200, SOPS decrypt, tailnet-private resolvability, public blocked at edge, orphan directory deleted, ArgoCD app Synced+Healthy, PR body documents manual cleanup, no funnel annotation. The only soft criterion is the ArgoCD Application check — hard to verify when the file location is undefined.Blast Radius
- Migration risk: medium. Deleting the orphan in the same PR is safe — no ArgoCD app currently watches
overlays/pal-e-streamlit/, so the delete is a pure file-tree cleanup. Live cluster is untouched until Lucas runskubectl delete namespace pal-e-streamlitmanually post-merge. - Hostname conflict: flag — ticket uses
westside-streamlit.tail5b443a.ts.netbutstory-westside-streamlit-reachableAC andarch-deployment-westside-streamlittarget diagram both saywestside-ops.tail5b443a.ts.net. The incident postmortem action items list "Rename pal-e-streamlit → westside-streamlit (namespace + hostname + local dir + overlay path)" supportingwestside-streamlit— but story/arch saywestside-ops. Reconcile before #6 funnel ticket lands. - Harbor project naming: per
feedback_harbor_project_naming, Harbor projects derive from image_repo prefix. Ticket specifiesharbor.tail5b443a.ts.net/westside/westside-streamlit:latest. Agent should verify thewestsideHarbor project exists before the kustomize build is valid at runtime. - Similar-pattern check: no other service has the per-service-base structure this ticket proposes, so convention drift is localized.
Decomposition Assessment
- Discrete file changes: 8 creates + 1 directory delete + 1 ArgoCD Application = ~10 discrete changes.
- File count: ~10 files, 1 repo — above 3-file threshold but all in one directory tree.
- Acceptance criteria: 12 — above 5-AC threshold.
- Estimated agent time: 10–15 minutes if conventions are clarified, longer if not.
- Parallelizable subtasks: no — all manifests are tightly coupled for one service landing.
Borderline — at the upper edge of the five-minute rule. No decomposition recommended if the convention ambiguities are resolved in the body; if the agent has to guess at base vs patch pattern or ArgoCD Application location, it will stall. Keep as a single ticket but refine body.
Recommendation
[BODY]Clarify base pattern — either (a) change file targets to usebases/standardwith per-overlay rename patches (matching every other service), OR (b) justify in the ticket body why westside-streamlit needs its own base. Current body is ambiguous and would force the agent to invent a new convention.[BODY]Fix overlay directory structure — changeoverlays/westside-streamlit/tooverlays/westside-streamlit/prod/(or/dev/) to match the existing convention across all 11 services.[BODY]Resolve hostname discrepancy — ticket sayswestside-streamlit.tail5b443a.ts.net, story + arch note saywestside-ops.tail5b443a.ts.net. Pick one and update the losing document.[BODY]Clarify ArgoCD Application location — noargocd-apps/directory and noapplication.yamlfiles exist in pal-e-deployments. Either (a) confirm Applications are managed elsewhere (pal-e-platform tofu or UI) and remove the file-target from this ticket (track as follow-up in that other repo), OR (b) specify the new path here and accept that this ticket is the first to establish the convention.[BODY]Clarify networkpolicy intent — specify whether the per-overlay policy is additive (egress rules) on top of the base ingress policy, or a replacement. Recommend additive.[SCOPE]Create architecture notearch-k8s-deploy— platform-wide gap, same asarch-ci-pipelinein #933. Waived for this ticket per #933 precedent.- Migration sequencing, Keycloak separation, and funnel prohibition are all handled correctly in the current body — no action on those.
-
Review: streamlit-keycloak in-app integration (#961)
review-961-2026-04-11Verdict: READY
Board item #961 — Forgejo issue
forgejo_admin/westside-streamlit#11— Child B of parent #7 Keycloak decomposition.Template Completeness
- [x] Type (Feature)
- [x] Lineage with parent:7
- [x] Repo
- [x] User Story (two-voice: Marcus + operator)
- [x] Context — explicitly justifies streamlit-keycloak over oauth2-proxy
- [x] File Targets (requirements.txt + app.py)
- [x] Acceptance Criteria (8 items, all testable)
- [x] Test Expectations
- [x] Constraints
- [x] Checklist
- [x] Related (parent, siblings, blockers)
Traceability
- [x] story:reachable — verified:
story-westside-streamlit-reachablenote exists - [x] arch:keycloak-oidc label present
- [ ] arch note MISSING —
arch-keycloak-oidcnot found in pal-e-docs. [SCOPE] Create architecture notearch-keycloak-oidc(shared across Children A/B/C, non-blocking for this ticket). - [x] parent:7 label links to umbrella
- [x] Forgejo issue #11 — open
File Targets
- [x]
requirements.txt— verified via Forgejo raw API. Currently pinsstreamlit==1.56.0,psycopg[binary]==3.3.3,pandas==3.0.2. Appendingstreamlit-keycloakis clean. - [x]
app.py— verified. Importsos/streamlit/psycopg/pandas, definesq()usingpsycopg.connect(DB), and the FIRSTq()call (counts = q(...)) fires immediately afterst.title. Correct insertion point for the auth wrapper is abovest.set_page_config/st.titleso that no SQL runs for non-admin users. Defense-in-depth guarantee is achievable with the proposed wrapper.
Targets are specific enough for an agent to act on without guessing.
Repo Placement
OK — all changes in
forgejo_admin/westside-streamlit. Child A (Keycloak client) is terraform, Child C (Secret+env) ispal-e-deploymentskustomize. Clean three-repo separation with an explicit env var contract gluing them together.Dependencies
- [pending] #10 Child A Keycloak client resource — needed for full E2E, NOT needed for local dev
- [pending] #12 Child C deployment Secret + env wiring — needed for full E2E, NOT needed for local dev
- [pending] #6 Tailscale funnel — hard-blocks public exposure, does not block code development
Cross-child env contract is explicit (
KC_SERVER_URL,KC_REALM,KC_CLIENT_ID,KC_CLIENT_SECRET) with example values. Child B code CAN be developed and committed against a local.envbefore A/C land — ticket explicitly documents this path in AC#8. Ordering is understood and tracked.Acceptance Criteria
All 8 criteria are testable by a fresh-context agent. Critical items verified: AC#2 pins the "auth before any
q()" defense-in-depth rule; AC#6 requires JWT claims inst.session_statefor downstream role-based filtering; AC#7 requires explicit failure mode on missing env vars; AC#8 mandates local-dev docs in the PR body. Test Expectations adds query-log inspection to prove non-admin flow executes ZEROSELECTstatements — belt and suspenders.Blast Radius
Narrow. Single repo, two files, purely additive. No sibling Streamlit apps exist in the platform. Rollback = revert PR. One minor cross-doc inconsistency:
story-westside-streamlit-reachablementions awestside-opsrole in its AC, while this ticket checks theadminrealm role. Not blocking — ticket's choice is defensible — but flagged as a scope recommendation.Decomposition Assessment
Three-thing limit: 2 discrete changes (dep add, app.py wrapper). Five-minute rule: estimated < 5 min agent work — one-line
requirements.txtappend plus ~30 lines of auth wrapper at the top ofapp.py. 1 repo, 2 files, 8 AC. Passes comfortably. No further splitting warranted; this ticket is itself the product of parent #7's decomposition.Specific Router Checks
- [x] Picks
streamlit-keycloak(in-app) NOT oauth2-proxy (sidecar) — justified with three reasons: JWT claim visibility inst.session_state, future role-based data filtering, Python-native alignment. - [x] Admin-role check runs BEFORE any
q()SQL call — AC#2 + Test Expectations enforce; file target analysis confirms insertion point is above the firstq(). - [x] Env var contract (KC_SERVER_URL, KC_REALM, KC_CLIENT_ID, KC_CLIENT_SECRET) defined clearly for A and C to implement against — example values provided.
- [x] Denied-page UX specified —
st.error("Access denied — admin role required")+st.stop(), friendly not raw 403. - [x]
session_stateusage correct — AC#6 requires JWT claims land there for downstream filtering. - [x] Ticket code DEVELOPABLE before A/C land — explicit local
.envpath.
Recommendation
[SCOPE]Create architecture notearch-keycloak-oidc(shared across #10/#11/#12; non-blocking for this ticket but should land before umbrella #7 closes).[SCOPE]Reconcile role name betweenstory-westside-streamlit-reachable(westside-ops) and this ticket (admin). Suggest updating the story to match the ticket'sadminrealm role.
Both recommendations are non-blocking. Scope is solid, file targets verified, traceability triangle effectively complete (story verified, arch labeled, Forgejo issue open), defense-in-depth correct, cross-child contract explicit, decomposition passes. READY for todo → next_up.
-
Review: Add Dockerfile (westside-streamlit #2) — Round 2
review-933-2026-04-10-r2Verdict: APPROVED
Round 2 re-review of board item #933 on
board-westside-streamlit. Prior reviewreview-933-2026-04-10returned NEEDS_REFINEMENT with a single finding: thearch:ci-pipelinelabel had no backing architecture note in pal-e-docs. That gap is now closed.Platform Gap Resolution
- [x]
arch-ci-pipelinenote created in pal-e-platform project (verified viaget_note, id 1424, created 2026-04-10T22:11). - [x] Note is comprehensive: covers motivation, 5-step pipeline (checkout → test → Kaniko build → update-kustomize-tag → ArgoCD sync), reference implementations (basketball-api, pal-e-docs, westside-ai-assistant), hard-won constraints (YAML parse validation, update-kustomize-tag success+failure gating per f17b49b, per-service Harbor robots, .dockerignore discipline), and a 7-step new-service onboarding checklist.
- [x] Tagged
architecture,active,platform,ci. Canonical anchor for thearch:ci-pipelinelabel platform-wide. - [x] Directly supports this ticket: the onboarding checklist enumerates "Add Dockerfile" as step 1, and the constraints section codifies the python:3.12-slim + uv + non-root + .dockerignore pattern the ticket asks for.
The resolution is sufficient. The arch note is not a stub — it captures real operational knowledge and gives this ticket and every future CI-pipeline ticket a concrete reference.
Template Completeness
- [x] Type — Feature
- [x] Lineage — standalone, discovered during project bootstrap 2026-04-10
- [x] Repo — forgejo_admin/westside-streamlit
- [x] User Story — operator wants container image for pipeline deploy
- [x] Context — Streamlit runtime model vs uvicorn pattern
- [x] File Targets — Dockerfile + .dockerignore (create); app.py, requirements.txt (do-not-touch)
- [x] Acceptance Criteria — 6 testable criteria
- [x] Test Expectations — 4 concrete test commands
- [x] Constraints — base image, uv, non-root uid 1000, healthcheck, CMD
- [x] Checklist — present
- [x] Related — project + arch-deployment-westside-streamlit
No changes required to ticket body since round 1 — it was already execution-ready.
Traceability
- [x] story:reachable — label present on board item
- [x] story note verified —
story-westside-streamlit-reachableexplicitly lists "Dockerfile" as supporting ticket #1 (re-confirmed from round 1) - [x] arch:ci-pipeline — label present on board item
- [x] arch note verified —
arch-ci-pipelinenow exists in pal-e-docs (id 1424). Round-1 gap CLOSED. - [x] Forgejo issue — forgejo_admin/westside-streamlit#2, open
- [x] Bonus:
arch-deployment-westside-streamlitalso exists and enumerates this Dockerfile as gap item #1 in the target topology
All three traceability legs (story, arch, Forgejo issue) plus backing notes fully resolved.
File Targets
- [x]
Dockerfile— confirmed absent in round 1 (create target) - [x]
.dockerignore— confirmed absent in round 1 (create target) - [x]
app.py— confirmed present in round 1, do-not-touch holds - [x]
requirements.txt— confirmed present in round 1 with pinned streamlit/psycopg/pandas, do-not-touch holds
File-target verification from round 1 carries forward — ticket body unchanged.
Repo Placement
OK. Fix lives in westside-streamlit, issue filed on westside-streamlit. Single repo, no cross-repo work.
Dependencies
First ticket in the "reachable" deployment chain. Blocks #3 (Woodpecker pipeline), #4 (kustomize overlay), #6 (Tailscale funnel), #7 (Keycloak OIDC). Does not block #5 (Postgres read-only role) or #8 (blast feature). No upstream blockers. arch-ci-pipeline onboarding checklist confirms Dockerfile is step 1 — this ticket is correctly sequenced as the foundation of the pipeline build chain.
Acceptance Criteria
Strong. All 6 AC are programmatically verifiable:
docker buildexit 0,docker imagessize <250 MB,docker run+ dashboard load,docker execuid check,curl /_stcore/healthHTTP 200, missing-PGURL fast-fail. Test Expectations duplicates these as explicit commands.Blast Radius
Low. New files only, no modifications. Pattern matches platform Python service convention — agent has working references (basketball-api, mcd-tracker-api, minio-api, and now the canonical arch-ci-pipeline note). Streamlit-specific deviation (long-lived server) called out explicitly. No downstream consumers yet. Rollback trivial.
Decomposition Assessment
- Discrete changes: 2 (Dockerfile, .dockerignore) — under 3-thing limit.
- File count: 2 files, 1 repo — under decomposition threshold.
- Acceptance criteria: 6 — at the upper edge but all are single-command checks against the same artifact.
- Estimated agent time: 3–5 minutes. Within the five-minute rule.
- Parallelizable subtasks: no — both files are tightly coupled to the same build.
No decomposition needed. Fits comfortably in a single agent pass.
Recommendation
No action needed. Ticket is APPROVED and ready to advance to
todo. Round-1 platform-gap finding is fully resolved by the newarch-ci-pipelinenote, which is substantive and operationally useful beyond just this ticket. - [x]
-
Review: Add Dockerfile (westside-streamlit #2)
review-933-2026-04-10Verdict: NEEDS_REFINEMENT
Ticket body is excellent — thorough, testable, well-scoped. Only gap is the backing architecture note for the
arch:ci-pipelinelabel.Template Completeness
- [x] Type — Feature
- [x] Lineage — standalone, discovered during project bootstrap 2026-04-10
- [x] Repo — forgejo_admin/westside-streamlit
- [x] User Story — operator wants container image for pipeline deploy
- [x] Context — explains Streamlit runtime model vs uvicorn pattern
- [x] File Targets — Dockerfile + .dockerignore (create), app.py/requirements.txt (do-not-touch)
- [x] Acceptance Criteria — 6 testable criteria
- [x] Test Expectations — 4 concrete test commands
- [x] Constraints — base image, uv, non-root uid 1000, healthcheck, CMD
- [x] Checklist — present
- [x] Related — project + arch-deployment-westside-streamlit
Traceability
- [x] story:reachable — present on board item
- [x] story note verified —
story-westside-streamlit-reachableexists and explicitly lists "Dockerfile" as supporting ticket #1 - [x] arch:ci-pipeline — present on board item
- [ ] arch note MISSING — no backing
arch-ci-pipelinenote in pal-e-docs. Label is used platform-wide (validation-pipeline convention, prior reviews) but no dedicated note exists. - [x] Forgejo issue — https://forgejo.tail5b443a.ts.net/forgejo_admin/westside-streamlit/issues/2 (open)
- [x] Bonus:
arch-deployment-westside-streamlitexists and explicitly enumerates this Dockerfile as gap item #1 in the target topology
File Targets
- [x]
Dockerfile— verified absent at~/westside-streamlit/Dockerfile(correct, this is the create target) - [x]
.dockerignore— verified absent (correct, create target) - [x]
app.py— verified present at~/westside-streamlit/app.py, do-not-touch constraint is correct - [x]
requirements.txt— verified present; pinsstreamlit==1.56.0,psycopg[binary]==3.3.3,pandas==3.0.2. Do-not-touch constraint holds.
Targets are specific enough for an agent to act on without guessing.
Repo Placement
OK. Fix lives in westside-streamlit, issue filed on westside-streamlit. Single repo, no cross-repo work.
Dependencies
- [x]
app.pyexists — satisfied - [x]
requirements.txtpinned — satisfied - [x] Reference Python service Dockerfiles (basketball-api, mcd-tracker-api, minio-api) — satisfied
First ticket in the "reachable" deployment chain. Blocks #3 (Woodpecker pipeline), #4 (kustomize overlay), #6 (Tailscale funnel), #7 (Keycloak OIDC). Does not block #5 (Postgres read-only role) or #8 (blast feature). No upstream blockers. Dependency chain is clear from
arch-deployment-westside-streamlitgap list andstory-westside-streamlit-reachablesupporting tickets list, even though not restated in the issue body.Acceptance Criteria
Strong. All 6 AC are programmatically verifiable:
docker buildexit 0,docker imagessize <250 MB,docker run+ dashboard load,docker execuid check,curl /_stcore/healthHTTP 200, missing-PGURL fast-fail. Test Expectations section duplicates these as explicit commands with a single run command. No ambiguous "works correctly" language.Blast Radius
Low. New files only (Dockerfile, .dockerignore), no modifications to existing code. Pattern (
python:3.12-slim+uv+ non-root uid 1000 + healthcheck) matches platform Python service convention — agent has working references. Streamlit-specific deviation (long-lived server vs uvicorn workers) is called out explicitly in Context. No downstream consumers yet — this ticket is the first to produce an image. Rollback is trivial:git rm Dockerfile .dockerignore.Decomposition Assessment
- Discrete changes: 2 (Dockerfile, .dockerignore) — under 3-thing limit.
- File count: 2 files, 1 repo — under decomposition threshold.
- Acceptance criteria: 6 — at the upper edge but all are single-command checks against the same artifact.
- Estimated agent time: 3–5 minutes. Within the five-minute rule.
- Parallelizable subtasks: no — both files are tightly coupled to the same build.
No decomposition needed. Fits comfortably in a single agent pass.
Recommendation
[SCOPE]Create architecture notearch-ci-pipelinefor the shared Woodpecker CI → Harbor → ArgoCD component. This is a platform-wide gap — the label is used on many tickets across projects but no backing note exists. Not a blocker unique to this ticket; escalate to Ava as a platform-level scoping item. If Ava waives the arch-note requirement (pre-existing platform gap), this ticket is effectively READY to advance totodo.- No other action needed. Ticket body, file targets, AC, and test expectations are execution-ready.
Project Page 1
-
westside-streamlit
project-westside-streamlitwestside-streamlit
Vision
An operational dashboard for Westside Basketball built in Streamlit — a read-only view over the
basketball-apiPostgres database that surfaces contract status, jersey payments, and parent-outreach cohorts in one place. It is deliberately not a CRUD app: its job is to turn raw database state into "who does Marcus need to chase today?" in a single screen. The dashboard is Marcus's window into his business — the read-only operational view where he identifies what needs to get done and messages Lucas in GroupMe to execute.Eventual finish line: an HTTP remote MCP server exposes the same data surface so Claude on Marcus's phone can answer "what are my data gaps and actionables today?" natively. Read-only stays the axiom at every layer.
User Stories
Each story is its own note (tag:
user-story). Keys become ticket labels (e.g.story:triage) per the traceability triangle.Key Story Note Role Success Metric triage story-westside-streamlit-triage Ops (Lucas/Ava) Identify unsigned players in <10s without drilling into cards blast story-westside-streamlit-blast Ops (Lucas/Ava) Grab a scoped email cohort for a blast in one copy-paste clusters story-westside-streamlit-clusters Ops (Lucas/Ava) Parents with multiple unsigned kids surface automatically jerseys story-westside-streamlit-jerseys Head Coach (Marcus) See who has/hasn't paid for jerseys, including cash payments audit story-westside-streamlit-audit Ops (Lucas/Ava) Audit signed contracts by date, version, and amount reachable story-westside-streamlit-reachable Platform + Head Coach Marcus can open the dashboard on his phone with Keycloak SSO (shared with westside-app) and no Tailscale client required Architecture
- Domain Model — entities touched
- Data Flow — how a page render moves data from Postgres to the browser
- Deployment — where the service runs and how it connects to the database
- CI Pipeline (shared platform pattern) — the Woodpecker → Harbor → pal-e-deployments → ArgoCD loop this service uses
Key decisions:
- Read-only forever. No writes. Defense in depth — enforced at app layer (no write code), auth layer (admin role only), AND database layer (SELECT-only Postgres role).
- Same-realm SSO. Keycloak client lives in the existing
westside-basketballrealm. Marcus logs into basketball-landing once, the dashboard is SSO-reachable in the same browser session. - Admin-role gating (Lucas's "B" decision). Only
adminrealm role sees the dashboard. Parents/coaches hit 403 via a friendly denied page. - In-app Keycloak integration (
streamlit-keycloak), not oauth2-proxy sidecar — the app needs JWT role claims inst.session_statefor the role check and any future role-based data filtering. - Canonical hostname:
westside-streamlit.tail5b443a.ts.net(NOTwestside-ops— that identity is retired). PGURLenv var is required — no fallback, no default. Fails loud.
🚨 Incident 2026-04-10
A parallel quick-and-dirty deploy (
pal-e-streamlit, mis-named) bypassed this project's entire ticket scaffold and shipped a public Tailscale funnel with zero authentication. PII for minors' families was exposed on the public internet for ~4 hours. Fully documented in incident-2026-04-10-pal-e-streamlit-public-funnel. Mitigated viakubectl annotateremoving the funnel annotation. Source-of-truth aligned via pal-e-deployments#109. Postmortem action items are tracked through ticket #4 (kustomize overlay rename migration) and the behavioral rule infeedback_funnel_requires_auth.The incident validated the project's existing ticket scaffold — ticket #7 Keycloak was already marked "Hard blocker on the Tailscale funnel ticket: a public funnel without auth would leak PII" BEFORE the incident happened. The failure was that a parallel work stream didn't flow through this project's gate.
Board
board-westside-streamlit — kanban for all work on this project. Columns: Backlog → Todo → Next Up → In Progress → Needs Approval → Done. Review gate at Backlog → Todo (per
feedback_ticket_review_gateandsop-ticket-scope-review).Status — 2026-04-23 (live)
2026-04-23 state. App code on
main: 211-lineapp.pywith Keycloak-OIDC admin gate + Contracts + Jerseys sections (filters, parent-cluster detection, email cohort export). Postgres rolewestside_streamlit_rolive in basketball-api prod (migration 044, 6-table allowlist — not 14 as the orphanarch-domain-westside-opsnote claims; that note is superseded). Kustomize overlay committed inpal-e-deployments/overlays/westside-streamlit/prod/. Zero pods running. Substrate audit 2026-04-23 found 4 critical-path gaps: no ArgoCD Application (nothing syncs the overlay), no Harbor project, no Keycloak client in terraform (#10 was closed on the board without the code landing), no Woodpecker registration. 6 new tickets filed 2026-04-23 to close the gaps — see table below. Marcus's content questions (contracts ✓, jerseys ✓, monthly, tournament) are answered by existing app.py plus two new content sections (C1, C3) + one GRANT migration (C2). Critical path to Marcus-sees-data: S1 + S2 + #3 + #6 applied → pod up → C1 + C2 + C3 merged → O1 walkthrough.Phase # Title Column Status (2026-04-23) Substrate pal-e-services#60 S1 — add westside-streamlit to var.services (Harbor + ArgoCD Application) backlog NEW — awaiting /review-ticketSubstrate pal-e-services#61 S2 — Keycloak client in terraform (supersedes the closed-without-code #10) backlog NEW — awaiting /review-ticketSubstrate #3 Woodpecker CI pipeline (.woodpecker.yaml + repo activation) todo Prior review APPROVED (review-934); unblocked once #60 lands Substrate #6 Tailscale ingress (needs body re-scope: private, NOT funnel) backlog Re-scope body → then /review-ticketSubstrate #12 Deployment Secret + env wiring for Keycloak (child of #7) next_up Verify still needed after #61 — SOPS secret already committed in overlay Content #14 C1 — app: Monthly subscription section backlog NEW — awaiting /review-ticketContent basketball-api#510 C2 — migration 048: GRANT tournament + registrations to ro role backlog NEW — awaiting /review-ticketContent #15 C3 — app: Tournament registration section (blocked by #510) backlog NEW — awaiting /review-ticketOnboard #16 O1 — Marcus onboarding: browser-SSO + walkthrough (final ticket) backlog NEW — awaiting /review-ticketDeferred #1 Track cash jersey payments backlog Enhancement — post-reachable Deferred #8 Extend cohort export to Declined + Jerseys-unpaid backlog Enhancement — post-reachable Done #2, #11, #13 Dockerfile (#9), streamlit-keycloak integration (merged via #13) closed Merged ⚠ Drift #10 Keycloak client (board said qa but code never landed) closed (incorrectly) Superseded by pal-e-services#61 Deferred scope from the incident postmortem
- Rotate + remove hardcoded PGURL fallback in
~/pal-e-streamlit/app.py:8(the orphan incident directory) - Retire orphan: delete
pal-e-streamlitnamespace + hostname + on-disk~/pal-e-streamlitdirectory after the properwestside-streamlitdeploy is live (bundled into #4) - Retire orphan: delete empty
westside-opsnamespace (bundled into #4) - Tailscale funnel access log audit for the 4-hour exposure window
- Hook proposal: pre-commit / pre-apply check that flags
tailscale.com/funnel: "true"without a documented auth layer (captured infeedback_funnel_requires_auth) - Hook proposal: block
kubectl applyof new prod-adjacent ingresses without a matching ArgoCD Application
Milestones
- Milestone 1 — "Marcus can see his data": tickets #2, #5, #7-decomposed, #4, #6 all merged and deployed. Marcus signs into basketball-landing on his phone, opens westside-streamlit, sees the dashboard. No Tailscale client. Target: as soon as reachable bundle clears.
- Milestone 2 — MCP remote: HTTP remote MCP server exposing the same data surface for Claude-on-phone queries. Deferred — scoped but not ticketed.
Repos
Repo Platform Role Status forgejo_admin/westside-streamlit Forgejo Streamlit app source 3 commits on main, PR #9 open (Dockerfile) forgejo_admin/basketball-api Forgejo Database owner — PRs for migration 040/041 and future ACL changes land here PR #435 open (Postgres RO role, fix round in progress) forgejo_admin/pal-e-deployments Forgejo Kustomize overlay + ArgoCD Application (to be created in #4) Partial overlay committed via incident PR #109 (ingress.yaml only); full overlay pending #4
Board 1
-
westside-streamlit
board-westside-streamlitNo content