project-westside-streamlit updated 2026-04-23westside-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
- Domain Model — entities touched
- Data Flow — how a page render moves data from Postgres to the browser
- Deployment — where the service runs and how it connects to the database
- CI Pipeline (shared platform pattern) — the Woodpecker → Harbor → pal-e-deployments → ArgoCD loop this service uses
Key decisions:
- Read-only forever. No writes. Defense in depth — enforced at app layer (no write code), auth layer (admin role only), AND database layer (SELECT-only Postgres role).
- Same-realm SSO. Keycloak client lives in the existing
westside-basketballrealm. Marcus logs into basketball-landing once, the dashboard is SSO-reachable in the same browser session. - Admin-role gating (Lucas's "B" decision). Only
adminrealm role sees the dashboard. Parents/coaches hit 403 via a friendly denied page. - In-app Keycloak integration (
streamlit-keycloak), not oauth2-proxy sidecar — the app needs JWT role claims inst.session_statefor the role check and any future role-based data filtering. - Canonical hostname:
westside-streamlit.tail5b443a.ts.net(NOTwestside-ops— that identity is retired). PGURLenv var is required — no fallback, no default. Fails loud.
🚨 Incident 2026-04-10
A parallel quick-and-dirty deploy (pal-e-streamlit, mis-named) bypassed this project's entire ticket scaffold and shipped a public Tailscale funnel with zero authentication. PII for minors' families was exposed on the public internet for ~4 hours. Fully documented in incident-2026-04-10-pal-e-streamlit-public-funnel. Mitigated 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-streamlitnamespace + hostname + on-disk~/pal-e-streamlitdirectory after the properwestside-streamlitdeploy is live (bundled into #4) - Retire orphan: delete empty
westside-opsnamespace (bundled into #4) - Tailscale funnel access log audit for the 4-hour exposure window
- Hook proposal: pre-commit / pre-apply check that flags
tailscale.com/funnel: "true"without a documented auth layer (captured infeedback_funnel_requires_auth) - Hook proposal: block
kubectl applyof new prod-adjacent ingresses without a matching ArgoCD Application
Milestones
- Milestone 1 — "Marcus can see his data": tickets #2, #5, #7-decomposed, #4, #6 all merged and deployed. Marcus signs into basketball-landing on his phone, opens westside-streamlit, sees the dashboard. No Tailscale client. Target: as soon as reachable bundle clears.
- Milestone 2 — MCP remote: HTTP remote MCP server exposing the same data surface for Claude-on-phone queries. Deferred — scoped but not ticketed.
Repos
| Repo | Platform | Role | Status |
|---|---|---|---|
| forgejo_admin/westside-streamlit | Forgejo | Streamlit app source | 3 commits on main, PR #9 open (Dockerfile) |
| forgejo_admin/basketball-api | Forgejo | Database owner — PRs for migration 040/041 and future ACL changes land here | PR #435 open (Postgres RO role, fix round in progress) |
| forgejo_admin/pal-e-deployments | Forgejo | Kustomize overlay + ArgoCD Application (to be created in #4) | Partial overlay committed via incident PR #109 (ingress.yaml only); full overlay pending #4 |