Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ 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 # 389 tests, xUnit v3, real SQLite
dotnet test # 401 tests, xUnit v3, real SQLite
cd src/FootballFormation.Web && dotnet run # http://localhost:5228
cd tests/ui && npm test # 39 Playwright tests in a browser, ~1 min (npm install first)
scripts/visual-check.sh # screenshots every page, then measures every touch target
Expand Down
16 changes: 8 additions & 8 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +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 (rendered by
UI/Components/PlannedChangesList, shared by the live card and its dialog)
PlannedChangesReport.cs — What the next period changes versus the one on the pitch, minus the
swaps play has already overtaken (Build() for the card in
UI/Components/PlannedChangesList, Swaps() for MatchClockService, which
applies the overtaken half rather than deciding again)
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
Expand Down Expand Up @@ -112,8 +114,6 @@ Pages/
LiveGoalDialog.razor(.cs) — Dialog: scorer, assister, own-goal toggle
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)
Expand All @@ -126,8 +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
PlannedChangesList.razor(.css) — What the next line-up does, as a team sheet, for the live
screen's "Changes at half-way" card
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)
Expand All @@ -150,8 +150,8 @@ Helpers/
PitchPositionHelper.cs — Maps PlayerPosition → (left%, top%) coordinates
UiFeedback.cs — Snackbar.Report()/ReportFailure() over Result (translates the error,
stays silent about a cancelled one), shared LockedDialog options
DialogPrompts.cs — ConfirmAsync()/ConfirmDeleteAsync(), and PromptAsync()/PromptValueAsync()
for an editing dialog that returns a value
DialogPrompts.cs — ConfirmAsync()/ConfirmDeleteAsync(), and PromptAsync() for an
editing dialog that returns a value
LineupDragState.cs — In-flight drag on the formation builder
PrincipalExtensions.cs — ClaimsPrincipal.IsAdmin()/DisplayName()/UserId(). Use IsAdmin(), never
Identity.IsAuthenticated — the two only agree while Admin is the one role
Expand Down
17 changes: 13 additions & 4 deletions docs/known_issues.md
Original file line number Diff line number Diff line change
Expand Up @@ -276,9 +276,10 @@ Avoid repeating these mistakes:
specify different base classes*, because the generated Razor partial already declares
`: ComponentBase`. Use `@inherits SeasonAwarePage` in the markup file.
- **A generic dialog result can't tell `default` from "cancelled"**: `PromptAsync<TDialog, TResult>`
is constrained to `class` for that reason; a dialog returning a value type uses
`PromptValueAsync`, which hands back `TValue?`. A dialog closing with `0` is otherwise
indistinguishable from the user pressing Cancel.
is constrained to `class` for that reason. A dialog closing with `0` is otherwise
indistinguishable from the user pressing Cancel, so one returning a value type needs its own
helper handing back `TValue?` — there was a `PromptValueAsync` doing exactly that until its last
caller went, and adding another value-typed dialog means writing it again.

## Result
- **A cancelled call is a failure with no message, and both halves matter.** Threading a
Expand Down Expand Up @@ -346,11 +347,19 @@ Avoid repeating these mistakes:
from the lineup as it finally stands, so a swap credits **the position moved into** for the whole
period, earlier minutes included — the opposite of what its comment used to claim. Totals are
right either way; only the split by position is affected, and a test pins it.
- **Advancing a period rewrites the next period's stored line-up.** `AdvancePeriodAsync` is not
purely a clock move: where a live substitution has already answered one of the next line-up's
swaps, it keeps the player who came on and benches the arrival the plan named — otherwise an
injury replacement is pulled straight back off at the quarter boundary. So the line-up the
formation builder shows for Q2 after a match has been run is not necessarily the one that was
saved, and that is deliberate rather than a lost edit. The rule is `PlannedChangesReport.Swaps`,
shared with the live screen's "Changes at half-way" card so the list and the button cannot part
ways; change one and the other follows.
- **Removing a control does not remove the state it could leave behind.** Deleting pause/resume
left `MatchState=InProgress` + a live period + `ClockRunningSince=null` unreachable going
forward, but still storable by a row an older build wrote. `AdvancePeriodAsync` deliberately
leaves the anchor alone — so on such a row it would roll on to the next line-up with the clock
still frozen, banking no minutes for the rest of the half while the dialog promised the opposite.
still frozen, banking no minutes for the rest of the half while the screen said it kept running.
It now restarts a stopped anchor, which is a no-op for every game that was never paused.

