feat: sessions, demo seed, phone pass, hardening, screenshots, deploy - #16
Merged
Conversation
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>
…, dev.sh overlap with T-031)
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.
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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=Noneplus a manualmodel_dumpmeansreceipts/viewed_count/recipient_countare absent from a player payload, andyou_watchedis absent from a coach one. SameRosterOut/CoachRosterOutpattern already used for fit warnings.The two
@pytest.mark.skipplaceholder 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.tsruns the Brief section 6 acceptance narrative end to end on both viewports.make permissionsfails on a skipped row, not just a failing one, and is its own CI step.Screenshots + README —
make screenshotsdrives 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.
.ctl-ghostwas 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.Decisions taken
you_watched, never anyone else's. The line the design README draws is "players never see each other's status".boardsrow.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
plantostarterand attaching a disk at/datais the only change needed to make it durable, andDATABASE_URLalready points there.Verification
make verifygreen 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