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 | <a href="/notes/story-westside-streamlit-triage">story-westside-streamlit-triage</a> | Ops (Lucas/Ava) | Identify unsigned players in <10s without drilling into cards |
| blast | <a href="/notes/story-westside-streamlit-blast">story-westside-streamlit-blast</a> | Ops (Lucas/Ava) | Grab a scoped email cohort for a blast in one copy-paste |
| clusters | <a href="/notes/story-westside-streamlit-clusters">story-westside-streamlit-clusters</a> | Ops (Lucas/Ava) | Parents with multiple unsigned kids surface automatically |
| jerseys | <a href="/notes/story-westside-streamlit-jerseys">story-westside-streamlit-jerseys</a> | Head Coach (Marcus) | See who has/hasn't paid for jerseys, including cash payments |
| audit | <a href="/notes/story-westside-streamlit-audit">story-westside-streamlit-audit</a> | Ops (Lucas/Ava) | Audit signed contracts by date, version, and amount |
| reachable | <a href="/notes/story-westside-streamlit-reachable">story-westside-streamlit-reachable</a> | 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 | <a href="https://forgejo.tail5b443a.ts.net/forgejo_admin/pal-e-services/issues/60">pal-e-services#60</a> | S1 — add westside-streamlit to var.services (Harbor + ArgoCD Application) | backlog | <strong>NEW</strong> — awaiting <code>/review-ticket</code> |
| Substrate | <a href="https://forgejo.tail5b443a.ts.net/forgejo_admin/pal-e-services/issues/61">pal-e-services#61</a> | S2 — Keycloak client in terraform (supersedes the closed-without-code #10) | backlog | <strong>NEW</strong> — awaiting <code>/review-ticket</code> |
| Substrate | <a href="https://forgejo.tail5b443a.ts.net/forgejo_admin/westside-streamlit/issues/3">#3</a> | Woodpecker CI pipeline (.woodpecker.yaml + repo activation) | todo | Prior review APPROVED (review-934); unblocked once #60 lands |
| Substrate | <a href="https://forgejo.tail5b443a.ts.net/forgejo_admin/westside-streamlit/issues/6">#6</a> | Tailscale ingress (needs body re-scope: private, NOT funnel) | backlog | Re-scope body → then <code>/review-ticket</code> |
| Substrate | <a href="https://forgejo.tail5b443a.ts.net/forgejo_admin/westside-streamlit/issues/12">#12</a> | Deployment Secret + env wiring for Keycloak (child of #7) | next_up | Verify still needed after #61 — SOPS secret already committed in overlay |
| Content | <a href="https://forgejo.tail5b443a.ts.net/forgejo_admin/westside-streamlit/issues/14">#14</a> | C1 — app: Monthly subscription section | backlog | <strong>NEW</strong> — awaiting <code>/review-ticket</code> |
| Content | <a href="https://forgejo.tail5b443a.ts.net/forgejo_admin/basketball-api/issues/510">basketball-api#510</a> | C2 — migration 048: GRANT tournament + registrations to ro role | backlog | <strong>NEW</strong> — awaiting <code>/review-ticket</code> |
| Content | <a href="https://forgejo.tail5b443a.ts.net/forgejo_admin/westside-streamlit/issues/15">#15</a> | C3 — app: Tournament registration section (blocked by #510) | backlog | <strong>NEW</strong> — awaiting <code>/review-ticket</code> |
| Onboard | <a href="https://forgejo.tail5b443a.ts.net/forgejo_admin/westside-streamlit/issues/16">#16</a> | O1 — Marcus onboarding: browser-SSO + walkthrough (final ticket) | backlog | <strong>NEW</strong> — awaiting <code>/review-ticket</code> |
| Deferred | <a href="https://forgejo.tail5b443a.ts.net/forgejo_admin/westside-streamlit/issues/1">#1</a> | Track cash jersey payments | backlog | Enhancement — post-reachable |
| Deferred | <a href="https://forgejo.tail5b443a.ts.net/forgejo_admin/westside-streamlit/issues/8">#8</a> | 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 <a href="https://forgejo.tail5b443a.ts.net/forgejo_admin/pal-e-services/issues/61">pal-e-services#61</a> |
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 |
|---|---|---|---|
| <a href="https://forgejo.tail5b443a.ts.net/forgejo_admin/westside-streamlit">forgejo_admin/westside-streamlit</a> | Forgejo | Streamlit app source | 3 commits on main, PR #9 open (Dockerfile) |
| <a href="https://forgejo.tail5b443a.ts.net/forgejo_admin/basketball-api">forgejo_admin/basketball-api</a> | Forgejo | Database owner — PRs for migration 040/041 and future ACL changes land here | PR #435 open (Postgres RO role, fix round in progress) |
| <a href="https://forgejo.tail5b443a.ts.net/forgejo_admin/pal-e-deployments">forgejo_admin/pal-e-deployments</a> | Forgejo | Kustomize overlay + ArgoCD Application (to be created in #4) | Partial overlay committed via incident PR #109 (ingress.yaml only); full overlay pending #4 |