diff --git a/CLAUDE.md b/CLAUDE.md index 9313dd2..f2a7b3e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -16,9 +16,9 @@ as in the markup. The UI is Dutch by default with English available. ```bash dotnet build -c Release # what CI builds — warnings are errors here (see below) -dotnet test # 374 tests, xUnit v3, real SQLite +dotnet test # 389 tests, xUnit v3, real SQLite cd src/FootballFormation.Web && dotnet run # http://localhost:5228 -cd tests/ui && npm test # 34 Playwright tests in a browser, ~1 min (npm install first) +cd tests/ui && npm test # 37 Playwright tests in a browser, ~1 min (npm install first) scripts/visual-check.sh # screenshots every page, then measures every touch target scripts/coverage.sh # coverage of the lines this branch changed, 80% floor ``` @@ -166,7 +166,7 @@ Test names are sentences: There are **no component tests** (no bUnit). The UI is checked by driving the real app in a real browser, in two places, both against a throwaway database: -- **`cd tests/ui && npm test`** — Playwright, ~34 tests, about a minute. Runs on every pull request +- **`cd tests/ui && npm test`** — Playwright, ~37 tests, about a minute. Runs on every pull request as a job in `ci.yml`, advisory rather than the merge gate. Behaviour: the public/admin split, the squad and match dialogs, the whole match-day journey from dragging a lineup to blowing the final whistle, both languages, and the phone layout. Read diff --git a/docs/architecture.md b/docs/architecture.md index 7a0a128..8c5cf17 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -52,7 +52,10 @@ Reporting/ PlayerStatsReport.cs — Per-player aggregates (PlayerStats, PositionStat, PlayerGameStat) PositionFitHelper.cs — 5-tier position fit: Preferred, NaturalFit, Alternative, Compatible, OutOfPosition MatchClockReport.cs — Derives the live clock and period state from the stored anchor + banked total - PlannedChangesReport.cs — What the next period changes versus the one on the pitch + PlannedChangesReport.cs — What the next period changes versus the one on the pitch (rendered by + UI/Components/PlannedChangesList, shared by the live card and its dialog) + ScoreProgressionReport.cs — The score after each goal (MatchScore), for the live timeline — + counted forwards because that list runs newest first HealthReport.cs — Whether a booted container is actually serving: the /health payload and the rule that pending migrations mean unhealthy. Pure, so it is tested Services/ @@ -68,13 +71,16 @@ Services/ renders, GetTodaysMatchAsync for the home-page banner (in-progress first, else today's fixture, upcoming or finished). Writing to one is the three services below, split by what happens on the touchline - MatchClockService.cs — The clock and the run of play: kick-off, pause/resume, ending a period, - starting or rolling into the next one, the final whistle. The - arithmetic a season's statistics are built from + MatchClockService.cs — The clock and the run of play: kick-off, ending a period, starting or + rolling into the next one, the final whistle. The arithmetic a + season's statistics are built from. There is no pause — the clock + runs from kick-off to the whistle and only a period boundary stops it MatchGoalService.cs — Goals logged live: storage delegated to GameService, the live minute and the recomputed scoreline added here MatchSubstitutionService.cs — The slot swap and the record of it, in one SaveChanges, plus undoing - the most recent one of a period + the most recent one of a period, plus SwapPositionsAsync — two players + already on trading slots, which writes no substitution row (so the undo + reads the slot back off the pitch, not off the row) LiveMatchOperation.cs — The write shape those three share: RunAdminAsync plus, on success, one LiveMatchNotifier call naming the game that changed LiveMatchQueries.cs — The tracked load they all start from (the game with its periods, via @@ -104,7 +110,10 @@ Pages/ MatchResult.razor(.cs)(.css)— /games/{id}/result — Score and goal entry LiveMatch.razor(.cs)(.css) — /games/{id}/live — Sideline screen: clock, subs, goals; admin drives, others watch LiveGoalDialog.razor(.cs) — Dialog: scorer, assister, own-goal toggle - LiveSubDialog.razor(.cs)(.css) — Dialog: pick the replacement for a player tapped on the pitch + LiveSubDialog.razor(.cs)(.css) — Dialog: for a player tapped on the pitch, either a replacement + from the bench or a position swap with someone already on + LiveNextLineupDialog.razor(.cs)(.css) — Dialog: what rolling on to the next line-up will change, + asked before it is done — advancing a period has no undo SeasonDialog.razor(.cs) — Dialog: season name, start date, end date Settings.razor(.cs) — /settings — Match preferences, own password, season management Users.razor(.cs) — /users — Accounts: add, edit, reset password, delete (Admin only) @@ -117,6 +126,8 @@ Components/ Pitch.razor(.cs)(.css) — The pitch. Read-only by default; Draggable for the builder, OnPlayerClicked for the live screen, Size for chip scale PlayerLabel.razor — A player as one line of text: "#7 Jasper" + PlannedChangesList.razor(.css) — What the next line-up does, as a team sheet; shared by the live + screen's card and the dialog that asks before applying it CancellableComponent.cs — Base for any component that reads: owns the CancellationToken its service reads take, tripped when the component is disposed SeasonAwarePage.cs — Base for pages that follow the season picker (a CancellableComponent) diff --git a/docs/known_issues.md b/docs/known_issues.md index d9ec2c7..67e6fd8 100644 --- a/docs/known_issues.md +++ b/docs/known_issues.md @@ -286,6 +286,31 @@ Avoid repeating these mistakes: the identical markup on `/settings` — those buttons rendered as native browser chrome for as long as nobody looked. There is no warning. Anything more than one page uses goes in `app.css`; `.action-btn`, `.badge-*`, `.stat-tile*` and `.stacked-table` are there for this reason. +- **The same trap catches a rule that never leaves its own page: a child component's root element + has no scope attribute either.** `.live-control-row > *` sat in `LiveMatch.razor.css` and matched + nothing, because every child of that row is a `MudButton` and the `