Skip to content

feat: sessions, demo seed, phone pass, hardening, screenshots, deploy - #16

Merged
BrandanBurgess merged 49 commits into
mainfrom
integration
Aug 7, 2026
Merged

feat: sessions, demo seed, phone pass, hardening, screenshots, deploy#16
BrandanBurgess merged 49 commits into
mainfrom
integration

Conversation

@BrandanBurgess

Copy link
Copy Markdown
Owner

Takes the build from "five screens work" to "a coach can be shown this", and adds the deployment that was previously deferred.

What's in it

Sessions (T-042) — the coach-to-player loop. Draft builder with a picker over library presets and the team's own recordings (mini-board thumbnails reuse PatternPreviewBoard), coach note, send, gold SENT pill with the x/y viewed counter, per-player read receipts. Player side is read-only: sent sessions only, Watch opens the item playing on the board (portrait on a phone), Mark as watched feeds the coach's counter.

Receipts are coach-only and the split is structural, not cosmetic: response_model=None plus a manual model_dump means receipts/viewed_count/recipient_count are absent from a player payload, and you_watched is absent from a coach one. Same RosterOut/CoachRosterOut pattern already used for fit warnings.

The two @pytest.mark.skip placeholder rows in the permission suite ("suggest own playstyle", "sessions") are now real assertions. The suite has zero skips.

Demo seed (make demo) — drops the dev database, migrates from zero, loads library content, then builds one realistic team: 14 players with roles and sliders including a live double-exposure pair, the team's whiteboard, a recorded multi-token pattern, and two sessions (one sent at 3 of 5 viewed, one draft). Idempotent.

Phone pass (T-050) — verified at iPhone 13. No horizontal overflow anywhere, portrait boards on every surface, sheets and menus dismissible by touch, plus a cross-device journey that records on a 1440x900 browser and replays on a separate 390x844 one.

Hardening (T-051)e2e/demo-path.spec.ts runs the Brief section 6 acceptance narrative end to end on both viewports. make permissions fails on a skipped row, not just a failing one, and is its own CI step.

Screenshots + READMEmake screenshots drives the real UI against the demo database and writes 11 PNGs; README is built around them.

Deploy (T-060) — Dockerfile, single-origin serving, scripts/start.sh, render.yaml.

Gaps found and closed while checking the product against the PNGs

These weren't on any ticket line; they turned up looking at real screens.

  • The ball's gold trace while recording was never implemented. The design README and PNG 03 specify it; only playback drew a trail, so a coach recording a move saw nothing behind the ball.
  • The swipe-up sheets pushed the page taller instead of overlaying it (PNG 07, 38, 40), so opening the library scrolled the header and the top of the board off screen. All three are now real bottom drawers.
  • .ctl-ghost was scoped to the save bar only, so every later reuse (Formations "Rondo map", the Patterns meta bar, the new Sessions controls) rendered as a default grey browser button.
  • The four auth and onboarding screens had no styling at all — and they're the first two screens of the demo.
  • Keystone tokens painted Chromium's blue focus ring onto the pitch, on a product where gold is the only interactive colour.
  • Search didn't match "third man" against "Third-Man Run", so the Brief's own narrative found nothing.

Decisions taken

  1. Session recipients are fixed at send time. Receipts are written for every player-role member on send and the player's list is gated on having one, so a later joiner doesn't appear in an already-sent session's denominator. Doc 03 section 6 says receipts exist "for every recipient at send time"; this is that read.
  2. A player sees their own you_watched, never anyone else's. The line the design README draws is "players never see each other's status".
  3. A sent session is immutable (edits 409). It's the record of what the team was told.
  4. There is no persisted "selected formation" or "team identity" in doc 03's schema, and those pages are board-first browse surfaces with local selection. Rather than invent a table, the demo seed expresses "a shape is set" through the one thing that is persisted: the team's boards row.
  5. Search is hyphen-insensitive across all four browse surfaces.

