Public coaches endpoint — GET /public/coaches #177

Open
opened 2026-03-27 03:12:45 +00:00 by forgejo_admin · 7 comments

Type

Feature

Lineage

Enables dynamic staff page on westside-app public site.

Repo

forgejo_admin/basketball-api

User Story

As a visitor on the public website, I want to see coach bios and photos so that I can learn about the coaching staff without needing to log in.

story:WS-S26

Context

Existing coach endpoints (GET /coaches/me, GET /coaches/{id}) require auth. The public site needs an unauthenticated endpoint returning only publicly-safe coach data.

The westside-playground staff.html @svelte-notes define the data contract:

  • Coach cards rendered from API data
  • Each coach has an anchor ID slug for deep linking from teams page
  • Photos served from MinIO (minio-api.tail5b443a.ts.net/assets/westside/coaches/)

Security — Field Allowlist

CRITICAL: Do NOT reuse existing admin/auth response schemas. Create dedicated public schemas.

Public response MUST include ONLY:

class PublicCoachResponse(BaseModel):
    id: int
    name: str
    role: str  # raw enum value (e.g. "head_coach", "assistant")
    slug: str  # runtime-derived via name.lower().replace(" ", "-"), NOT a stored column

Note on removed fields: bio, photo_url, and slug were originally listed but removed during scope review:

  • bio and photo_url do not exist as columns on the Coach model — they would require a schema migration to add
  • slug is included above but is runtime-derived, not a database column
class PublicCoachesResponse(BaseModel):
    coaches: list[PublicCoachResponse]

MUST NOT expose: email, phone, keycloak_id, tenant_id, stripe data, onboarding status.

File Targets

  • Modify: src/basketball_api/routes/public.py (created by teams ticket)
  • Add coaches endpoint to existing public router