## General
Expand Down
45 changes: 31 additions & 14 deletions docs/ui_components.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,11 +107,11 @@ watches the same URL read-only. Every control sits in an `<AuthorizeView Roles="
the score, clock and state from its first load while newly inserted goals appear alongside them —
a live screen stuck at the old scoreline. Identity resolution keeps shared `Player` rows single.
- Controls are context-sensitive, and **only half time is a break**. A quarters game is still two
halves: mid-half the screen offers **"Next line-up"**, which calls `AdvancePeriodAsync` and rolls
the *next quarter's planned lineup* onto the pitch without stopping the clock. It **asks first**
(`LiveNextLineupDialog`), listing the swaps and moves it is about to make: advancing a period has
no undo — no timeline event records it — and the changes used to be readable only from a card
further down the screen. Only after the first
halves, and the mid-half line-up change is not a clock control: **"Next line-up"** lives at the
foot of the "Changes at half-way" card (below), not in `.live-controls`. It calls
`AdvancePeriodAsync` and rolls the *next quarter's planned lineup* onto the pitch without
stopping the clock. It asks nothing first — the list it sits under already says what the tap is
about to do, which is what the dialog it replaced was for. Only after the first
half or Q2 does the screen offer "Half time" (`EndPeriodAsync`, which does stop it) followed by
"Start 2nd half". The rule lives in `PeriodTypeExtensions.IsFollowedByBreak`, not in the page.
- **There is no pause.** The clock runs from kick-off until the period is whistled off, and only
Expand All @@ -126,8 +126,8 @@ watches the same URL read-only. Every control sits in an `<AuthorizeView Roles="
the children are `MudButton`s, and the old scoped `.live-control-row > *` rule never matched
them — the classic CSS-isolation miss, and why they used not to fill the panel.
- The pitch shows the live period; at the break and after full time the last one played, and before
kick-off the first — so it is never blank when a lineup exists. A **"Show subs" checkbox** folds
the bench strip under it away; the state is per circuit and deliberately not stored.
kick-off the first — so it is never blank when a lineup exists. The bench strip under it is
always drawn.
- **Tapping a player offers two changes, one dropdown each** (`LiveSubDialog`): someone comes on for
them (`SubstituteAsync`), or they trade positions with a team-mate who stays on
(`SwapPositionsAsync`). Choosing in either list clears the other, so the single action button
Expand All @@ -144,16 +144,33 @@ watches the same URL read-only. Every control sits in an `<AuthorizeView Roles="
scoreboard's order — home side first. It is counted forwards over the whole match and looked up
by goal id, because the timeline itself runs newest first and a total accumulated while rendering
would count down.
- A **"Show substitutions" checkbox** (`.live-timeline-toggle`) drops the substitutions from the
timeline and leaves the goals: a rotated squad buries the goals among swaps nobody is scrolling
back for. The state is per circuit and deliberately not stored.
- Finishing asks for confirmation via `DialogPrompts.ConfirmAsync` (not `ConfirmDeleteAsync`,
whose button says "Delete"). "Next line-up" needs a list rather than a sentence, so it has its own
dialog and goes through `PromptValueAsync<…, bool>` instead.
- **`PlannedChangesList` is the one rendering of what the next line-up does.** The card and the
dialog show the same thing, so the markup and its `.planned-*` styling live in the component —
scoped CSS follows the file that owns the elements, so moving the markup and leaving the CSS
behind would have left both callers unstyled.
whose button says "Delete").
- **The "Changes at half-way" card is what the mid-half line-up change is made from.** It lists
what `PlannedChangesReport` makes of the difference between the two planned line-ups
(`PlannedChangesList`, which owns the `.planned-*` styling), and carries the "Next line-up"
button underneath. Admin only, like the minutes table. It stays on screen while the change can
be made even when nothing differs — otherwise the only way on to the next quarter would vanish
exactly when nobody needs swapping — and appears before kick-off, without the button, as
something to read.
- **Only viable changes are listed, and only viable changes are made.** The report is handed the
substitutions already made in the period so it can rewind to the line-up that kicked off. A swap
whose outgoing player has since been taken off is dropped: the difference between the line-ups
still names their slot, but it now proposes withdrawing whoever came on for them, which nobody
planned. `AdvancePeriodAsync` drops the same swaps when it rolls the line-up on — it applies
`PlannedChangesReport.Swaps(...).Overtaken` rather than forming a second opinion, because a card
promising one thing while the button under it does another is worse than either behaviour alone.
An injury replacement therefore keeps the place for the rest of the half instead of lasting
exactly one quarter.
- **Minutes played is admin-only** (`LiveMinutesReport`), and shows exact time on the pitch rather
than the `periodsPlaying × periodDuration` estimate the planning screens use. It is a computed
property, so the running player's total climbs with the clock tick.
property, so the running player's total climbs with the clock tick. Until the first kick-off
there is no time played at all and the figures are the planned line-up costed at a full period
each, so the card is headed **"Planned minutes"** rather than "Minutes played"
(`Game.HasActualTimings`) — the numbers cannot say which they are, so the heading does.
- **Mobile reorders the column with flex `order`**: what just happened matters more at a touchline
than where everyone stands, so the line-up card (`.live-lineup`, `order: 1`) and the minutes
table (`.live-minutes-card`, `order: 2`) drop below the timeline under 600px. Both rules live in
Expand Down
Loading