Deployment note

The blueprint uses Render's free plan, which has no persistent disk: the SQLite file is rebuilt on every boot, which is why the demo seed runs at start and the README credentials always work. Fine for a demo instance, not for a real club. Raising plan to starter and attaching a disk at /data is the only change needed to make it durable, and DATABASE_URL already points there.

Verification

make verify green locally (exit 0): 100 passed, 8 skipped (the capture script and the viewport-gated journeys). The deployment image was built and smoke-tested locally: SPA and hashed assets serve, deep links fall back, unknown API paths stay JSON 404s, path traversal is refused, both demo accounts log in and read their seeded data, and the JWT_SECRET boot gate fires.

🤖 Generated with Claude Code

BrandanBurgess and others added 30 commits July 16, 2026 02:39
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…T-030)

Whiteboard page (Brief step 16, PNG 01-05/14/34): toolbar, view menu
(zones + thresholds), record/replay, and save into My Patterns, all
persisted through the scoped query layer (backend/app/routers/whiteboard.py)
into the boards and saved_patterns rows (doc 03 4.2/4.3). Delete is
coach-only, enforced server-side (403 for players), and every saved
pattern is author-stamped from the caller's own membership, never a
client-supplied field.

Fixes a real reload bug: the view menu's settings popover floated above
the toolbar and, left open, could visually sit on top of a token (e.g.
the ball's default spot, or any token near the toolbar on a phone-width
board). A drag or click landing there hit the popover instead of the
token, and for a checkbox's label that silently flipped a zone toggle
the user never touched, which then persisted to the boards row on
reload. Fixed by closing the view menu when recording starts (a
deliberate mode switch) and, for phone-width boards where the popover
has no gap in the pitch it can occupy without covering some token,
pinning it to the viewport's top edge instead of floating over the
pitch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Systematic permission test suite covering every Brief section 3 table
row in both roles, API and UI: backend/tests/test_permissions.py names
each test after its table row (whiteboard, delete-a-pattern, library/
formations/identity, roster/fit-warnings, plus the additive-only and
player-token-gets-403 principles), and e2e/permissions.spec.ts sweeps a
player-role visit across all five pages asserting coach-only controls
and data are absent from the DOM, not merely hidden.

Audit result: no enforcement gaps found. Whiteboard delete, roster CRUD,
and fit_warnings exclusion were already API-enforced from their own
tickets (T-030, T-033); this suite pins that behavior explicitly against
the table rather than leaving it implicit across five separate files.

Two table rows (Suggest own playstyle, Sessions) have no API surface yet
in this codebase state, only their models: marked as named, skipped
tests with a reason, for T-041/T-042 to turn into real assertions rather
than silently omitted.

One known ambiguity is pinned, not changed, per this ticket's own
instruction: the API returns join_code to any team member while the UI
shows it to coaches only (docs/agent/STATE.md open founder question 2).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Player submits a free-text playstyle suggestion on their own roster
profile, sees it pending; coach reviews from a team-wide queue with a
gold badge and an Approve/Dismiss card; approve merges the text into
players.playstyle_note (visible both roles), dismiss clears it with no
merge. New backend/app/routers/suggestions.py enforces every direction
at the API: only a player may submit, and only against their own linked
roster row; only a coach may list the pending queue, approve, or
dismiss (403 otherwise). No new migration: playstyle_suggestions already
exists in the 0002 schema migration.

Player-row linkage (needed for "own profile") did not exist yet before
this ticket; app/routers/roster.py adds a minimal, UI-free claim: on a
player's own GET /api/roster, an unclaimed row whose name uniquely
matches their display_name becomes theirs.
BrandanBurgess and others added 19 commits July 16, 2026 13:04
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…-012)