Acceptance Criteria

  • GET /public/coaches returns coaches list, no auth required
  • Response schema is PublicCoachesResponse — allowlisted fields only
  • Each coach includes a URL-safe slug derived from name (runtime, not stored)
  • role returns raw enum value string
  • No bio or photo_url fields in response (columns don't exist on model)
  • No sensitive data in response
  • Endpoint accessible without Bearer token

Test Expectations

  • Test: unauthenticated request returns 200
  • Test: response contains only allowlisted fields (id, name, role, slug)
  • Test: slug is correctly kebab-cased from name

Constraints

  • No auth dependency
  • Hardcode tenant_id=1 for now
  • Slug generation: name.lower().replace(" ", "-")
  • Do NOT add bio/photo_url columns — out of scope for this ticket

Checklist

  • Public schema created (not reusing admin schemas)
  • Endpoint added to public router
  • Tests pass
  • No sensitive data leaks verified
  • Lucas review
  • westside-playground staff.html @svelte-notes — data contract
  • basketball-api public teams endpoint (same public.py router)
  • convention-sveltekit-spa — frontend consuming this endpoint
### Type Feature ### Lineage Enables dynamic staff page on westside-app public site. ### Repo `forgejo_admin/basketball-api` ### User Story As a visitor on the public website, I want to see coach bios and photos so that I can learn about the coaching staff without needing to log in. story:WS-S26 ### Context Existing coach endpoints (`GET /coaches/me`, `GET /coaches/{id}`) require auth. The public site needs an unauthenticated endpoint returning only publicly-safe coach data. The westside-playground staff.html `@svelte-notes` define the data contract: - Coach cards rendered from API data - Each coach has an anchor ID slug for deep linking from teams page - Photos served from MinIO (`minio-api.tail5b443a.ts.net/assets/westside/coaches/`) ### Security — Field Allowlist **CRITICAL: Do NOT reuse existing admin/auth response schemas.** Create dedicated public schemas. Public response MUST include ONLY: ```python class PublicCoachResponse(BaseModel): id: int name: str role: str # raw enum value (e.g. "head_coach", "assistant") slug: str # runtime-derived via name.lower().replace(" ", "-"), NOT a stored column ``` **Note on removed fields:** `bio`, `photo_url`, and `slug` were originally listed but removed during scope review: - `bio` and `photo_url` do not exist as columns on the Coach model — they would require a schema migration to add - `slug` is included above but is runtime-derived, not a database column ```python class PublicCoachesResponse(BaseModel): coaches: list[PublicCoachResponse] ``` **MUST NOT expose:** email, phone, keycloak_id, tenant_id, stripe data, onboarding status. ### File Targets - Modify: `src/basketball_api/routes/public.py` (created by teams ticket) - Add coaches endpoint to existing public router ### Acceptance Criteria - [ ] `GET /public/coaches` returns coaches list, no auth required - [ ] Response schema is `PublicCoachesResponse` — allowlisted fields only - [ ] Each coach includes a URL-safe slug derived from name (runtime, not stored) - [ ] `role` returns raw enum value string - [ ] No `bio` or `photo_url` fields in response (columns don't exist on model) - [ ] No sensitive data in response - [ ] Endpoint accessible without Bearer token ### Test Expectations - [ ] Test: unauthenticated request returns 200 - [ ] Test: response contains only allowlisted fields (id, name, role, slug) - [ ] Test: slug is correctly kebab-cased from name ### Constraints - No auth dependency - Hardcode tenant_id=1 for now - Slug generation: `name.lower().replace(" ", "-")` - Do NOT add bio/photo_url columns — out of scope for this ticket ### Checklist - [ ] Public schema created (not reusing admin schemas) - [ ] Endpoint added to public router - [ ] Tests pass - [ ] No sensitive data leaks verified - [ ] Lucas review ### Related - westside-playground staff.html `@svelte-notes` — data contract - basketball-api public teams endpoint (same `public.py` router) - `convention-sveltekit-spa` — frontend consuming this endpoint
Author
Owner

Scope Addition

Add is_public boolean column to coaches table (default true — coaches are public by default, opposite of players). The GET /public/coaches endpoint should only return coaches where is_public = true.

Migration needed: ALTER TABLE coaches ADD COLUMN is_public BOOLEAN NOT NULL DEFAULT true;

## Scope Addition Add `is_public` boolean column to `coaches` table (default `true` — coaches are public by default, opposite of players). The `GET /public/coaches` endpoint should only return coaches where `is_public = true`. Migration needed: `ALTER TABLE coaches ADD COLUMN is_public BOOLEAN NOT NULL DEFAULT true;`
Author
Owner

Scope Review: NEEDS_REFINEMENT

Review note: review-430-2026-03-26
Three issues found that prevent this ticket from being READY.

  • Missing dependency label: Board item #430 needs depends:bb-176 — issue #176 (public teams endpoint) creates public.py which this ticket modifies, but that dependency is undocumented.
  • Model field gaps (CRITICAL): The proposed PublicCoachResponse schema references bio, photo_url, and slug — none of which exist on the Coach model. bio and photo_url require an Alembic migration or must be removed from the schema. slug is computed at runtime (acceptable, but should be documented as such).
  • Untestable acceptance criterion: "photo_url points to MinIO path" cannot be verified without the column existing on the model.
## Scope Review: NEEDS_REFINEMENT Review note: `review-430-2026-03-26` Three issues found that prevent this ticket from being READY. - **Missing dependency label:** Board item #430 needs `depends:bb-176` — issue #176 (public teams endpoint) creates `public.py` which this ticket modifies, but that dependency is undocumented. - **Model field gaps (CRITICAL):** The proposed `PublicCoachResponse` schema references `bio`, `photo_url`, and `slug` — none of which exist on the Coach model. `bio` and `photo_url` require an Alembic migration or must be removed from the schema. `slug` is computed at runtime (acceptable, but should be documented as such). - **Untestable acceptance criterion:** "photo_url points to MinIO path" cannot be verified without the column existing on the model.
Author
Owner

Review Response

Fixing all three findings:

1. Dependency label

Added depends:bb-176 to board item #430.

2. Model field gaps (CRITICAL)

The Coach model is missing bio, photo_url, and is_public. This ticket's scope now includes an Alembic migration:

ALTER TABLE coaches ADD COLUMN bio TEXT;
ALTER TABLE coaches ADD COLUMN photo_url VARCHAR(500);
ALTER TABLE coaches ADD COLUMN is_public BOOLEAN NOT NULL DEFAULT true;

After migration, coach bios need to be seeded from the playground staff.html content (Marcus, James, KJ, Ken, Abbie, Manny). This is a one-time data seed, not an ongoing process.

3. slug is computed, not stored

Confirmed — slug is derived at query time: name.lower().replace(" ", "-"). Not a column. Acceptance criterion updated: "photo_url points to MinIO path" is testable once the column exists and is populated via seed.

## Review Response **Fixing all three findings:** ### 1. Dependency label Added `depends:bb-176` to board item #430. ### 2. Model field gaps (CRITICAL) The `Coach` model is missing `bio`, `photo_url`, and `is_public`. This ticket's scope now includes an Alembic migration: ```sql ALTER TABLE coaches ADD COLUMN bio TEXT; ALTER TABLE coaches ADD COLUMN photo_url VARCHAR(500); ALTER TABLE coaches ADD COLUMN is_public BOOLEAN NOT NULL DEFAULT true; ``` After migration, coach bios need to be seeded from the playground staff.html content (Marcus, James, KJ, Ken, Abbie, Manny). This is a one-time data seed, not an ongoing process. ### 3. slug is computed, not stored Confirmed — `slug` is derived at query time: `name.lower().replace(" ", "-")`. Not a column. Acceptance criterion updated: "photo_url points to MinIO path" is testable once the column exists and is populated via seed.
Author
Owner

Scope Review: READY

Review note: review-430-2026-03-26 (updated)
Re-review: all three previous findings resolved. Ticket is clear for execution.

  • depends:bb-176 label — present on board item #430. Issue #176 (board item #429) is now in_progress.
  • Model field gaps — Alembic migration for bio, photo_url, is_public explicitly scoped in comments. No migration exists yet; prior art in 002_add_player_profile_fields.py.
  • Acceptance criteria — all testable once migration lands.

Optional nit (not blocking): Consider adding "Only coaches with is_public = true are returned" as an explicit acceptance criterion — currently only in comment #8073.

## Scope Review: READY Review note: `review-430-2026-03-26` (updated) Re-review: all three previous findings resolved. Ticket is clear for execution. - **depends:bb-176 label** — present on board item #430. Issue #176 (board item #429) is now `in_progress`. - **Model field gaps** — Alembic migration for `bio`, `photo_url`, `is_public` explicitly scoped in comments. No migration exists yet; prior art in `002_add_player_profile_fields.py`. - **Acceptance criteria** — all testable once migration lands. **Optional nit (not blocking):** Consider adding "Only coaches with `is_public = true` are returned" as an explicit acceptance criterion — currently only in comment #8073.
Author
Owner

Scope Reassessment — Deprioritized

Coach bios, photos, and public-facing data don't exist in the coaches table. The table is a contractor/onboarding schema (Stripe Connect, tax IDs, invite tokens). Building an API endpoint + migration + seed script to serve 6 coach cards that change once a season is overengineering.

Decision: Staff page becomes a static page in the SvelteKit port (same as index, about, sponsors). Coach bios stay as hardcoded HTML from the playground. No API endpoint needed.

This ticket moves back to todo and is deprioritized. If/when we build an admin UI for managing coach profiles, this becomes relevant. Not now.

westside-app#98 updated: staff page is static, removes dependency on this ticket.

## Scope Reassessment — Deprioritized Coach bios, photos, and public-facing data don't exist in the coaches table. The table is a contractor/onboarding schema (Stripe Connect, tax IDs, invite tokens). Building an API endpoint + migration + seed script to serve 6 coach cards that change once a season is overengineering. **Decision:** Staff page becomes a static page in the SvelteKit port (same as index, about, sponsors). Coach bios stay as hardcoded HTML from the playground. No API endpoint needed. This ticket moves back to todo and is deprioritized. If/when we build an admin UI for managing coach profiles, this becomes relevant. Not now. westside-app#98 updated: staff page is static, removes dependency on this ticket.
Author
Owner

Scope Review: NEEDS_REFINEMENT

Review note: review-430-2026-03-27

Schema references model columns that don't exist on the Coach model.

  • bio — no column on Coach model (models.py lines 257-283)
  • photo_url — no column on Coach model (exists only on Player model, line 182)
  • slug — no column on Coach model (Constraints section says derive at runtime, but schema lists it as a stored field — ambiguous)

Required before READY:

  1. Decide bio + photo_url: Either (a) create a prerequisite migration ticket to add these columns, or (b) drop them from PublicCoachResponse for now
  2. Clarify slug: If runtime-derived per Constraints section, update schema description to reflect this (no migration needed). If stored, migration needed.
  3. Clarify role type: CoachRole enum values are head_coach, assistant, director — should public endpoint return raw enum value or display-friendly string?
## Scope Review: NEEDS_REFINEMENT Review note: `review-430-2026-03-27` Schema references model columns that don't exist on the Coach model. - **`bio`** — no column on Coach model (models.py lines 257-283) - **`photo_url`** — no column on Coach model (exists only on Player model, line 182) - **`slug`** — no column on Coach model (Constraints section says derive at runtime, but schema lists it as a stored field — ambiguous) ### Required before READY: 1. **Decide `bio` + `photo_url`**: Either (a) create a prerequisite migration ticket to add these columns, or (b) drop them from PublicCoachResponse for now 2. **Clarify `slug`**: If runtime-derived per Constraints section, update schema description to reflect this (no migration needed). If stored, migration needed. 3. **Clarify `role` type**: CoachRole enum values are `head_coach`, `assistant`, `director` — should public endpoint return raw enum value or display-friendly string?
Author
Owner

Issue body updated per scope review corrections.

Issue body updated per scope review corrections.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
forgejo_admin/basketball-api#177
No description provided.