westside-streamlit

westside-streamlit forgejo

Notes

Doc 18
  • Verdict: APPROVED

    Board item #1069 · Forgejo issue forgejo_admin/pal-e-services#60 · Reviewer: Dottie · Date: 2026-04-25 · Round: 2 (re-review of review-1069-2026-04-22)

    Round 1 Findings Resolution

    The prior review (id 1603) returned NEEDS_REFINEMENT with 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 live funnel = false precedent) and basketball-api (k3s.tfvars line ~209, alternate shape) with explicit warning "do NOT copy that field from here" for basketball-api's funnel = true. The non-existent westside-contracts and pal-e-dora-exporter references are gone.
    • [x] funnel = false AC pinned — AC bullet 3 now reads: "funnel = false — the overlay's ingress.yaml already provides the Tailscale ingress; setting funnel = true would create a duplicate kubernetes_ingress_v1.service_funnel (dead-address class warned at services.tf:171-175) AND violate feedback_funnel_requires_auth." AC bullet 6 verifies tofu plan does NOT show creation of kubernetes_ingress_v1.service_funnel["westside-streamlit"].
    • [x] forgejo_repo AC 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 at forgejo_admin/westside-app)."
    • [ ] [SCOPE] arch-services-tf note still missing — search returned no results for arch-services-tf in pal-e-docs. Body's Related section now explicitly calls out this discovered scope: "create arch-services-tf note documenting the var.services fan-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=false and no-funnel reminders
    • [x] Checklist — present
    • [x] Related — present, includes new feedback_funnel_requires_auth + incident-2026-04-10-pal-e-streamlit-public-funnel references

    Traceability

    • [x] story:reachable label — Dashboard Reachable from Phone
    • [x] story note verified — story-westside-streamlit-reachable exists (id 1401, confirmed in round 1)
    • [x] arch:services-tf label present
    • [ ] arch note MISSING — arch-services-tf still 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-streamlit returns zero hits, so the additive change is correctly scoped.
    • [x] Reference services verified live in k3s.tfvars:
      • westsidekingsandqueens at lines 224-231 — confirmed funnel = false, source_repo = "forgejo_admin/pal-e-deployments", source_path = "overlays/westsidekingsandqueens/prod". Exactly the shape the body recommends copying.
      • basketball-api at lines 208-215 — confirmed funnel = true. Body's warning to NOT copy this field is correct.
    • [x] ~/pal-e-services/terraform/variables.tf lines 165-177 — schema verified, supports source_repo + source_path as optional fields. forgejo_repo required with no default (line 168). Body's "no schema extension needed" claim is accurate.
    • [x] ~/pal-e-services/terraform/services.tf lines 168-180 — funnel resource gated for_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 name kubernetes_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 notes westside-streamlit#3/#6 are blocked by this ticket.
    • Lineage notes that westside-streamlit#10 was closed-but-incomplete and is superseded by sibling #61.
    • No board blockers — this ticket can advance to todo immediately on approval.

    Acceptance Criteria

    9 criteria, all individually verifiable:

    • AC1 (key = westside-streamlit) — verifiable via grep
    • AC2 (image_repo prefix matches key per feedback_harbor_project_naming) — verifiable via diff
    • AC3 (new) funnel = false with 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.tf resource names
    • AC7 (new) tofu plan does NOT show creation of funnel ingress — proves funnel = false is 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 apply in 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 = false AND verify the funnel resource is NOT created in plan output. The services.tf:171-175 warning is now cited in the AC. Resolved.
    • forgejo_repo field — AC4 pins the value with the explicit override-on-copy warning. Resolved.
    • port field — body Constraints section cites the westsidekingsandqueens shape (which uses port = 80). With funnel = false the 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 in k3s.tfvars, pins funnel = false with both the configuration AC and a plan-output verification AC, and pins forgejo_repo with the verbatim-copy override warning.

    The single remaining finding (arch-services-tf note 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-tf architecture note at platform scope documenting the var.services fan-out pattern (Harbor project + robot accounts + Harbor creds + namespace + ArgoCD app + optional funnel). Already cited in body's Related → Discovered scope.
  • Verdict: NEEDS_REFINEMENT

    Board item #1069 · Forgejo issue forgejo_admin/pal-e-services#60 · Reviewer: Dottie · Date: 2026-04-22

    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
    • [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:reachable label — Dashboard Reachable from Phone
    • [x] story note verified — story-westside-streamlit-reachable exists at id 1401
    • [x] arch:services-tf label present
    • [ ] arch note MISSING — no arch-services-tf note in pal-e-docs (search returned no results). The project-westside-streamlit Architecture section lists arch-domain-westside-streamlit, arch-dataflow-westside-streamlit, arch-deployment-westside-streamlit, arch-ci-pipeline — but nothing for the services-tf component. [SCOPE] Create architecture note arch-services-tf documenting the var.services fan-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 the services map block. Confirmed westside-streamlit key 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 optional source_repo + source_path for 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 via for_each = var.services across harbor_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 in namespace.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.tfvars returns zero hits; same for pal-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 — recommend basketball-api (close shape: same file lives in pal-e-deployments overlay, image_repo prefix matches service key) AND pal-e-mail (matches the funnel = false pattern 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 that westside-streamlit#3/#6 are blocked by this ticket.
    • Lineage notes that westside-streamlit#10 was 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 via grep
    • AC2 (image_repo prefix) — 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.tf resource names; agent can grep plan output
    • AC5 (ArgoCD targets namespace westside-streamlit) — guaranteed by destination.namespace = each.key in 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.yaml already 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"] in services.tf (line 177) will create a SECOND westside-streamlit-funnel ingress 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 above kubernetes_ingress_v1.service_funnel (services.tf:171-175) explicitly warns about this pattern — it's the exact lesson learned from westside-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). Only westsidekingsandqueens and pal-e-mail use funnel = false. A literal "copy verbatim" instruction will likely select the wrong template. [BODY] Add an explicit AC: "funnel = false — the kustomize overlay at overlays/westside-streamlit/prod/ingress.yaml already manages the Tailscale ingress; a Terraform-managed funnel would create a duplicate ingress and cause the same dead-address class of bug warned about in services.tf:171-175."

    Secondary risk: forgejo_repo field omission. The schema requires forgejo_repo (line 168 of variables.tf, no default). The issue's AC list does not name it. A dev who copies westsidekingsandqueens verbatim will end up with forgejo_repo = "forgejo_admin/westside-app" — wrong. The correct value is forgejo_admin/westside-streamlit. [BODY] Add to AC: "forgejo_repo = "forgejo_admin/westside-streamlit" (the source repo for ArgoCD Image Updater write-back; distinct from source_repo which points to pal-e-deployments)."

    Tertiary risk: port field. The funnel block uses each.value.port as the upstream port. With funnel = false this is harmless (the funnel resource is gated if v.funnel), so any sane number works. Recommend documenting port = 80 to match the overlay's Service port (overlay maps port: 80 → targetPort: 8501 in kustomization.yaml lines 47-51). Not a hard blocker since funnel = false short-circuits the field, but it pre-empts a future toggle bug.

    Pattern-similar bugs to consider: Per feedback_harbor_project_naming, the image_repo prefix 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-api for the kustomize-overlay shape and pal-e-mail for the funnel = false precedent. Without this, a dev grepping for the suggested examples wastes time and may copy a wrong-shape template.
    • [BODY] Add explicit AC: funnel = false with a one-line reason citing the existing overlay-managed ingress and the services.tf:171-175 warning. 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 from westsidekingsandqueens doesn't carry the wrong source repo through.
    • [SCOPE] Create architecture note arch-services-tf at platform scope documenting the var.services fan-out pattern (label-traceability requirement; the label arch:services-tf currently 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 to todo.

  • Verdict: APPROVED

    Note: Both READY and APPROVED are accepted as passing verdicts by the check-board-advance hook. Item passed two prior informal review rounds; the second was APPROVED after rewriting the schema premise from registrations to orders → 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 confirming orders → tournament_products → tournaments join chain
    • [ ] story note NOT YET in project page user-stories table — [SCOPE] Add a tournament row to the User Stories table on project-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 note arch-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 with st.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 has player_id, parent_id, product_id, status (OrderStatus enum), amount_cents, tenant_id — exactly what the ticket queries assume. Tournament has id, name, event_date (Date, nullable), tenant_id — the dropdown query SELECT id, name, event_date FROM tournaments WHERE tenant_id = 1 ORDER BY event_date DESC NULLS LAST is valid against this schema.
    • [x] ~/basketball-api/alembic/versions/044_add_westside_streamlit_ro_role.py — verified orders is in GRANT_TABLES tuple. C3's queries against orders will work as soon as the role is in the connection string.
    • [x] Migration 045 (045_add_tournament_tables.py) — verified present; creates tournaments and tournament_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.txt and Dockerfile exist but no new deps are needed (st, q() helper already imported); registrations table correctly excluded.

    Repo Placement

    Correct. App-layer change (Streamlit dashboard) lives in westside-streamlit; the corresponding GRANT migration is correctly placed in basketball-api as a separate ticket (C2 / #510). Per feedback_basketball_hands_off, this ticket does not touch basketball-api code — it only consumes via the existing q() 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 tournaments or tournament_products; C3 will fail at runtime with permission 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_coordination applied 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.py is 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. The orders SELECT 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 throws permission denied on 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 a tournament row to the User Stories table on project-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 note arch-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 when event_date is NULL across multiple rows — current ORDER BY handles it (NULLS LAST), but a tie-breaker on id DESC would 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.

  • Verdict: APPROVED

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

    Board item #1072 — Forgejo issue forgejo_admin/basketball-api#510. Third review round; scope corrected on 2026-04-24 to drop registrations after C3 cascading review confirmed tournament payments flow through orders (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:tournament label — 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-streamlit user-stories table — current rows: triage, blast, clusters, jerseys, audit, reachable. The tournament key needs a row added.
    • [x] arch:postgres-role label — postgres-role component (the read-only role created in 044 and being extended here)
    • [ ] no dedicated arch-postgres-role note found via search_notes. Pattern is documented in project-westside-streamlit Architecture 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 is 047_add_stripe_checkout_url_to_orders.py, slots 030–047 occupied + one legacy hash-named file (e09c9e678004_add_division_column_to_players.py). down_revision 047 is correct.
    • [x] Reference migration 044_add_westside_streamlit_ro_role.py exists and defines role westside_streamlit_ro with the canonical ACL-only style.
    • [x] Reference migration 045_add_tournament_tables.py exists and creates exactly the two tables being granted (tournaments, tournament_products).
    • [x] src/basketball_api/models.py defines Tournament (line 596, table tournaments) and TournamentProduct (line 614, table tournament_products) — names match exactly. No edits required (correctly listed under "Files NOT to touch").
    • [x] registrations table exists in models.py (line 303, Registration bound 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 into westside-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-api main. Per feedback_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 with GRANT SELECT ON TABLE public.{table} TO {ROLE_NAME}; (lines 82–83) — explicit TABLE keyword and explicit public. 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-ops uses 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 the tournament key to the User Stories table on project-westside-streamlit (anchor user-stories). Story note story-westside-streamlit-tournament already exists (id 1594) but isn't linked from the project page table.
    • [SCOPE] Consider creating a standalone arch-postgres-role note documenting the read-only-role pattern (defense-in-depth, allowlist-only, password from env). The pattern is currently documented across review-936-2026-04-10, migration 044's docstring, and project-westside-streamlit Architecture — consolidation would harden the arch:postgres-role label'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-tournament

    Story: Tournament Signup + Payment Visibility

    Role

    Marcus — Lucas's brother, head coach at Westside Basketball.

    Key

    tournament

    Want

    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, not registrations:

    • tournaments (id, name, event_date) — added in migration 045
    • tournament_products (tournament_id, product_id, team_id) — links a tournament to specific Stripe products
    • products — Stripe products
    • orders (player_id, product_id, status, amount_cents) — who paid for what; orders is already in migration 044's GRANT allowlist

    Join chain for "who paid for tournament X": tournamentstournament_productsproductsordersplayersparents.

    • project-westside-streamlit
    • Migration 045_add_tournament_tables — creates tournaments + tournament_products
    • Section pattern: app.py:147-211 Jerseys section

  • Story: Monthly Subscription Visibility story-westside-streamlit-monthly

    Story: Monthly Subscription Visibility

    Role

    Marcus — Lucas's brother, head coach at Westside Basketball.

    Key

    monthly

    Want

    As Marcus, I want to see every player's monthly subscription stateactive, past_due, canceled, or nonewith 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 SubscriptionStatus value (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
    • project-westside-streamlit
    • Source enum: ~/basketball-api/src/basketball_api/models.pySubscriptionStatus
    • Section pattern: app.py:51-112 Offered pool

  • Verdict: 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 from review-936-2026-04-10 are 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.py with 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 ### Repo header stating PR lands in basketball-api while the issue is filed on westside-streamlit for 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-funnel as defense-in-depth rationale

    Traceability

    • [x] story:reachable label — verified story-westside-streamlit-reachable listed in project-westside-streamlit user-stories (confirmed R1).
    • [x] arch:postgres label — present on board item.
    • [!] arch note missing — arch-postgres does 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, slot 030_ is now occupied by 030_add_registration_type_to_registrations.py (landed between R1 and R2). Next free slot is 031_. 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 ### Repo header — PR lands in basketball-api, issue tracked on westside-streamlit. Acceptable cross-repo pattern, now documented.

    Dependencies

    Independent. No in_progress blockers on board-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 todo and 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.

  • Verdict: NEEDS_REFINEMENT

    Board item #936 on board-westside-streamlit. Forgejo issue: forgejo_admin/westside-streamlit#5 (state: open). Issue body declares target repo as forgejo_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-streamlit and feedback_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:reachable label — verified: story-westside-streamlit-reachable exists and is listed in project-westside-streamlit user-stories section (Platform + Head Coach role).
    • [x] arch:postgres label — present on board item.
    • [ ] arch note MISSING — no arch-postgres note found in pal-e-docs via search_notes. The issue also references arch-dataflow-westside-streamlit and arch-deployment-westside-streamlit; neither exist yet (project page links them as planned architecture artifacts). [SCOPE] Create architecture note arch-postgres (or scope down to arch-deployment-westside-streamlit which 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 (likely 030_add_westside_streamlit_ro_role.py) rather than timestamp, to match repo convention.
    • [x] Tests directory ~/basketball-api/tests/ exists. New file test_westside_streamlit_ro_role.py is 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 in basketball-api (schema owner). Issue body is explicit about this via the ### Repo header. 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 in in_progress on 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 string jersey_orders appears at line 123 but is a WITH jersey_orders AS (...) CTE, not a real table — it reads from orders + products internally. 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_ro and run DELETE FROM players, then the query fails." Symmetry with the INSERT/UPDATE cases.

    Blast Radius

    Sibling pattern exists: westside-ops project uses the same "read-only Postgres role" architectural invariant (see story-westside-ops-spreadsheet-access, arch-deployment-westside-ops). This migration establishes the pattern; westside-ops should follow with its own westside_ops_ro role 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 — either arch-postgres or arch-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 existing 001_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.

  • Verdict: 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.yaml which contains update-kustomize-tag step at line 60 and downloads the shared scripts/update-kustomize-tag.sh from 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 note arch-ci-pipeline documenting 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-reachable

    Story: 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 kubectl touches 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-ops role 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:

    1. Add Dockerfile
    2. Add Woodpecker CI pipeline
    3. Add kustomize overlay in pal-e-deployments
    4. Create read-only Postgres role for westside-streamlit
    5. Expose via Tailscale funnel on dedicated hostname
    6. 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.
  • Story: Signed Contract Audit story-westside-streamlit-audit

    Story: 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.py lines 102–119 inside a st.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).
  • Story: Jersey Payment Tracking story-westside-streamlit-jerseys

    Story: 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 orders rows 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.py lines 121–185. Shows players with a jersey_option set, joined with their most recent jersey order to derive pay_status from orders.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 orders row, so they show up as pay_status = 'none' in the "unpaid" filter. This is wrong — they ARE paid, just not through Stripe. See ticket: "Add cash jersey payment tracking" on board-westside-streamlit.

    Acceptance criteria (current + post-cash-fix)

    • Every player with jersey_option IS NOT NULL appears 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.
  • Story: Parent Cluster Detection story-westside-streamlit-clusters

    Story: 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.py lines 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-blast

    Story: 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.py lines 85–86. Matches acceptance criteria. Captured here for traceability.

    • 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-triage

    Story: 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-app shows 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.py lines 15–75. Matches all acceptance criteria. This story is captured primarily for traceability — any future work touching the triage view should reference story:triage in its ticket.

    Out of scope

    • Mutating contract status from this view (all writes go through the admin UI).
    • Sending outreach messages directly (see story:blast for the copy-paste export workflow).
    • Historical trend lines (how many were signed last week vs this week) — future story if requested.
  • Deployment — westside-streamlit arch-deployment-westside-streamlit

    Deployment — 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 --> CNPG

    Target — 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 --> POD

    Gap to target

    The following pieces are missing and live as tickets on board-westside-streamlit:

    1. Dockerfile — multi-stage build producing a small Python 3.12 + streamlit image.
    2. Woodpecker pipeline.woodpecker.yaml mirroring the basketball-api pattern (test, build, push to Harbor, update pal-e-deployments overlay).
    3. Harbor robot account — per-service credential for the Woodpecker build step.
    4. Kustomize overlay in pal-e-deployments — Deployment + Service + SOPS-encrypted Secret for PGURL.
    5. Tailscale funnel — new dedicated hostname (never stomp archbox per feedback_never_stomp_archbox). Proposal: westside-ops.tail5b443a.ts.net.
    6. ArgoCD Application pointing at the new overlay.
    7. 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 is basketball; 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-streamlit

    Data 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 UI
    

    Query load estimate

    Current app.py issues 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). With ttl=30 and 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-streamlit

    Domain Model — westside-streamlit

    Entities that the Streamlit dashboard reads from the basketball Postgres database. All schemas are owned by basketball-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_status drives the counts cards, the Offered pool, the Declined pool, and the Signed audit.
    • players.division is mapped to "Kings" / "Queens" in the UI (boys → Kings, girls → Queens).
    • players.monthly_fee drives the fee tier filter (≤$100, $100–180, $180–200, >$200).
    • players.custom_notes surfaces as a "custom deal" filter — any non-empty value means a bespoke contract.
    • parents.email is the dedup key for parent-cluster detection (one parent → multiple unsigned kids).
    • orders.status filtered to products.category = 'jersey' drives the jersey payment view. Gap: cash-paid jerseys (e.g. Seydou) have no orders row, so they show as pay_status = 'none'. See story-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
  • Verdict: 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 locking public_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-18 as browser-side streamlit-keycloak wrapper 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=false commands
    • [x] Constraints — explicit "copy westside-spa shape verbatim", -lock=false per feedback_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 on project-westside-streamlit user-stories table at row "reachable"
    • [x] arch:services-tf label — services terraform layer (Keycloak clients, services map)
    • [~] arch note for services-tf — no dedicated arch-services-tf note exists in pal-e-docs. The project's arch-deployment-westside-streamlit covers 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-services and the issue is filed there. Consistent.

    File Targets

    • [x] terraform/k3s.tfvars — verified file exists. westside-spa entry occupies lines 92-111 (issue says "around line 92" — accurate). Shape includes realm_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 iterates var.keycloak_clients, wires access_type = each.value.public_client ? "PUBLIC" : "CONFIDENTIAL" at line 142, pkce_code_challenge_method at line 150, valid_redirect_uris/web_origins at 152-153, and gates the realm-roles mapper on include_realm_roles_mapper at line 169. No schema changes needed — confirmed.
    • [x] terraform/variables.tf — schema for keycloak_clients map (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-landing entries — 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 in westside-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.tfvars Keycloak block vs k3s.tfvars services block). Migration-slot-style collision is possible if dispatched in parallel — recommend serializing dispatch (S1 first → tofu apply → S2) per feedback_migration_slot_coordination. Flag for Ava when promoting to todo.
    • 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=false output is the canonical evidence — provides create-plan diff for keycloak_openid_client.westside-streamlit
    • client_id, public_client, pkce_code_challenge_method, redirect URIs, web origins, and include_realm_roles_mapper are all observable in plan output
    • tofu validate + tofu fmt commands are concrete and CI-runnable
    • "No changes to westside-landing or westside-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 = true emits a separate keycloak_openid_user_realm_role_protocol_mapper resource (or equivalent in the terraform-keycloak provider). Adds realm_access.roles claim to JWT — required by app.py:26-31 admin gate.
    • Funnel auth gate (feedback_funnel_requires_auth): This Keycloak client is the auth verification mechanism for the eventual funnel exposure of westside-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 backlog to todo. The two prior informal review rounds did the heavy lifting (locking public_client = true PKCE S256). Live verification confirms file targets, line references, schema support, and absence of existing streamlit references in terraform.

    • [SCOPE] Convention gap: arch-services-tf note does not exist. Same gap on sibling #1069. Recommend Ava create a single platform-level architecture note for the pal-e-services terraform module shape (Keycloak clients map, services map, realms) to close the traceability triangle for all arch:services-tf labels. Do NOT block this ticket on it.
    • [SCOPE] Dispatch ordering: serialize #1069 (S1) before #1070 (S2) to avoid simultaneous edits to terraform/k3s.tfvars. Same-file collision risk per feedback_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. Per feedback_funnel_requires_auth.
    • No [BODY] or [LABEL] changes required. Issue body and labels are clean.
  • Verdict: APPROVED

    Final ticket in the story:reachable arc 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 ### Type header). Routed to template-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:reachable label — Dashboard Reachable from Phone
    • [x] story note verified — story-westside-streamlit-reachable (id 1401) exists; entry present in project-westside-streamlit user-stories table (row "reachable" → Platform + Head Coach)
    • [x] arch:keycloak-oidc label — Keycloak OIDC component
    • [ ] arch note MISSING — arch-keycloak-oidc not found via search_notes. Architectural decisions ARE documented inline in project-westside-streamlit key-decisions (same-realm SSO, in-app integration via streamlit-keycloak, admin realm role gating, canonical hostname). [SCOPE] Create arch-keycloak-oidc note 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 — assign admin realm 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-streamlit even though the change is in Keycloak; this is correct because the ticket completes the project's story:reachable arc and validates the reachable user story's acceptance criteria. Keycloak realm/client provisioning is already covered separately by pal-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#14 Monthly (open, backlog) and #15 Tournament (open, backlog)
    • DB: basketball-api#510 migration 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 todo until 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 done only after the retro is filed.

    Blast Radius

    Bounded:

    • Granting admin realm 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] Create arch-keycloak-oidc architecture note (realm topology, client registration pattern, admin role mapping, streamlit-keycloak JWT claim contract). Non-blocking for this ticket — decisions already inlined on project-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 on project-westside-streamlit is a separate doc-fix item and is NOT a blocker for this ticket. Confirmed.

    This ticket is APPROVED to advance from backlogtodo. It will remain todo until the upstream blocker chain (S1, S2, #3, #6, #14, #15, #510) clears. Both READY and APPROVED are passing verdicts per the check-board-advance hook.

  • Verdict: 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, tag user-story, status active, project westside-streamlit. Role/want/so-that match issue body.
    • [~] story note NOT yet listed in project-westside-streamlit user-stories table — table currently shows triage / blast / clusters / jerseys / audit / reachable. Missing: monthly row + tournament row (separate ticket #1073's story).
    • [x] arch:streamlit-app label — references the streamlit app component
    • [~] arch note arch-streamlit-app not located via slug lookup. Architecture is described in project-westside-streamlit Architecture 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-39 q() helper — verified, matches issue claim (cached psycopg + pandas read_sql).
    • [x] app.py:51-112 Offered pool — verified as the mirror pattern (header → query → filters → dataframe → cluster detection → cohort export).
    • [x] app.py:103-109 parent-cluster detection — verified, exact pattern referenced in AC #6.
    • [x] app.py:111-112 email cohort export — verified (st.text_area pattern).
    • [x] ~/basketball-api/src/basketball_api/models.py SubscriptionStatus — 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_TABLES includes players, parents, teams, player_teams. subscription_status + monthly_fee columns live on the already-granted players table — no new GRANT needed. Issue claim is accurate.

    Repo Placement

    OK. Issue is filed on westside-streamlit; the only file change is app.py in that repo. The basketball-api reference is read-only (consult the enum file, no edits) — explicitly called out in "Files NOT to touch" and consistent with feedback_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 enter todo/next_up until 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, existing st.dataframe + st.text_area patterns.
    • Auth gate at app.py:13-31 already 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 todo once Lucas confirms.

    One discovered scope item (does NOT block this ticket):

    • [SCOPE] Add monthly row to project-westside-streamlit user-stories table linking to story-westside-streamlit-monthly (id 1593). Story note exists and is correct — only the project-page table needs the new row. The tournament story for sibling ticket #1073 has the same gap. Per feedback_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-monthly id 1593 (exists, project-bound, user-story tag)
    • pal-e-docs: project-westside-streamlit user-stories section (no monthly row — recorded as discovered scope)
    • pal-e-docs: board-westside-streamlit via list_board_items (item #1071 in backlog, dependencies #1069/#1070 also in backlog)
  • Verdict: 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-deployments repo. 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/servicemonitor via resources: [../../../bases/standard, ../../../bases/servicemonitor], with JSON6902 rename patches turning base appwestside-streamlit for Deployment/Service/ServiceMonitor name + labels + container name. Matches overlays/basketball-api/prod/kustomization.yaml exactly.
    • [x] Finding 2 — Overlay directory structure (flat, not /prod/): FIXED. All file targets live under overlays/westside-streamlit/prod/. Matches overlays/basketball-api/prod/ and overlays/westsidekingsandqueens/prod/ convention.
    • [x] Finding 3 — Hostname discrepancy: Ticket uses westside-streamlit.tail5b443a.ts.net consistently. Story/arch notes still say westside-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 update story-westside-streamlit-reachable + arch-deployment-westside-streamlit to 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.yaml as 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/ and bases/servicemonitor/ exist (only two shared bases)
    • [x] overlays/basketball-api/prod/ contains deployment-patch.yaml, harbor-creds.enc.yaml, kustomization.yaml — matches body's proposed file set (minus app-specific extras)
    • [x] overlays/westsidekingsandqueens/prod/ contains ingress.yaml alongside the standard files — confirms per-overlay ingress.yaml is a valid pattern (ingress not in base)
    • [x] overlays/westside-email/prod/namespace.yaml exists — confirms separate namespace.yaml is a valid pattern (2 of ~15 services use it, rest inline or manage externally)
    • [x] .sops.yaml creation_rules regex: \.enc\.yaml$ — confirms body's requirement that SOPS files use .enc.yaml suffix
    • [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 (tracked dev/ingress.yaml from 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-reachable note exists and lists this work
    • [x] arch:k8s-deploy — label present; arch-k8s-deploy backing 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-funnel linked
    • [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 base app
    • [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, hostname westside-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-encrypted westside-streamlit-secrets with PGURL from westside_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.yaml as 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 build exit 0, kubectl apply --dry-run=server passes, 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 namespace post-merge.
    • Harbor project: body specifies harbor.tail5b443a.ts.net/westside/westside-streamlit. Per feedback_harbor_project_naming, agent should verify westside Harbor project exists before image pull works (acceptance-time check, not scoping-time).
    • Hostname doc drift (westside-ops in story/arch vs westside-streamlit in 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-deployments repo (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.
  • Verdict: 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-reachable listed 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.yaml creation_rules regex is \.enc\.yaml$. A .sops.yaml file will NOT be auto-encrypted by SOPS and will commit plaintext. Rename to secret-keycloak.enc.yaml. Precedent: overlays/basketball-api/prod/harbor-creds.enc.yaml.
    • [ ] overlays/westside-streamlit/deployment.yaml — ISSUE: existing overlays don't ship a literal deployment.yaml. Pattern is bases/standard + a deployment-patch.yaml (see overlays/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 writes overlays/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 via kubectl exec ... env | grep KC_, rollout status, kustomize build. Real commands. Only gap: AC does not assert the git status plaintext-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_SECRET rotation 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] Rename secret-keycloak.sops.yamlsecret-keycloak.enc.yaml in File Targets, Acceptance Criteria, and Test Expectations. Match the repo's existing SOPS creation_rules regex (\.enc\.yaml$). Cite precedent overlays/basketball-api/prod/harbor-creds.enc.yaml.
    • [BODY] Replace overlays/westside-streamlit/deployment.yaml with overlays/{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. Either overlays/pal-e-streamlit/{dev,prod}/ or confirm #4 creates overlays/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 note arch-keycloak-oidc covering realm, client, redirect URIs, and the env var contract. Blocks arch traceability for #7, #10, #11, and #12.
  • Verdict: 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-deployments layout 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-reachable exists, 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-deploy note in pal-e-docs. Platform-wide gap (same as #933 arch:ci-pipeline). Treat as waiver per review-933-2026-04-10 precedent — not a blocker unique to this ticket.
    • [x] Bonus — arch-deployment-westside-streamlit explicitly 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-funnel cross-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 shared bases/standard base with per-overlay kustomize patches that rename the generic app Deployment/Service. Only bases/standard and bases/servicemonitor exist today. Inventing a per-service base for westside-streamlit is a new pattern.
    • [ ] bases/westside-streamlit/deployment.yaml — same convention mismatch. See overlays/basketball-api/prod/kustomization.yaml for the patch-the-shared-base pattern.
    • [ ] bases/westside-streamlit/service.yaml — same.
    • [ ] overlays/westside-streamlit/kustomization.yaml — DIRECTORY STRUCTURE MISMATCH. All existing overlays use overlays/{service}/prod/ (or dev/) subdirectory. Ticket specifies a flat overlays/westside-streamlit/. ArgoCD Application paths will break if this convention is broken.
    • [ ] overlays/westside-streamlit/namespace.yaml — same structure mismatch. Also, base Deployment in bases/standard/deployment.yaml already 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 named harbor-creds.enc.yaml for Harbor creds — this one is app secret, name is fine).
    • [ ] overlays/westside-streamlit/networkpolicy.yaml — PATH AMBIGUITY. bases/standard/networkpolicy.yaml already 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 to postgres + 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 at overlays/pal-e-streamlit/dev/ingress.yaml.
    • [x] overlays/pal-e-streamlit/ DELETE — verified: this directory contains 5 files, only dev/ingress.yaml is tracked in git (committed via PR #109 at commit 693721c). 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-service application.yaml." Verified: neither exists in pal-e-deployments. No argocd-apps/ directory, no application.yaml files anywhere. ArgoCD Applications are declared externally (likely pal-e-platform tofu 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 build exit 0, ArgoCD namespace creation, probe rollout, in-cluster postgres reachability + default-deny test, _stcore/health 200, 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 runs kubectl delete namespace pal-e-streamlit manually post-merge.
    • Hostname conflict: flag — ticket uses westside-streamlit.tail5b443a.ts.net but story-westside-streamlit-reachable AC and arch-deployment-westside-streamlit target diagram both say westside-ops.tail5b443a.ts.net. The incident postmortem action items list "Rename pal-e-streamlit → westside-streamlit (namespace + hostname + local dir + overlay path)" supporting westside-streamlit — but story/arch say westside-ops. Reconcile before #6 funnel ticket lands.
    • Harbor project naming: per feedback_harbor_project_naming, Harbor projects derive from image_repo prefix. Ticket specifies harbor.tail5b443a.ts.net/westside/westside-streamlit:latest. Agent should verify the westside Harbor 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

    1. [BODY] Clarify base pattern — either (a) change file targets to use bases/standard with 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.
    2. [BODY] Fix overlay directory structure — change overlays/westside-streamlit/ to overlays/westside-streamlit/prod/ (or /dev/) to match the existing convention across all 11 services.
    3. [BODY] Resolve hostname discrepancy — ticket says westside-streamlit.tail5b443a.ts.net, story + arch note say westside-ops.tail5b443a.ts.net. Pick one and update the losing document.
    4. [BODY] Clarify ArgoCD Application location — no argocd-apps/ directory and no application.yaml files 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.
    5. [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.
    6. [SCOPE] Create architecture note arch-k8s-deploy — platform-wide gap, same as arch-ci-pipeline in #933. Waived for this ticket per #933 precedent.
    7. Migration sequencing, Keycloak separation, and funnel prohibition are all handled correctly in the current body — no action on those.
  • Verdict: 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-reachable note exists
    • [x] arch:keycloak-oidc label present
    • [ ] arch note MISSING — arch-keycloak-oidc not found in pal-e-docs. [SCOPE] Create architecture note arch-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 pins streamlit==1.56.0, psycopg[binary]==3.3.3, pandas==3.0.2. Appending streamlit-keycloak is clean.
    • [x] app.py — verified. Imports os/streamlit/psycopg/pandas, defines q() using psycopg.connect(DB), and the FIRST q() call (counts = q(...)) fires immediately after st.title. Correct insertion point for the auth wrapper is above st.set_page_config / st.title so 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) is pal-e-deployments kustomize. 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 .env before 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 in st.session_state for 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 ZERO SELECT statements — 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-reachable mentions a westside-ops role in its AC, while this ticket checks the admin realm 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.txt append plus ~30 lines of auth wrapper at the top of app.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 in st.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 first q().
    • [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_state usage correct — AC#6 requires JWT claims land there for downstream filtering.
    • [x] Ticket code DEVELOPABLE before A/C land — explicit local .env path.

    Recommendation

    1. [SCOPE] Create architecture note arch-keycloak-oidc (shared across #10/#11/#12; non-blocking for this ticket but should land before umbrella #7 closes).
    2. [SCOPE] Reconcile role name between story-westside-streamlit-reachable (westside-ops) and this ticket (admin). Suggest updating the story to match the ticket's admin realm 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.

  • Verdict: APPROVED

    Round 2 re-review of board item #933 on board-westside-streamlit. Prior review review-933-2026-04-10 returned NEEDS_REFINEMENT with a single finding: the arch:ci-pipeline label had no backing architecture note in pal-e-docs. That gap is now closed.

    Platform Gap Resolution

    • [x] arch-ci-pipeline note created in pal-e-platform project (verified via get_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 the arch:ci-pipeline label 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-reachable explicitly 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-pipeline now 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-streamlit also 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 build exit 0, docker images size <250 MB, docker run + dashboard load, docker exec uid check, curl /_stcore/health HTTP 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 new arch-ci-pipeline note, which is substantive and operationally useful beyond just this ticket.

  • Verdict: NEEDS_REFINEMENT

    Ticket body is excellent — thorough, testable, well-scoped. Only gap is the backing architecture note for the arch:ci-pipeline label.

    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-reachable exists and explicitly lists "Dockerfile" as supporting ticket #1
    • [x] arch:ci-pipeline — present on board item
    • [ ] arch note MISSING — no backing arch-ci-pipeline note 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-streamlit exists 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; pins streamlit==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.py exists — satisfied
    • [x] requirements.txt pinned — 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-streamlit gap list and story-westside-streamlit-reachable supporting tickets list, even though not restated in the issue body.

    Acceptance Criteria

    Strong. All 6 AC are programmatically verifiable: docker build exit 0, docker images size <250 MB, docker run + dashboard load, docker exec uid check, curl /_stcore/health HTTP 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

    1. [SCOPE] Create architecture note arch-ci-pipeline for 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 to todo.
    2. No other action needed. Ticket body, file targets, AC, and test expectations are execution-ready.
Project Page 1
  • westside-streamlit project-westside-streamlit

    westside-streamlit

    Vision

    An operational dashboard for Westside Basketball built in Streamlit — a read-only view over the basketball-api Postgres 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

    1. Domain Model — entities touched
    2. Data Flow — how a page render moves data from Postgres to the browser
    3. Deployment — where the service runs and how it connects to the database
    4. 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-basketball realm. Marcus logs into basketball-landing once, the dashboard is SSO-reachable in the same browser session.
    • Admin-role gating (Lucas's "B" decision). Only admin realm 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 in st.session_state for the role check and any future role-based data filtering.
    • Canonical hostname: westside-streamlit.tail5b443a.ts.net (NOT westside-ops — that identity is retired).
    • PGURL env 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 via kubectl annotate removing 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 in feedback_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_gate and sop-ticket-scope-review).

    Status — 2026-04-23 (live)

    2026-04-23 state. App code on main: 211-line app.py with Keycloak-OIDC admin gate + Contracts + Jerseys sections (filters, parent-cluster detection, email cohort export). Postgres role westside_streamlit_ro live in basketball-api prod (migration 044, 6-table allowlist — not 14 as the orphan arch-domain-westside-ops note claims; that note is superseded). Kustomize overlay committed in pal-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-ticket
    Substrate pal-e-services#61 S2 — Keycloak client in terraform (supersedes the closed-without-code #10) backlog NEW — awaiting /review-ticket
    Substrate #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-ticket
    Substrate #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-ticket
    Content basketball-api#510 C2 — migration 048: GRANT tournament + registrations to ro role backlog NEW — awaiting /review-ticket
    Content #15 C3 — app: Tournament registration section (blocked by #510) backlog NEW — awaiting /review-ticket
    Onboard #16 O1 — Marcus onboarding: browser-SSO + walkthrough (final ticket) backlog NEW — awaiting /review-ticket
    Deferred #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-streamlit namespace + hostname + on-disk ~/pal-e-streamlit directory after the proper westside-streamlit deploy is live (bundled into #4)
    • Retire orphan: delete empty westside-ops namespace (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 in feedback_funnel_requires_auth)
    • Hook proposal: block kubectl apply of 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