Founder decision 2026-07-16: doc 03's identities schema had no age_hint
when T-010 shipped, so Bible 8.2.4's age-suitability rule only covered
library_items. Adds migration 0004 (down_revision 0003), the age_hint
column on Identity/IdentityOut, backfills all 27 identity cards from the
Bible, makes age_hint required in the seed validator, and renders it on
the Identity page's details panel.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…042)

The coach-to-player loop (Brief step 23, PNG 21-23, 26, 28).

Backend (app/routers/sessions.py, app/schemas.py):
- Draft builder: create, edit title/note, attach library presets and the
  team's own recordings, reorder, remove. Draft-only: a sent session is a
  record of what the team was actually told, so edits 409.
- Send writes a receipt for every player-role member at send time with
  viewed_at null (doc 03 section 6); Mark as watched (player-only) sets it
  once and keeps the first watch time.
- Coach/player payload split follows RosterOut/CoachRosterOut exactly:
  response_model=None plus a manual model_dump, so receipts/viewed_count/
  recipient_count are ABSENT from a player body and you_watched is absent
  from a coach body. Every route scopes through TeamScope; session_items
  and session_receipts read through query_via, their parent's team_id.

Frontend (pages/SessionsPage.tsx, sessionsApi.ts, AppShell nav entry):
- Coach: session rail, draft builder with the "+ Add from library" picker
  over presets and My patterns (mini-board thumbnails reuse TileThumb),
  coach note, gold SENT pill with the x/y viewed counter, per-player
  receipts (Will receive / Viewed / Not yet).
- Player: sent sessions only, read-only, Watch opens the item playing on
  PatternPreviewBoard (portrait on phone), plus Mark as watched.
- The wire types make the split structural: isCoachSession() narrowing,
  no nullable receipt field a player view could ever render.

Tests: backend/tests/test_sessions_routes.py (15 cases: item kinds,
reorder, send validation, cross-team scoping, receipt contract, jersey
badges), e2e/sessions.spec.ts on both viewports, and the two remaining
@pytest.mark.skip placeholder rows in test_permissions.py ("suggest own
playstyle", "sessions") replaced with real assertions. The permission
suite now has zero skips.

make verify green (exit 0).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
One command before a meeting: drop the dev database, rebuild it from the
migration chain, load the library content, then populate one realistic
team so the app is never empty when a coach opens it.

Seeded state:
- Coach and player accounts with fixed credentials (printed to stdout),
  a team with fixed join codes, both memberships.
- 14 roster players with roles, flanks, work rates and all six sliders,
  including exactly one double-exposure flank pair (Jordan Tavares, right
  wing, AWR high DWR low, in front of Marco Silva, right back, AWR high),
  so the coach-only warning fires for one reason on one flank.
- The team's live whiteboard: the 4-3-3 shape with two confirmed lanes.
- A recorded pattern, "Our build-out vs press": eight tokens moving over
  five seconds (centre-back steps out, pivot drops, fullback goes high,
  the eight arrives inside, the winger spins in behind, two opponents
  pressing, the ball chasing whoever has it), not a stub.
- One sent session with two items (library A5 plus that recording) and a
  receipt already watched, so the counter reads 1 of 1.

Idempotent by natural key like the content seeder, so it is safe to rerun.
check_copy.py now also scans scripts/ and README.md, since the demo seed
ships real user-facing copy; the two files that must name the em dash to
search for it are exempted explicitly.

make verify green (exit 0).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…T-050)

Verified every page at iPhone 13 (390x844) and fixed what the pass found.

Real gaps closed:
- `.ctl-ghost` was defined only as `.save-bar .ctl-ghost` and
  `.saved-pattern .ctl-ghost` in Board.css, so every later reuse of the
  class (Formations "Rondo map", the Patterns meta bar's Open on
  whiteboard and Clear, the new Sessions controls) matched nothing and
  fell back to the browser's default grey button. Promoted to a real
  shared neutral button in global.css; the scoped Board.css rules keep
  winning inside the save bar on specificity.
- The four auth screens (register, log in, create team, join by code) had
  no styling at all: labels ran into their inputs, buttons were browser
  chrome. These are the first two screens of the demo narrative and Brief
  section 8 asks for exactly this ("smallest functional version using the
  token system and existing component patterns"). New auth.css gives them
  the same card/pill/gold-primary idiom, with no DOM change, so every
  getByLabel and testid the platform journeys use still resolves.
- The phone topbar ate a fifth of the screen before the board started:
  tighter padding, join codes side by side, smaller type. Nothing hidden.
  The Manage team panel is pinned to the viewport so it cannot overhang.
- TileThumb dots shrink past 12 tokens, so a saved 23-token recording
  reads as a shape rather than confetti.

New coverage:
- e2e/phone.spec.ts (mobile project only): the 52px icon rail, all six
  pages portrait with no horizontal overflow, and every sheet, the board
  view menu, and the Manage team panel opening AND closing by tap().
- e2e/cross-device.spec.ts: a coach records on a 1440x900 browser and the
  same coach, signed in on a separate iPhone 13 browser, replays it from
  the server. Asserts both halves of the contract: identical stored model
  coordinates, and rendering through the portrait mapping.
- fixtures.ts gains signIn() for second-device journeys.

make verify green (exit 0), 96 passed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…tcher (T-051)

Hardening, scoped to the demo path.

- e2e/demo-path.spec.ts runs the Brief section 6 acceptance narrative as
  one journey under BOTH Playwright projects: coach signs up and creates a
  team, adds six players with roles and sliders (the pair that fires the
  double-exposure warning among them), loads 4-3-3 and taps the pivot
  keystone, toggles the Rondo Map and taps the first-line zone, searches
  "third man" and plays A5, drags and records a build-out and saves it as
  "Our build-out vs press", builds a session with A5 plus the recording
  and a note and sends it, then the player (always in a 390x844 context,
  per "switches to a player account on a phone") opens it, watches A5
  portrait, marks it watched, and the coach's counter reads 1 of 1.

- `make permissions` runs the Brief section 3 table and fails if any row
  is SKIPPED rather than asserted, not just if one fails. Wired into
  `make verify` and into CI as its own named step. Two rows sat as skip
  placeholders for most of this build; a green suite that quietly stops
  checking a permission row is the failure mode worth pinning.

- Em-dash sweep is clean repo-wide: the only remaining occurrences are
  docs/source (raw product docs, allowed), the two scripts that must name
  the character to search for it, and three tests asserting its absence.

- pages/search.ts replaces four identical copies of the search matcher and
  makes matching hyphen-insensitive. The content is full of hyphenated
  names ("Third-Man Run", "Slide-Rule Through Ball") and a coach types
  what they say out loud, so the Brief's own narrative (searches "third
  man") previously found nothing. Hyphenated queries still match.

make verify green (exit 0), 98 passed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Phase 5: the shop window, plus the polish gaps capturing it exposed.

e2e/screenshots.spec.ts drives the real UI against the `make demo` database
and writes eleven PNGs to docs/screenshots/. It is a capture script, not a
test: it skips unless POP_SCREENSHOTS=1, so `make verify` never runs it or
rewrites the images. `make screenshots` reseeds the demo data first, so the
captures always show the same content. Animated surfaces are caught mid
motion (a pass in flight, a keystone at full glow, a drag still in hand),
and it uses the pitch theme rather than dark: every shot is a football
board, and the flat grey dark theme reads as a generic dashboard.

Board and screen gaps found while checking each shot against its PNG:

- The ball's gold trace WHILE RECORDING (design README, PNG 03) was never
  implemented. Only playback drew a trail; a coach recording a move saw
  nothing behind the ball. Now drawn by the same AnimationOverlay and the
  same trail buffer the player uses, so a trace being recorded and one
  being replayed are one piece of code. Covered by a new recorder journey.
- The swipe-up sheets pushed the page taller instead of overlaying it as a
  bottom drawer (PNG 07, 38, 40), so opening the library scrolled the
  header and the top of the board off screen. All three sheets are now real
  fixed bottom drawers that scroll internally.
- Keystone tokens painted Chromium's default blue focus ring on the pitch,
  on a product where gold is the only interactive colour. Pointer focus
  drops the ring; keyboard focus gets a gold one.
- The rondo zone card was clipped by the sheet handle; it now sits above it
  with its own max height.
- Confirmed lanes in the demo seed were placed where the mirrored opponent
  shape blocks them, so the whiteboard opened entirely red. Moved to two
  lines that are clear in the default shape.

Demo seed also gains four more player accounts and a second, DRAFT session,
so the receipts view reads like a squad (3 of 5 viewed, some Not yet) and a
coach has something to send live in the room.

README.md: hook, hero shot, four coach-language bullets, the eleven
screenshots in demo-narrative order with a caption each, quickstart with the
demo credentials, and a stack and architecture section at the bottom.

BACKLOG and STATE updated: T-040, T-042, T-050, T-051 done, with the
decisions taken and the gaps closed recorded for the next session.

make verify green (exit 0), 100 passed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…rint (T-060)

The repo had no deployment configuration at all; this adds it.

- backend/app/main.py serves the built SPA alongside the API when the build
  output is present. One origin is what makes the existing session cookie
  work as written: it is SameSite=Lax and host-only, and api.ts fetches
  relative /api paths with no CORS setup and no credentials mode. A second
  origin would mean SameSite=None, an allow-list, and a flag on every fetch.
  The block is conditional on frontend/dist existing, so a dev checkout and
  the test suite are unchanged (API only, no catch-all registered).
  /api/* that matches nothing stays a JSON 404 rather than falling through
  to the HTML shell, and path traversal resolves outside the build output
  and falls back to index.html instead of being served.

- Dockerfile: two stages, node builds the SPA (via `npm run build`, which
  typechecks first, so a type error fails the image), python runs it. No
  node and no dev extras in the runtime image.

- scripts/start.sh mirrors dev.sh's boot order (migrate, seed, serve) so a
  deployed instance and a dev checkout cannot drift. It refuses to boot when
  COOKIE_SECURE=true and JWT_SECRET is unset, because the fallback secret in
  app/config.py is in the repository and would let anyone forge a session.

- render.yaml: one free Docker web service, health check on /api/health,
  generated JWT_SECRET, POP_SEED_DEMO=true.

Storage is deliberately ephemeral on the free plan and the blueprint says so
in full: no disk, so the database is rebuilt on every boot, which is why the
demo seed runs at start and the README credentials always work. Raising the
plan and attaching a disk at /data is the one change needed to make it
durable; DATABASE_URL already points there.

Verified locally: image builds, container boots, SPA and hashed assets
serve, deep links fall back, unknown API paths 404 as JSON, both demo
accounts log in and read their seeded data, and the JWT_SECRET gate fires.

make verify green (exit 0), 100 passed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`make verify` was green locally and failed in CI with 140 ruff errors in
files this branch never touched. Cause: `ruff>=0.5` and `mypy>=1.10` are
open-ended, so CI resolved whatever had shipped that morning while local
kept what it installed months ago. A ruff minor release expanded its
default rule set (UP007, RUF100, PLW1510 among them) and the build broke on
migrations and test files that had not changed.

Pinned to the minor: patch fixes still flow, but a rule-set change becomes a
deliberate bump someone reviews rather than a surprise on an unrelated PR.
Verified by resolving the pins in a clean venv the way CI does: ruff 0.15.22,
mypy 2.3.0, pytest 9.1.1, both lint and typecheck clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@BrandanBurgess
BrandanBurgess merged commit f66cae9 into main Aug 7, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant