Skip to content

fix: iPad operator fixes; a team's lineup carries between matches - #458

Merged
gitrgoliveira merged 40 commits into
mainfrom
worktree-bridge-cse_018uNoSEX98xtx8fduygp2Qe
Oct 7, 2026
Merged

gitrgoliveira merged 40 commits into
mainfrom
worktree-bridge-cse_018uNoSEX98xtx8fduygp2Qe

Conversation

@gitrgoliveira

@gitrgoliveira gitrgoliveira commented Oct 5, 2026 •

Copy link
Copy Markdown
Owner

Summary

Six operator-path fixes from the iPad persona review (1180x820, coarse pointer), one PR, plus the lineup rule they led to and the code review that followed it:

  • bc-cfbd: a double tap on a button that opens a layer no longer acts on that layer. Each layer a tap opens takes the bounce of that tap for TAP_BOUNCE_MS: the confirms (DialogHost, Modal, the court console's two), the participant Edit dialog, the public match card, the sign-in dialog, and the overlay score editors and the at-court lineup panel (their backdrop only, so their controls work at once). Keyboard activation is never swallowed. A confirm now opens above every overlay.
  • bc-tp44: under a coarse pointer, the encho label and +/- steppers, the team sheet's name box (the input itself), the lineup panel's Rename link and the kachinuki collapse caret reach 44px via --tap-floor in classes. Fine pointers are unchanged. The lineup panel's position row is now a div: as a <label> with no for, a tap on the position name activated Rename.
  • bc-lnrf: a lineup refusal, an identity warning, or a failed lineup save on the team score sheet renders inside the bout row and side the operator typed in (red role=alert when nothing was written, amber role=status when the save landed), instead of below every bout.
  • bc-lpfb (operator ruling 2026-10-05): a team's lineup carries from its previous team match unless one is entered for a match. One engine rule (engine.LineupInForce) decides the lineup a team fields at a match, and every surface asks it: the score sheet, the at-court lineup panel, the viewer, the court display, the streaming overlay, the kachinuki roster and the Kachinuki Detail export (new public read GET .../teams/:tid/lineup-in-force/:matchId). A lineup is matched by the team's participant id only, and discarding a draw removes the lineups saved for its matches. The panel shows where its lineup comes from ("Lineup for this match", "Same as Pool D · Match 1", "Starting lineup"). The Lineups page's Round box is replaced by Lineup for: the team's starting lineup or any of its matches. Copy from previous match is gone (carrying over is the default); Use the previous match's lineup removes a match's own lineup so it inherits again.
  • bc-lnul (operator decision, option A): unsaved lineup edits survive a reload, the app's Back and closing the panel: both lineup editors keep them as a draft in the browser tab and offer them back with "Unsaved lineup changes restored" and Discard. Never saved until Save lineup.
  • bc-tmfd (operator decision, option B): the team header and the IV/PW + RESULT band are one sticky bar under the admin topbar (whose height AdminTopbar publishes as --topbar-stack-h), and the footer actions (Finish / Record bout / End match) are a sticky dock at the bottom. The name list opens toward the larger room between the two bars and caps its height. In the Scores overlay a team of six or more scrolls its bouts the same way under the pinned bar (before this fix, measured on this branch, its bout list was 8 px tall and no bout could be scored).
  • A lineup save names the positions it changed (code review; operator decisions 2026-10-05 and 2026-10-07, "Only changed positions"):
    • Both lineup saves send changed, and the server writes only those positions, under the competition's lock, onto the lineup stored for that match or starting lineup, or, for a match with no lineup of its own, onto the lineup in force there. The answer is the whole lineup as stored, which the editors then show. Two devices changing different positions of one lineup both keep their change in either arrival order, a save sent later from a device that was offline included; the same position keeps the later arrival. A save without changed replaces the whole lineup, which is how a save queued by the previous build replays.
    • The offline queue keeps one save per lineup and joins a later save into it position by position. The editors no longer read the lineup again at Save, and the team sheet's name box sends its one position.
    • Both lineup editors save only after a successful read (Try again otherwise). An editor with no unsaved change shows a lineup saved on another device, with the team's members read again. Both lineup reads have the 12-second deadline the other requests have.
    • A name picked on a sheet side whose lineup has not been read yet reads that side first, and is refused only when it cannot be read: "The lineup could not be read, so this name was not saved. Check the connection and try again." A lineup never read would show positions empty that may not be. The name boxes look disabled while a pick is being saved.
  • Lineups an earlier release saved for a round (operator decision 2026-10-06: migrate them; every match shows what v2.1.1 showed): releases up to v2.1.1 saved a lineup per round, and their score sheet read a match's lineup as the match's own, else the round lineup with the highest round at or below the match's round, else the highest round. Such lineups are converted rather than read:
    • Every match a team with a round lineup is seated in gets a lineup of its own equal to what v2.1.1 showed there, at load and then in the write that seats the team (a knockout winner advancing, a Swiss round generated).
    • Each team and match is settled once; config.md records them (round_lineups_given, team id to match ids), so a lineup the operator removes with Use the previous match's lineup stays removed. Discarding a draw clears the record, so the new draw is given the converted lineups again.
    • A team with no starting lineup gets the one v2.1.1 showed before its first saved round (its highest round's), and a lineup saved under a team's name is stored under the team's id.
    • The round lineups stay on disk, never read, until the competition is completed; then the next write of the draw or the next start removes them and sets the round_lineups_converted marker. A new competition starts with it set. The Lineups page's later-round note and Remove it are gone, and PUT .../lineups/:round refuses a round above 0.
    • v2.1.1's lineup panel showed only a lineup saved at the match's exact round (blank boxes otherwise), so it disagreed with its own score sheet; both now show the sheet's lineup.
    • A team that had a lineup saved for a match by v2.1.1 is converted too (operator decision 2026-10-07, "Show what v2.1.1 showed"): v2.1.1 used such a lineup for that match alone, so carrying it on would have changed what the team's later matches show, fought ones included. Each match the team is seated in gets what v2.1.1 showed there: the lineup entered for it, else the team's starting lineup, or an empty lineup of its own where v2.1.1 showed none. The list of these teams is recorded once, when the competition is first settled (round_lineups_legacy), so a lineup entered after the upgrade carries as usual. A discarded or regenerated draw drops them from the list, since their match lineups went with it.
  • Three leftovers closed (operator decision 2026-10-06): the at-court panel refuses one new name typed at two positions before anything is written; an editor's own change to the team's members (a rename, a cleared name, an added member) is no longer undone by a read of the members that began before it; and the overlay's pinned bar covers the strip above the team names when the unreadable-data note comes before it.
  • Second round of decisions and findings (2026-10-06): the team score sheet keeps a member name it wrote over an older list of the team's members (since 2026-10-07 by the members' server stamps, below), so a mistaken repeat of that name is refused rather than added as a second member. Ending a reopened match from the editor that reopened it records it again: End used to send a write naming no change, and the match stayed running with no error (found by PR test(bc-e2eh, mp-yqxn.2): e2e harness and a clumsy-operator review of knockout and mixed #442's kachinuki journey; the code that causes it is on main too). A refusal names a position the same way on every surface and goes once a position changes. PR test(bc-e2eh, mp-yqxn.2): e2e harness and a clumsy-operator review of knockout and mixed #442's journeys are updated on its own branch for this PR.
  • Third round (2026-10-06), the team's members:
    • A name typed before the team's members have been read waits for them, up to the 12-second deadline, in the at-court panel, the Lineups page and the team sheet. Against no list, a new name used to be added as a new member where the position's seeded slot was free, and the name of a member the team already had was saved without its member id. A read that fails or does not answer ends the wait for good, with the members-unavailable warning.
    • The admin team sheet reads the members again whenever a lineup or member change is announced, so a member renamed or added on another device shows without reopening, and a name the sheet wrote gives way to a newer name given elsewhere.
    • A sheet side given another team (its feeding match corrected elsewhere) drops the old team's members and lineup, reads the new team's, and does not write a name typed for the old team; the team's id arriving for a side that carried only its name is not such a change.
  • Operator rulings 2026-10-06, controls that do not move under the finger: the team sheet's "Tap a scored mark to clear it" line holds its space before the first point, so the bout rows no longer drop 28 px at a bout's first point (a quick second tap on M then opened the fighter list). Undo encho holds its place beside Encho, so a double tap on Encho no longer lands on Undo encho and cancels itself: it now gives overtime (two periods stored, shown as (E)).
  • Also found and fixed this round: Escape on an open fighter list in the Scores overlay closed the whole score editor; it now closes the list only. The court console's own confirms (the court move, Send back to queue) now take focus, close on Escape while their request is not out, and give focus back to the control that opened them; from a keyboard they ignored Escape and dropped focus to the page. The court picker on every queue and score row took focus as it mounted, so focus jumped to the last row's picker (with its focus ring) on load and whenever a row appeared, and the page scrolled to it; it now gives focus back only when its list closes. A control a keyboard Tab or a scroll brings only just into view no longer stops under the team sheet's pinned header or footer dock: the sheet publishes the bars' heights and its own content controls carry them as scroll margin (margin on the content, never padding on the scroller, which made focusing a bar's own button scroll the page about 300 px).
  • Code review of 40b1af57, and the operator's three decisions on it (2026-10-07):
    • Team members carry a server stamp ("Do it in this PR"): every write that adds, names, renames or clears a member stamps it (modifiedAt: the server's time, or one more than that member's previous stamp) and answers with the member; renaming and clearing answer 200 with it instead of 204, and adding a member is announced like the other member writes. The editors and the sheet keep each member's newer copy, so an older list can never put back an old name and a change made on another device always shows. This replaced the client's pending-name and read-order rules.
    • Fixed without a decision (ec422925): a Setup roster save that gave teams their ids deleted the lineups stored under their names; a draw that failed and was retried kept the first attempt's converted lineups; a Swiss round's odd team out got a lineup for a match nobody plays; the member-id repair ran before the conversion, so converted lineups had no member ids; the Kachinuki Detail sheet ordered positions as text (Chuken before Senpo, 10 before 2); the at-court panel stayed on a team a correction had moved out of its match; the Lineups page edited the starting lineup when its chosen match no longer held the team (it now keeps that match chosen, locked, and says so); the panel is now a dialog (it closes on Escape and gives focus back, and like the overlay score editors it guards only its backdrop, so its controls work at once); a queued name pick read as saved; member writes had no deadline; Escape in a name box with no list drawn needed two presses; the court picker took focus back after a tap elsewhere; modals sat under announcement cards; ending a match reopened with a correction reason sent no result; an edited editor stopped following member changes made elsewhere; a draft the operator's own save had already landed was reported as not restored. The lineup change event now names the team and the match, and each editor follows only its own team's.
    • Also: an unnamed member's rename box on the Lineups page is labelled with its slot ("Rename T2.1"), not "Rename ".
    • A lineup save held on the device (operator decision 2026-10-07, "Held-write words, pending icon"; found in the browser): the Lineups page and the at-court panel said "Offline: lineup not saved yet, will retry" beside the success check every toast that is not an error carried. They now say what the score editors say for a held write, "Not sent yet: saved on this device, and sent when the connection returns." (or "keep this page open" when the browser could not store it), in a pending toast with an hourglass (5000ebca).

Why

  • cfbd: Preact renders the dialog on the microtask after the trigger's click, so the second click of a bouncing tap landed on the new dialog layer: on the backdrop or Cancel it dismissed the confirm, and on Confirm it confirmed it unasked. The sign-in dialog took the bounce as a submit of an empty password.
  • tp44: these controls had local sizes that predate --tap-floor: an inline style that outranked the .btn floors, a fixed 28px stepper, an unfloored label, a button rotated as a whole (so 44x36 hit-tested as 36x44), and a bar whose floor counted its own borders.
  • lnrf: the editor had one sheet-level message slot for outcomes produced by one row's name box.
  • tmfd: the band and the actions were laid out after the bout list, and the inline host's overflow: hidden made the band's sticky style inert.
  • lpfb: three places picked a team's lineup three ways. The panel read the match's exact round (empty boxes under "Inheriting round default" while the sheet showed names, and a Save there wiped them); the score sheet read a pool match's lineup from its draw round, which is not the playing order (a pool's first match showed the Round 2 lineup); the engine read Round 1 for every pool match.
  • lnul: typed lineup positions lived only in component state.
  • Lineup writes: a name write and a Save restate the whole lineup, and both were composed on the copy the screen read when it opened. A failed read looked like "nothing saved", so a write could store a partial lineup as the match's own, which every later match then carries; and a Save put back, on every position the operator left alone, the value it had read, over a change another device had made since.

Files changed

File What
internal/engine/lineup_in_force.go (new) The one rule for the lineup a team fields at a match (its own, else the latest earlier match's, else the starting lineup); id-only matching
internal/engine/kachinuki.go, kachinuki_export.go The kachinuki roster and the Kachinuki Detail export ask that rule, built from the matches they already loaded; findTeamMatch through findMatchHome; a name at two positions labelled deterministically
internal/engine/competition.go Discarding a draw, and starting one, clears the lineups saved for its matches and the conversion's records first (Store.ClearDrawLineups); Competition.IsTeam() replaces this PR's and seven identical spellings
internal/state/draw_matches.go (new) DrawMatch/DrawMatches: a no-copy projection of the cached draw for the lineup rule; a bye is marked Hidden, and a pool or league match carries its stored circle round (PoolRound)
internal/state/round_lineups.go (new) The conversion of round lineups (settleRoundLineups): v2.1.1's reading (legacyReading, at the round legacyLineupRound gives the match) becomes the own lineup of every match a team is seated in, once per team and match, at load and in every write of the draw; the round lineups go once the competition is completed. A team with a v2.1.1 match lineup at the first settlement is one too, recorded in round_lineups_legacy; where v2.1.1 showed no lineup the match gets an empty one of its own
internal/domain/team_member.go, internal/state/squad.go, store.go, internal/mobileapp/handlers_squad.go Members carry ModifiedAt (Store.stampMember: the server's time, or one more than the member's last stamp); member writes answer the member (PUT and DELETE 200), and an add is announced
internal/state/match_order.go, draw_matches.go, internal/engine/lineup_in_force.go state.TeamMatches, the one list of a draw's team matches (a bye, a pool daihyosen or tie-break bout and a vestigial bracket are none); engine.LineupInForceFrom over what a transaction loaded; LineupInForce copies only the lineup it returns
web-mobile/js/lineup_save.jsx (new) changedLineupSave, every save's body, and joinQueuedLineupSave, the queue's join of two saves of one lineup
web-mobile/js/dialog_focus.jsx (new), admin_schedule_score_editor.jsx, admin_shiaijo.jsx useDialogFocus: focus into a layer as it opens and back to its opener without scrolling (the console's confirms, the lineup panel); the panel's hosts keep its match's key and read the row every render
internal/mobileapp/handlers_lineup_changed_test.go, handlers_team_member_stamp_test.go, handlers_lineup_event_test.go, internal/state/round_lineups_match_only_test.go, team_member_stamp_test.go, internal/engine/round_lineups_match_only_test.go (new) The changed save, the member stamp and answers, the event payload, the v2.1.1 match-lineup conversion
js/__tests__/lineup_save, lineup_save_queue, helpers/lineup_server, helpers/team_members, render/lineup_panel_hosts, render/lineup_panel_overlay (new) The save body and the queue's join (offline device against a server that follows the contract), stamped members, the panel following its match and as a dialog
internal/state/match_order.go (new) Match order (MatchPlace, DrawMatch.Place/Seats), shared by the round-lineup conversion and the engine rule (swiss.go, match_label.go delegate to it)
internal/state/transactions.go, bracket.go, pools.go, models.go, competition.go The conversion runs at the end of a transaction that wrote the draw and after each direct draw write; Competition.RoundLineupsConverted and RoundLineupsGiven (team id to match ids, deep-copied)
internal/mobileapp/handlers_competition.go A new competition starts with the marker set
web-mobile/js/admin_scoring_autosave.jsx useChangedGroups agrees with the result the match holds once it leaves completed (any reopen, a send back to the queue), so End names it
web-mobile/js/lineup_draft.jsx, admin_scoring_team.jsx (members) mergeMembers (each member's newer copy, by its server stamp) and newMembersWait; useLineupForm.waitForMembers, changeMembers and memberRenamed(member), which merge the answered member and read nothing; the sheet's takeSideMembers/wroteSideMembers on the same merge; reads begun in one tick share one request
web-mobile/js/admin_lineup.jsx, admin_schedule_lineup.jsx (members) A typed name waits for the team's members before it is resolved
web-mobile/js/lineup_resolver.jsx (lineupPositionLabel) The one label for a position in a refusal, used by the panel, the Lineups page and the sheet
js/__tests__/render/team_editor_member_writes_stand, lineup_draft_members, pick_manual_bout_name (new); render/kachinuki_reopen, render/changed_groups_every_editor, render/lineup_editors_save_only_changes The sheet's member writes; End after a reopen; a reopen moves only the result; a refusal goes on a position change and names Position N
internal/state/team_lineup.go ClearDrawLineups (one transaction: a draw's match lineups, the given record, the legacy list trimmed); FindBestLineup/FindBestLineupAny removed (no caller left)
internal/state/legacy_upgrade.go upgradeRoundLineupsLocked, the load pass of the conversion, whose copy of the competition carries the marker and the record for the load steps after it; comment: who calls EnsureLegacyUpgraded, none of them holding the competition's lock
internal/domain/team_lineup.go ApplyChanges (a save's changed positions landed on a base) and ComparePositions (the one position order, for the roster, the export and the member-id repair); comment: which lineup a team fields is decided by the engine rule
internal/mobileapp/handlers_lineup.go, deps.go, server.go New public lineup-in-force read; the dead ?fallback=best branch removed; a lineup PUT for a round above 0 is refused (400); both PUTs may name their changed positions, landed under the lock on lineupSave.base (the stored lineup, else the one in force); the change event names the team and the match
web-mobile/js/tap_guard.jsx useOpenedTapGuard: the one owner of "a layer takes the bounce of the tap that opened it"
web-mobile/js/ui.jsx DialogHost and Modal take that bounce; a dialog's backdrop stacks above every overlay; a pending toast (hourglass, a polite status) for a write the device holds
web-mobile/js/admin_shiaijo.jsx The court move and "Send back to queue?" confirms take it too, and take focus, close on Escape and give focus back
web-mobile/js/admin_participants.jsx, viewer_match.jsx, app.jsx The participant Edit dialog, the public match card and the sign-in dialog take it too; the viewer no longer declares a round on an opened match
web-mobile/js/admin_scoring_individual.jsx, admin_scoring_engi.jsx The overlay editors take it on their backdrop only
web-mobile/js/admin_scoring_team.jsx The clear-mark line and Undo encho hold their space (.holds-space); caret rotates its glyph only; row-keyed lineup notice; header and band in .team-sheet-pin; useMatchLineups reads both sides, re-reads before a write, reads an unread side on a pick and when the connection returns; the overlay takes the opening bounce on its backdrop
web-mobile/js/admin_scoring_shared.jsx LineupNameInput places its list between the pinned bars, measured against the box that clips it; Escape on an open list closes the list only; a box with no list drawn lets Escape through to the editor
web-mobile/js/admin_shell.jsx, published_height.jsx (new) CourtPicker takes no focus as it mounts, and gives it back only for a close made inside it; AdminTopbar publishes --topbar-stack-h through publishHeight, the one owner of a published bar height, which the team sheet also uses for --team-pin-h and --team-dock-h
web-mobile/package-lock.json, web/package-lock.json source-map-js 1.2.1 to 1.2.2 (GHSA-68fv-2mgg-jv7q, a development-only dependency through vite and jsdom; the advisory, published 2026-09-18 and updated 2026-10-05, now fails the security scan on main too)
web-mobile/css/styles.css .holds-space (hidden, its box kept); scroll margin on the team sheet's own content by the published bar heights (never scroll padding); the overlay's bouts flow in its scrolling body for every team size, and its pinned bar covers the body's top padding; tap floors; sticky team header and footer dock; a disabled name box looks disabled; dialogs and toasts above overlays; .modal-backdrop stacks above announcement cards (1000 over 900)
web-mobile/js/lineup_draft.jsx (new) The unsaved-lineup draft; useLineupForm, the one state of both lineup editors (read first, follow changes, the team's members, save only changes, remove); changeMembers, the one door for an editor's own change to the members
web-mobile/js/lineup_resolver.jsx resolveMatchLineup reads the lineup in force; changedLineupPositions, alreadyPlacedNote, lineupDuplicateNote, the source labels and read-failure wording; buildInlineLineupWrite also returns its one changed position
web-mobile/js/admin_lineup.jsx Lineups page: Lineup for (starting lineup or a match), draft, Use the previous match's lineup, save only changes; the later-round note and Remove it removed; a save sends only the changed positions and shows the answered lineup; a chosen match the team is no longer in stays chosen, locked; typedNameTarget, the one order for placing a typed name; an unnamed member's rename box is named by its slot
web-mobile/js/admin_schedule_lineup.jsx At-court panel: lineup in force with its source, Save only after a read and only of changes, draft, Copy removed; a new name typed at two positions refused before anything is written; a dialog (Escape unless a write is out, focus back; only its backdrop guarded against the opening tap, like the score editors); a save sends only the changed positions
web-mobile/js/lineup_rename.jsx The rename row's buttons are off while the editor's own save is out
web-mobile/js/admin_schedule.jsx, admin_competition.jsx Copy's helper unexported; the Lineups list is handed the draw it lists matches from
web-mobile/js/api_client.jsx fetchLineupInForce; both lineup reads bounded; a lineup save takes changed, and a save of a lineup with one queued joins it (joinQueuedLineupSave); queuedLineupSave answers whether one is queued; member writes bounded (_memberWrite) and answered with the member; a lineup removal bounded (_deleteLineup folded into deleteMatchLineup); fetchMatchLineup and deleteTeamLineup removed
web-mobile/js/write_result.jsx withinDeadline/TIMED_OUT moved here beside FETCH_TIMEOUT_MS
web-mobile/js/match_scoreboard.jsx, display_scoreboard.jsx, streaming_overlay.jsx, viewer_competition.jsx, viewer_utils.jsx Read the lineup in force; no surface derives a round
web-mobile/js/admin_helpers.jsx resolveRoundIndex removed (no caller left)
web-mobile/check-imports.mjs The new and changed ES-imported modules are checked
docs/user-guide/organisers/team-tournaments.md, docs/architecture/data-model.md, specs/openapi.yaml, specs/005-per-match-lineup/spec.md, CLAUDE.md The lineup rule, the Lineups page, the draft, the editors' save rule; a save of changed positions, the v2.1.1 match-lineup conversion and the member stamp (changed, TeamLineupSave, modifiedAt, the member answers and the DELETE that was missing from the API spec)
docs/user-guide/court-operators/recording-decisions.md The team result is under the team names, at the top of the sheet
docs/screenshots/kachinuki-*.png (4), team-lineup.png Recaptured: the pinned header; Lineup for instead of Round; the clear-mark line's held space (kachinuki-scoring-buttons) and Undo encho's held place (kachinuki-knockout-tie-encho)
docs/videos/kachinuki-demo.webm Re-recorded for the two held spaces; its chapter marks still round to the list under it
internal/engine/lineup_in_force_test.go (new), kachinuki_test.go, kachinuki_export_test.go, competition_test.go, load_error_propagation_test.go The rule's tiers, match order, id-only matching, damaged files; roster and export on it; discard clears match lineups
internal/state/draw_matches_test.go (new), team_lineup_match_test.go, team_lineup_test.go, legacy_upgrade_lineup_ids_test.go The projection; deleting match lineups; the removed finders' tests
internal/mobileapp/handlers_lineup_test.go The lineup-in-force read: tiers, source fields, unknown competition; the PUT refusal above round 0
internal/state/round_lineups_test.go, round_lineups_upgrade_test.go, match_order_test.go, internal/engine/round_lineups_settlement_test.go, internal/mobileapp/handlers_round_lineups_marker_test.go, handlers_round_lineups_upgrade_test.go (new) The conversion at load and through every write door, in a transaction and aborted; every match of real v2.1.1 draws (leagues of 5 and 6, a mixed draw of 8) against an independent v2.1.1 oracle; a semi-final-only lineup does not hide the round lineup at the final; a removal stands across writes and a reload; a discarded draw is given its readings again; the record readable in config.md; a correction before completion; retirement at completion, also by the next load; the load steps after the conversion keep the record; the settings save keeps it; the marker at creation; the public read
js/__tests__/render/lineup_editors_wait_for_members, team_editor_members_read_first, lineup_name_input_escape, court_picker_focus (new); render/admin_shiaijo (the console confirms from a keyboard); lineup_form, lineup_draft_members, render/team_editor_member_writes_stand, render/team_editor_clear_hint, render/kachinuki_reopen A typed name waits for the members, in each editor; the deadline ends a wait; a side given another team (and one only resolved to its id); the old team's lineup dropped; the sheet follows a rename made elsewhere; only the opening read survives a change; the clear-mark line and Undo encho hold their space; Escape on an open list
js/__tests__/render/confirm_dialog_double_tap, auth_modal_opened_tap, participant_edit_dialog_opened_tap, match_viewer_modal_opened_tap, overlay_editor_opened_tap, dialog_stacks_above_overlays (new); tap_guard.test.jsx The opening-tap guard on every layer; stacking
js/__tests__/operator_tap_floor.test.jsx (new), render/team_editor_config_matrix The tap-floor and disabled-box rules; the caret's glyph rotates
js/__tests__/render/team_editor_pinned_bars, admin_topbar_stack_height, lineup_name_input_placement (new) The overlay's bouts never get a scroll area of their own and its bar covers the body's padding; the pinned bars, the published topbar height, where the list opens; the published pin and dock heights (inline and overlay, removed on unmount), the scroll-margin rules reaching only the sheet's own content, and no scroll padding by a bar height
js/__tests__/render/team_editor_lineup_refusal_at_row, team_editor_lineup_read_state (new); render/team_editor_error_surface, scoring_modal_match_lineup.test.jsx Notices in the row; the sheet's read, re-read, pick on an unread side, reconnect, write ordering
js/__tests__/lineup_form, lineup_draft, lineup_editors_read_first, lineup_changed_positions, lineup_source_label (new); render/lineup_editors_read_and_remove, lineup_editors_save_only_changes, lineup_source_line, lineups_page_targets, match_lineup_side_editor_draft, match_lineup_side_editor_use_previous, admin_lineup_draft (new); match_lineup_side_editor_in_force (new) Both editors: read first, follow, save only changes, members, draft, Use the previous match's lineup, the Lineups page targets
js/__tests__/api_client_match_lineup, write_result_deadline (new) The bounded reads and removals, the queued lineup save; the moved deadline helper
js/__tests__/match_lineup_side_editor_member_ids, match_lineup_side_editor_trim, match_lineup_panel_roster, admin_lineup_form, render/use_team_lineups_squads, viewer_team_lineup, viewer_bronze_match, streaming_overlay_score, render/kachinuki_reopen, render/fighter_pick_double_tap, render/self_run_daihyosen, render/self_run_team_names, render/viewer_competition_live_match, viewer_competition_round_labels, bracket_round_label_agreement, admin_helpers, admin_schedule_surface Adapted to the lineup in force, the save path, and the removed round helpers
js/__tests__/pick_copy_source.test.jsx, render/match_lineup_side_editor_order.render.test.jsx (deleted) Their code (Copy, the round order) is gone
42 render tests (listed below) The resolveRoundIndex: () => 0 stub dropped
The 42 render tests whose only change is the dropped stub

admin_scoring_modal, autosave_debounce, autosave_every_scoring_edit, autosave_pending_edit, changed_groups_every_editor, decision_form_closes, discarded_held_write_disarms, editor_barred_match, editor_every_way_out, finish_arm_dwell, individual_editor_config_matrix, individual_editor_hantei_correction, individual_editor_ko_tie_blocked, individual_editor_live_hantei, individual_editor_slot_labels, ippon_double_tap, kachinuki_cleared_bout_reaches_server, kachinuki_done_bout_double_tap, kachinuki_row_beyond_teamsize, kachinuki_same_name_winner_id, match_history_view, not_landed_banner_call_sites, not_saved_banner_no_retry, queued_write_banner, recorded_withdrawal_reopen, remove_withdrawal_correction, score_editor_dismiss_prompt, score_editor_dispatch, score_editor_mount_sites, score_editor_seed_from_score_string, self_run_team_members, start_then_strike_saves, team_bout_row_records_no_team_name, team_daihyosen_silence, team_editor_clear_hint, team_editor_data_issue, team_editor_superseded, team_editor_vacancy_not_flagged, team_finish_refuses_unfought_bouts, team_fusensho_keeps_points, team_fusensho_refusal_clears, withdrawal_correction_shows_result (each web-mobile/js/__tests__/render/<name>.render.test.jsx).

Screenshots

All at 1180x820 (iPad Air landscape) with a coarse pointer.

bc-tmfd: team sheet mid-scroll on the court console. The header and RESULT band are pinned under the topbar while bouts 3-5 scroll, and Finish + Start Next is pinned at the bottom.
pinned bars, court console

bc-tmfd: before / after (docs capture, kachinuki correction). Before, the band was below the bouts. After, it is under the team names.

Before (main) After
before after

bc-tmfd: Scores overlay. The header and band stay pinned at the top of the scrolling body.
overlay

bc-tmfd: name list near the dock. It opens upward, clear of both bars.
list opens up

bc-lnrf: the refusal in the row it came from (bout 2, Shiro side).
refusal at row

bc-cfbd: after a 30ms double tap on "Send back to queue". The confirm is still open.
confirm survives double tap

bc-lpfb: Pool D · Match 1 now shows the team's starting lineup. Before this PR it showed the team's Round 2 lineup (the pool draw's round, not the playing order).
pool match shows starting lineup

bc-lpfb: Lineups page, Lineup for = Pool D · Match 2. After a lineup was saved for Match 1, Match 2 carries it ("Same as Pool D · Match 1").
lineup carried from the previous match

bc-lpfb: Lineups page before / after (docs capture).

Before (main) After
before after

bc-lnul: after a reload, the panel offers the unsaved name back.
draft restored

bc-tp44: lineup panel. Rename is 48x44 under a coarse pointer.
rename

Review fix: "Use the previous match's lineup" asks on top of the panel (it opened under it).
confirm above the panel

Review fix: a lineup that could not be read. Plain words and Try again; the boxes and Save stay off until it is read.
editor read failed

Review fix: a name picked while every lineup read is held for 6 seconds. The boxes look disabled while the pick reads the side; the name then saves, with no refusal.
pick while the read is slow

Review fix: a pick whose read fails is refused in the box it was typed in, and nothing is written.
pick refused when the read fails

Review fix: an untouched panel shows a lineup saved on another device ("Other Device", "Lineup for this match").
panel follows another device

Operator decision: Save writes only what was changed. The operator typed Chuken while another device changed Senpo to "Second Change"; after the operator's Save the lineup holds both.
save only changes

Review fix: a Save refused because another device placed the same member. The refusal names the position the other device filled, and the form now shows it there (Senpo), so the operator can fix their own pick.
conflict shown

Browser pass: a team of six in the Scores overlay, before / after. Before, the bout list was 8 px tall under the result band; after, the six bouts scroll under the pinned bar.

Before After
six-person overlay before six-person overlay after

A refusal names where the member was picked, not the box their name was typed into (Noda picked at 1, typed at 3).
refusal names the picked position

Old round lineups, on data the real v2.1.1 release wrote (a team knockout and a mixed competition, lineups entered on v2.1.1's own pages). Every match shows what v2.1.1's score sheet showed there. Team A had a Round 2 lineup and its own lineup for the semi-final only: once A won the semi-final, the final shows A's Round 2 lineup (A-R2), as v2.1.1 read it, beside Team E's Round 3 lineup. In the mixed competition's pool, Team A's first match in playing order is a round-2 match, and shows its Round 2 lineup, as v2.1.1 did. Team C's semi-final, after Use the previous match's lineup, a score written elsewhere and a reload, still reads "Same as Match 2".

Final, score sheet Pool A · Match 1, score sheet Semi-final panel after the removal
the final shows A's Round 2 lineup the first pool match shows the Round 2 lineup the removed lineup stays removed

Real build, controls that hold their place (operator rulings 2026-10-06). Before the first point the clear-mark line's space is held; after one tap on M the line shows and M has not moved (measured 0 px). On a tied kachinuki bout the place for Undo encho is held beside Encho; one tap on Encho shows Undo encho in that place and Encho has not moved.

Before the first point After one tap on M
clear-mark line held clear-mark line shown, nothing moved
Tied bout After one tap on Encho
Undo encho's place held Undo encho in its place, Encho unmoved

Real build, the team's members. A rename made on the Lineups page in another tab shows in the open sheet's name list without reopening (K2.2 Shibata). A new name typed while the members read was held for 3 seconds renamed the seeded slot of its position once the read landed, with no new member. A correction in another tab that seats Team Bravo in the final makes the open final sheet show Bravo's lineup (Doi, Endo) and nothing of the team that was there.

Rename made elsewhere Name typed before the members load Side given another team
rename shows in the open sheet typed name renamed the seeded slot final shows the new team

The strip above the team names when the unreadable-data note comes first (team of six, scrolled). Before (the old rule re-applied in the page), a bout scrolled past shows above the names; after, the bar covers it.

Before After
strip before strip after

One new name typed at two positions is refused before anything is written ("No lineup saved yet" stays).
typed twice refused

Team score sheet: a repeat of a name the sheet just wrote is refused, after an older read of the team's members answered with that slot unnamed (bout 2: "Ito is already at Position 1.", nothing written).
sheet repeat refused

Operator decision 2026-10-07, "Only changed positions": two devices, one offline. Device 1 went offline and saved Senpo (T6.1): the save waits on the device ("Offline: lineup not saved yet, will retry"). Device 2, online, saved Taisho (T6.2). Device 1 came back online and its save was sent.
device 1 offline save

The same lineup read on device 2 after the reconnect: Senpo T6.1 (device 1) and Taisho T6.2 (device 2), both kept.
both changes kept

Operator decision 2026-10-07, "Held-write words, pending icon": a lineup save held on the device now reads as the score editors' held write, with an hourglass instead of the success check.
held lineup save

Operator decision 2026-10-07, "Show what v2.1.1 showed", on data the v2.1.1 release wrote. Team A had a starting lineup (A0) and a lineup entered for its first match (A1). Its second match shows the starting lineup, as v2.1.1 did, as that match's own (the names read "Unresolved" because v2.1.1 saved names with no member):
v2.1.1 match-lineup team, later match

Team B had a lineup for its first match and no starting lineup: v2.1.1 showed none at its second match, so that match has an empty lineup of its own, with Use the previous match's lineup beside it.
empty lineup where v2.1.1 showed none

Team E had only a starting lineup in v2.1.1: a lineup entered for its Match 2 after the upgrade carries to Match 3 ("Same as Pool A · Match 2"), also after a restart.
a lineup entered after the upgrade carries

Members carry a stamp: a rename on another device reaches an open score sheet. Team C's T2.1 was renamed "Kaito Ono" on the Lineups page of another device; the sheet, never reopened, lists it.
rename reaches the open sheet

Self-run public page: a participant's score editor over an announcement card. The card (top right) sits under the editor's backdrop; a hit test where the two overlap lands in the editor.
editor above the announcement

Code review

First review (before the lineup rule)

Each finding was verified before fixing. Fixed in Keep the name list and the dock under the topbar on the team sheet and Guard every tap-opened layer through one tap_guard helper:

  • Open name list over the bars (found in browser): with a name list open, scrolling the sheet painted the list over the pinned header and the admin topbar. The editor body has container-type, which in Chrome creates no stacking context, so the list's z-index: 60 beat the topbar's 30. Inside a score editor the list is now z-index: 8, under the pinned header (9), and slides under the bars like its input.
  • Footer dock over the topbar's held-writes panel: the dock is now z-index: 20, under the topbar (30).
  • Actions scrolled out of the dock: a tall prompt, banner or History could push Finish / End match into the dock's own scroll. The action row is now sticky at the dock's bottom.
  • overflow: clip before Safari 16: an @supports fallback keeps the bars pinned (only the rounded-corner clip is lost).
  • List placement: it measures against the overlay's (static) footer and the iPad's visual viewport (on-screen keyboard), re-measures on scroll and resize while open, and the height cap lives in one place (JS).
  • Lineup panel: the div row lost tap-to-focus on an empty position. The position name is now a <label htmlFor> pointed at the name box only.
  • One owner for the opening-tap guard: useOpenedTapGuard replaces four hand-written copies, and the overlay score editors use it on their backdrop.
  • data-tone: the row notice tells an error from a warning.

Review loop after the lineup rule

Five review rounds, each scoped to what the previous round changed; every finding was checked against the code before it was fixed. Rounds 4 and 5 found no major problem.

Round 1 (79573d23, 803ce8a9, fb7c0873, 2a55ea84, 34b8604a):

  • The participant Edit dialog, the public match card and the sign-in dialog took the opening tap's bounce (the sign-in one as a submit of an empty password).
  • The lineup rule also matched a lineup stored under a team's name: it now reads the participant id only.
  • Discarding a draw kept the lineups saved for its matches, and a regenerated draw reuses their ids: they are now removed with the draw.
  • An unreadable pool or bracket file failed every lineup read: the rule now logs it and uses the matches that loaded. Reads use a no-copy projection of the cached draw.
  • After a failed read, both editors showed an empty form with Save on: Save now needs a read, and Try again re-reads.
  • "Use the previous match's lineup" opened its confirm under the panel: dialogs now stack above every overlay.
  • Removing a lineup is a bounded request, says plainly when the server did not answer, and waits while a save of that lineup is still queued.
  • The team sheet composed each name write on the copy it read at open, where a failed read looked like "nothing saved": it now re-reads before writing, refuses a side it never read, keeps a queued save's edit and follows changes made elsewhere.
  • A read the server never answered showed "Failed to fetch": it now says so in plain words.

Round 2 (66b860dd):

  • A name picked before the side's first read had landed was refused with "Close and reopen the match": the pick now reads the side, a side never read is read again when the connection returns, and a write counts as the side's latest read so an older read cannot put the old lineup back.
  • Both editors now follow a lineup change made elsewhere while they hold no unsaved change.
  • A disabled name box now looks disabled.
  • "Not restored, the lineup changed since" no longer goes on the first edit; it goes once the lineup is saved or given up.

Round 3 (933cc9f9):

  • Major: a lineup the editors followed could name a member created on another device after they opened. The panel's next Save looked that position up by name against its old member list and created a duplicate member, and the Lineups page showed the position as empty. The editors now read the members again with a followed lineup, and a Save never looks up a position the operator did not change.
  • Found in the browser, operator decision 2026-10-05: a Save put back positions another device had changed meanwhile. Both editors now save only the positions the operator changed, on the lineup read again at Save (on the lineup as loaded when that read fails, so an offline save still queues).
  • The two lineup reads had no deadline: they now have the 12-second one, and an answer whose body cannot be read is a failed read, never an empty lineup.
  • " is already at ." has one owner, and both editors refuse a lineup that would field one member twice.
  • Two comments said something the code does not do; both corrected.

Round 4 (4d4e3cb5), no major finding:

  • The refusal for a lineup that would field one member twice could name the operator's own position, and on the Lineups page the position another device had filled still showed its old name, so the refusal could not be resolved without reopening the lineup. It now names the position the operator did not change, and a Save shows what it read on the positions the operator left alone (their own changes kept), so the conflict is on screen and can be fixed there.
  • While Save waited for its re-read, Rename, Clear name, an open add row and Discard stayed usable, and a rename made then was written over with the old spelling. They are off while a save is out, and Save waits for a rename, a clear or an add.
  • The team's members list could be replaced by an older read, or left empty when a re-read was discarded because the operator was editing (and in the panel every typed name would then be added as a new member). The list shown is now always the newest read that answered, and it is read again when a Save shows a change made on another device.
  • The panel could rename a blank member and then refuse the save as a duplicate: the duplicate check also runs before any name is looked up.
  • Comments that said more than the code does were corrected, and new tests pin the member ids the name lookup is given and a picked, unnamed slot the operator left alone.

Round 5 (4d4e3cb5, major problems only): none found. Its three minor observations were then fixed (below).

Gaps closed after the loop (5b1962c9, 87ce8801)

  • Major, found in a browser pass of the surfaces only render tests had covered: in the Scores overlay a team of six or more uses the roomy layout, which hid the body's overflow and scrolled the bout list on its own in the height left over. With the pinned bar, a knockout's tie-breaker panel and the rows below the bouts, that was 8 px: no bout could be seen or scored. The bouts now flow in the overlay's scrolling body under the pinned bar, as a five-person team's do. Stylesheet tests pin it.
  • In every overlay a bout scrolled past showed in a strip above the team names (the body's top padding): the pinned bar now covers it. It does so whether or not the note for a match whose stored data cannot be read comes before it (that case was closed after the operator's ruling, below).
  • When the operator changed both positions of a pair that would field one member twice, the refusal could name the box they had typed into: it now names the position where they picked the member.
  • The panel's check before any name is looked up counted the blank slot a typed name sat on, so a valid save could be refused; it now counts the member the lookup will place there (the existing member with that name first), so it also refuses a duplicate of a member who already exists before anything is written. One new name typed into two positions is now refused before anything is written too (below).
  • A list of the team's members that arrives is merged with the one shown by member id, so a member just added is never dropped from the pickers by a read that began before the add.
  • A lineup change announced during an editor's read of the lineup now waits for that read and is followed once the lineup is shown, instead of replacing it.
  • The Competition > Bracket inline sheet and kachinuki bout mode were checked in a browser as well, with no change needed.
  • source-map-js was bumped to 1.2.2 in both lockfiles: GHSA-68fv-2mgg-jv7q (published 2026-09-18, updated 2026-10-05) now fails the security scan on every branch, main included. The commit message calls it "published today", which is wrong: it was updated that day.
  • A review of 5b1962c9 for major problems found none. Its point that the at-court panel kept a side's editor when another device re-seated that side (a save still out for the old team then landed in the new team's form, members included) is fixed in 87ce8801: each side's team is part of its editor's key, as the Lineups page already keys its editor.

After the loop: operator rulings of 2026-10-06

  • The overlay score editors keep guarding only their backdrop (ruling: leave it as it is). Measured first: at 1180x820 under touch emulation, a double tap on a running match's row on the Pools tab opened the editor, and the second tap scored Shiro's M, which the autosave stored (screenshot); on a finished match it opened the Kiken – Injury panel (nothing saved). The Scores tab is not affected at that size, since its Score buttons sit outside the editor. Recorded in CLAUDE.md as accepted.
  • Old round lineups are migrated (ruling: migrate old content to the new format), as the Summary describes. In dfdf0526, checked on data the real v2.1.1 release wrote (Test plan).
  • The three leftovers the loop had accepted are fixed (314c3ea3): a name typed at two positions, a rename undone by an older read of the members, and the strip with the unreadable-data note.
  • Two devices changing the same position of the same lineup at once: the later Save wins, as the save-only-changes decision implies; recorded in CLAUDE.md.
  • Second round, also 2026-10-06: the score sheet's own members race is fixed here (d8ca9103, 280eaf88); old round lineups in round-robin pools stay as built (the operator's answer, "all team matches need lineups", which either rule meets; recorded in CLAUDE.md); a discarded draw removes moved lineups like any match lineup ("the data is migrated"); PR test(bc-e2eh, mp-yqxn.2): e2e harness and a clumsy-operator review of knockout and mixed #442's journeys are fixed on test(bc-e2eh, mp-yqxn.2): e2e harness and a clumsy-operator review of knockout and mixed #442's own branch (31a1c66e, handoff on bc-e2eh). Both lineup answers are superseded by the rebuilt conversion (review below): every match, round-robin pools included, now shows what v2.1.1 showed, and a discarded draw's new matches are given the converted lineups again.
  • Third round, 2026-10-06, chosen from screenshots of each outcome: the team sheet's "Tap a scored mark to clear it" line holds its space ("Hold its space"), and Undo encho keeps a place beside Encho ("Keep Encho in place"). Measured before on the build of 51e32fb9: M dropped 28.5 px at a bout's first point and a quick second tap on M opened the fighter list (9 of 9 tries), and a double tap on Encho landed on Undo encho and cancelled itself (8 of 8). After, on the real build: 0 px, one M and no list (11 of 11), and overtime with two periods stored (8 of 8).
  • Found by running PR test(bc-e2eh, mp-yqxn.2): e2e harness and a clumsy-operator review of knockout and mixed #442's journeys against this PR: End match on a reopened kachinuki encounter, with nothing else changed, sent changed: [], and the match stayed running with no error. useChangedGroups (on main since 2026-10-03) judged End against the finished result it agreed with when the editor opened; it now agrees with the reopened result once the match turns reopenPending, and that group alone (329000e5). Two render tests: End after a reopen names the result, seen failing against the code before the fix; and a held edit when the match turns reopened still names no other group, which passes on the old code too and fails if a reopen re-seeds every group (checked by making that change). The commit message says both were seen failing first, which is true of the first only. Also found: the panel named a position "1" where the sheet and the Lineups page said "Position 1", and a refusal stayed up after the operator had fixed the lineup; both closed in 51e32fb9.

Review of 87ce8801..51e32fb9, and every gap found while closing it (10386bff, 63ee0db9, bea7769d, 40b1af57)

The review measured the old-lineup move against v2.1.1, the last release whose data is in the field:

  • Major: a round lineup "shadowed" by one match's own lineup was deleted, though v2.1.1 still read it at the team's later matches (a Round 2 lineup and a semi-final-only lineup: the final carried the semi-final's lineup, and the Kachinuki Detail export would have labelled it from that).
  • Major: v2.1.1's own pools of four and leagues of five are not played in round order, so moving a round lineup to the team's first match at that round or later changed what several matches showed and deleted round lineups still in use.
  • Minor: a roster without participant ids had the marker set before its name-keyed lineups were converted.
  • Minor: a second member change while the editor's opening members read was out ended that read, so the rest of the team could stay hidden.
  • Minor (regression): on the public sheet, a side given another team merged the old team's members into the new team's list.
  • Untested: each of the above, and the load repair's copy of the marker.

All closed. The conversion was rebuilt on v2.1.1's own reading (Summary) and checked against an independent v2.1.1 oracle on real v2.1.1 draws, and again in the browser on data the v2.1.1 release itself wrote. Gaps found while closing them, each fixed with a test that failed first:

  • The rebuilt conversion gave a lineup back at the next write after the operator removed it: each team and match is now settled once (round_lineups_given).
  • It seeded a team's starting lineup with its lowest round; v2.1.1 showed its highest before the first saved round, so a match seated later read differently. It is now v2.1.1's reading at round 0.
  • It removed the round lineups once every match was seated, so a correction could then seat a team whose lineup was gone; they now stay until the competition is completed.
  • The record was first written with an internal key holding a NUL byte; config.md now lists match ids under each team's id.
  • A name typed before the team's members were read minted a member or lost its member id (all three editors); a wait that timed out held every later name for the full deadline; a second member change could still show an old spelling when the last read failed.
  • The team sheet: a side given another team during a pick could write the new team's member into the old team's lineup; it kept showing the old team's lineup; a side that carried only its team's name read as given another team when the roster resolved it; and the admin sheet never read the members again after it opened.
  • Found in the browser: the clear-mark line and Undo encho moved the controls under the finger (operator rulings above); Escape on an open fighter list closed the whole Scores-page editor; the court console's own confirms ignored Escape and dropped focus to the page; the court picker took focus as it mounted; and a control scrolled only just into view could sit under the pinned footer (found by PR test(bc-e2eh, mp-yqxn.2): e2e harness and a clumsy-operator review of knockout and mixed #442's kachinuki journey, whose double tap on × Remove this bout landed on the footer). The first fix, scroll padding by the bars' heights, made focusing a bar's own button scroll the page (measured about 300 px per Tab through the dock, and on a dialog's focus return); it was replaced by scroll margin on the sheet's own content, measured clear of the bars with no page movement from the bars' own controls.

Code review of 40b1af57 (2026-10-07): ec422925, 2f50e84f, 2db4bb20, 5000ebca

The review verified 15 defects and listed 26 smaller findings; each was fixed, put to the operator, or weighed here.

  • Fixed without a decision (ec422925; every fix red first): the list under "Code review of 40b1af57" in the Summary. Notable: a Setup roster save that minted the team ids deleted the lineups stored under their names, because the orphan prune ran before the re-key; a retried draw kept the failed attempt's converted lineups (Store.ClearDrawLineups now runs before every draw's first write, as at a discard); state.TeamMatches is now the one list of a draw's team matches for the rule and the conversion alike, so a bye is never given a lineup.
  • Put to the operator (answered 2026-10-07, each shown with its outcome first):
    • Lineup saves were stored whole, so a save made offline could put back another device's change to a different position when it was sent, and a queued save could be overwritten by a later save of the same lineup. Answer: "Only changed positions" (2f50e84f).
    • A team that had a v2.1.1 match lineup and no round lineup would have carried that lineup to its later matches, fought ones included. Answer: "Show what v2.1.1 showed" (2f50e84f).
    • Team members had no server stamp, so the client kept its own rules for ordering member lists. Answer: "Do it in this PR" (2f50e84f).
    • Found in the browser while verifying: a lineup save held on the device showed the success check beside "not saved yet". Answer: "Held-write words, pending icon" (5000ebca).
  • Weighed while building them:
    • A Save makes no read of the team's members. A member another device adds is now announced, and the editors read the members again on it. If this device missed that event, a name typed for the new member resolves against the stale list, the server refuses a second member of that name, and the position saves by name with the identity warning. A bounded read at every Save would have added a second wait on a link that does not answer, for a rare race that ends visibly.
    • The team sheet still refuses a pick on a side whose lineup it has never read. Its first reason (a write composed on nothing replaced the lineup) went with changed, but a lineup never read would show positions empty that may not be, and an operator filling them would write over fighters they cannot see.
    • A first settlement that finds no v2.1.1 team while a lineup still waits for its team's id records nothing, so the next pass settles first again; a match lineup entered in between would read as one v2.1.1 saved. Only a roster where some teams still lack an id reaches it.
  • Found while verifying this round, and fixed (2db4bb20):
    • PR test(bc-e2eh, mp-yqxn.2): e2e harness and a clumsy-operator review of knockout and mixed #442's journeys: the at-court lineup panel, made a dialog this round, swallowed every tap for 0.4 s after it opened, so a Close tapped at once did nothing (the team and kachinuki journeys both stopped there). It now guards only its backdrop, as the overlay score editors do under the 2026-10-06 ruling, so its controls work at once; the role, Escape and focus stay.
    • In the browser: an unnamed member's rename box on the Lineups page was labelled "Rename ", naming nothing to a screen reader; it is now labelled with the member's slot.

Weighed and not changed

  • InForceLineup.Source repeats the stored entry's MatchID/Round: kept. The read answer spells the source out (sourceMatchId/sourceRound) because, when nothing is saved, the embedded lineup's own matchId echoes the match asked about.
  • One endpoint for both sides of a match: not added. The sheet reads both sides at once, every other surface reads one team, and one read per team keeps one answer shape.
  • --topbar-stack-h is published on every admin page: kept. It costs one ResizeObserver on the topbar, which stays the one owner of its own height.
  • A shared golden table for the JS order of the Lineup for choices: not added. The client only lists the matches there; which lineup a match carries, and the "Same as" label, come from the server.
  • Match order uses the pool-match number, not the scheduled time: by operator ruling, a pool bout's number is its place in the playing order.
  • The overlay score editors guard only their backdrop, so their controls work at once after opening: kept by the operator's ruling above.
  • Capture and bubble listeners on the same backdrop: WebKit (since r236002, 2018) and Chromium 89 run a target's capture listeners first, so the guard runs before the dismiss.
  • lineup_draft.jsx keeps its own sessionStorage helper: importing viewer_watchlist_core's would pull a viewer module into a leaf.
  • The editors and the sheet keep their own deadline around the lineup re-read although the read is now bounded in the API client, so the bound holds whatever API object they are handed.

Test plan

  • make go/test passes (lint + security scan + tests) on 5000ebca: Go lint 0 issues, gosec 0 issues, npm audit passed, every Go package ok (domain 90.6%, state 89.9%, engine 90.9%, mobileapp 90.2% coverage), JS unit 328 files and 6457 tests, plus the web/ suite (4 files, 76 tests). The render suite (npm run test:render, not in the gate) on the same code: 135 files, 1621 tests.
  • New/updated unit tests cover the change. Each new test was seen failing, against the code before its fix or, for a test that pins behaviour the code already had, against a deliberate break of the code it pins.
  • Manual browser verification at 1180x820 with a coarse pointer (CDP touch emulation), court console /admin/shiaijo/A:
    • tmfd: scrolled through the bouts: header and band pinned exactly at the topbar's bottom (65px). Finish stayed inside the viewport. No ancestor of the panel is a scroll container. A 48px element added to the topbar moved the header to 110px and back. Scores overlay: the header stays pinned at the top of the scrolling body, and the footer stays pinned. Fine pointer: bars still pin, the name bar stays 36px, and the header still lines up with the bout columns. Also checked in iPad Air portrait (820x1180): both bars pin and nothing scrolls sideways.
    • tmfd: the name list opens upward from a row just above the dock and downward from a row just under the header, clear of both bars; with a list open, the topbar and the pinned header stay on top (hit tests land on them).
    • lnrf: typed a fighter already placed at Senpo into bout 2: "Alice Tanaka is already at Senpo." appears in bout 2's Shiro side, in view, role=alert, and the box stays empty.
    • cfbd: a 30ms double tap on "Send back to queue": the confirm stays open; a tap after 600ms dismisses it, and the match stays running. A 30ms double tap on a Scores-tab Score button leaves the editor open.
    • tp44: name input 44px (empty and filled); Rename 48x44, and a tap on its position name no longer opens it. The encho label, both steppers and the kachinuki caret measured 44px inside the real editors (opened through the docs harness's own seed and steps, then measured under touch emulation).
    • lpfb: Pool D · Match 1 shows the starting lineup (was the Round 2 lineup). Lineups page: Lineup for lists Starting lineup and the team's matches; Save is disabled until a change; saving Match 1 makes Match 2 read "Same as Pool D · Match 1" with the new pick, and Match 2's score sheet agrees. Use the previous match's lineup confirms on top of the panel, then both matches return to the starting lineup.
    • lnul: typed a name in the panel without saving, reloaded, reopened: "Unsaved lineup changes restored" (role=status) with the name back; Discard measures 64x44. The Lineups page offers the same draft for that match; Discard puts the saved lineup back and removes the draft.
    • Review, team sheet: with every lineup read held for 6 s, a name typed 70 ms after the sheet opened waited for its own read and saved (one PUT at 6.2 s), with no refusal; all 10 name boxes were disabled meanwhile (opacity 0.6, muted surface, not-allowed cursor). With the reads failing, a pick was refused in bout 1's Aka box (role=alert) and no PUT was sent; the next pick, once the reads were let through, read the side and saved.
    • Review, editors: a panel whose read failed shows "The lineup could not be read: the server did not answer. Check the connection and try again." with Try again, its boxes and Save off. An untouched panel showed a lineup saved by another device; a panel with an unsaved edit kept the edit and read nothing.
    • Review, save only changes: before the fix, an operator who typed Jiho and saved put "Other Device" back over Senpo "Second Change" saved by another device. After it, an operator who typed Chuken while another device changed Senpo saved, and the stored lineup holds Senpo "Second Change", Jiho "Typed Here" and Chuken "Operator Pick".
    • Review, conflict: on the Lineups page (Pool D · Match 1), the operator picked T11.1 at Fukusho while another device saved T11.1 at Senpo. Save was refused with "Other Device is already at Senpo.", nothing was written, and the form then showed T11.1 at Senpo. After moving Fukusho to T11.4, Save went through: the stored lineup holds the other device's Senpo, the operator's Fukusho, and Jiho and Chuken as they were.
    • Competition > Bracket inline sheet, team of six: opened M1 from the bracket and started it; scrolled 700 px, the header and band sat exactly under the 100 px topbar, nothing scrolled sideways, and a name typed into bout 4 saved with one PUT and no notice.
    • Scores overlay, team of six: before the fix the bout list measured 8 px under the band (screenshot above); after it (rebuilt binary) the six bouts measure 1721 px in a body that scrolls, and scrolled 900 px the pinned bar sits on the body's top edge with nothing showing above it. The compact overlay, with a running five-person team match scrolled to the bottom, keeps its bar on the body's top edge with nothing showing above it.
    • Kachinuki bout mode, court console: the team header pinned under the 65 px topbar while scrolling, Record bout and End match stayed in the panel's dock, and the first row's name boxes showed the lineup's Senpo pair.
    • Refusal wording: in the at-court panel, Noda picked from the list at 1 and typed at 3: Save was refused with "Noda is already at 1." and nothing was sent (no rename, no new member, no lineup write).
    • Old round lineups, on data the real v2.1.1 release wrote: built v2.1.1 and, through its own pages, saved round lineups for four teams of a team knockout (Team A Rounds 1 and 2 plus a semi-final-only lineup, C Round 2, E Round 3, G Round 2; two quarter-finals scored) and for two teams of a mixed competition's pool of four (A Rounds 1 to 3, C Round 2), recording what v2.1.1's panel and sheet showed at every match. Then started this build on copies of that data:
      • every match of those teams showed, on the lineup panel and the score sheet, exactly what v2.1.1's score sheet showed (the panel reads Lineup for this match where v2.1.1's was blank);
      • the starting lineups: A its Round 1, and C, E and G their only round's;
      • A winning its semi-final seated it in the final with its Round 2 lineup, not the semi-final-only one, in that write; E and G seated later got theirs the same way;
      • Use the previous match's lineup on C's semi-final and E's quarter-final, then a score written elsewhere, a reload and a restart: both still carried ("Same as Match 2", "Starting lineup"); the same on A's final, whose carried lineup (the semi-final's) differs from the converted one;
      • the pool: A showed its Round 2, Round 1 and Round 3 lineups in its playing order, as v2.1.1 did, and C its Round 2 at all three;
      • config.md listed the settled matches under each team's id; the round lineups stayed until the competition was completed, and the next start after completion removed them.
    • Clear-mark line and Encho, real build (court console, touch taps): the line is in the page and hidden before the first point; one tap on M moves nothing (0 px), on bout 1 and, after Record bout, on bout 2; 11 double taps on M recorded one M each and opened no list; a fixed-order team match moves no row at its first point. On a tied kachinuki bout Undo encho's place is held; one tap on Encho moves nothing and fills it; 8 double taps on Encho gave overtime with two periods stored, and two Undo taps returned the bout to its tie each time.
    • The team's members, real build: a rename made on the Lineups page in another tab showed in the open sheet's list without reopening; with the members read held 3 seconds, an existing member's name typed at once was placed by its id and a new name renamed its position's seeded slot, with no member added; a correction in another tab that seated another team in the final made the open sheet show the new team's lineup and members, and nothing of the old team.
    • Escape on an open fighter list: found in the browser closing the whole Scores-page editor; the new render tests fail on the old code and pass now, as do the 13 existing test files that press Escape. In the browser on the final build: on the Scores page, with a fighter list open, the first Escape closed the list and left the editor open, and the second closed the editor (or asked to discard an unsaved change first); on the court console the first Escape closed the list only.
    • Pinned bars and a control scrolled into view, real build: on the court console, "× Remove this bout" arranged under the footer dock (the dock on top at its centre) landed clear of it by Tab, Shift+Tab and Playwright's scrollIntoViewIfNeeded, the button on top at its centre each time; Tab through the dock's buttons, focusing the topbar's buttons, and a dialog returning focus to a dock button left the page where it was. In the Scores overlay, a name box under the pinned header landed clear of it the same three ways. The earlier fixes still hold (no row moves at the first point; Encho's place held).
    • The court console's confirms and court pickers, real build: "Send back to queue?" opened by Enter took focus (its Cancel), Escape closed it with the match still running and focus back on the opener, with the page where it was; by touch, Cancel closed it with focus on the opener. While its request was held 3 seconds, Escape left it open on "Sending…", and it closed when the answer came, the match back in the queue. The court move confirm, from the keyboard and by touch, gave focus back to the court control. A freshly loaded console focuses nothing (no focus ring on any court control), and after a send-back the page moved only by the height it lost.
    • The strip with the unreadable-data note, in the app: the note put before the bar in a compact (team of three) and a roomy (team of six) overlay. Scrolled, the bar sits on the body's top edge and every probe in the strip hits the bar; with the old rule re-applied in the page, the bar sits 6 px / 18 px lower and a bout shows in the strip. Unscrolled, the bar's box stops 8 px / 6 px short of the note.
    • A name typed at two positions: in the at-court panel, Kudo at 1 and 3 and Save: "Kudo is already at 1.", and no request other than reads was sent.
    • A rename and an older read of the members: with the members reads held in the browser, reads begun before a rename answered after it with the old name; the name box kept the new one, and a Save typing the new name at another position was refused ("Onodera is already at 1.") with no second member. The panel has no member list that can be inspected, so this run cannot show which read supplied the list; the unit and render tests do, and fail without the fix.
    • Team score sheet, a members read answering after a name it wrote: with the sheet's second read of the members held, Ito typed for bout 1 named the blank slot; the held read then answered with that slot unnamed, and Ito typed again for bout 2 was refused ("Ito is already at Position 1."), with nothing written; the server holds one Ito.
    • Two devices, one offline (the operator's approved case), real build: on the Lineups page (Team D, starting lineup), device 1 went offline and saved Senpo T6.1 ("Offline: lineup not saved yet, will retry"); device 2, online, saved Taisho T6.2; device 1 came back online. Device 2's reload shows Senpo T6.1 and Taisho T6.2, and the server's read holds both positions. (That the replayed save names only Senpo is pinned by lineup_save_queue.test.jsx against a server that follows the contract.)
    • A save onto a carried lineup, real build: Team E's Match 2 carried its starting lineup; picking T3.1 at Senpo sent {"positions":{"senpo":""},"memberIds":{"senpo":"…"},"changed":["senpo"]}, and the answer kept the four carried positions, so the server composed it on the lineup in force.
    • v2.1.1 match lineups, on data the real v2.1.1 release wrote: built v2.1.1, drew a mixed team competition through its API and saved, as v2.1.1's own endpoints do: Team A a starting lineup and a lineup for its first match, Team B a lineup for its first match only, Team C a Round 1 lineup, Team E a starting lineup only. On the new build:
      • config.md lists A, B and C under round_lineups_legacy; E is not one;
      • Team A's second match shows its starting lineup as that match's own (v2.1.1's reading), its first match its own;
      • Team B's second match shows an empty lineup of its own ("Lineup for this match", every position "none", with Use the previous match's lineup);
      • Team C's two matches and its new starting lineup show its Round 1 lineup;
      • a lineup entered for Team E's Match 2 carries to Match 3 ("Same as Pool A · Match 2"), and still does after a restart.
    • Self-run public page (no password): a participant's score editor opened from the match card sits on top (a hit test at its centre lands in it); an announcement posted meanwhile stays under the editor's backdrop (900 under 1000), and a hit test where they overlap lands in the editor.
    • Members carry a stamp, real build: with a team score sheet open on device 1, device 2 renamed Team C's T2.1 "Kaito Ono" on the Lineups page; the sheet's list showed "T2.1 Kaito Ono" without reopening. On disk the member gained modifiedAt; members no write touched carry none.
    • A lineup save held on the device, real build: on the Lineups page, offline, Save showed "Not sent yet: saved on this device, and sent when the connection returns." with an hourglass (toast toast--pending, role=status, aria-live=polite, no dismiss); back online, the save landed (the starting lineup read back with the member at position 1).
    • PR test(bc-e2eh, mp-yqxn.2): e2e harness and a clumsy-operator review of knockout and mixed #442's journeys against this PR's head (5000ebca): smoke, individual, team and kachinuki all pass (18 passed, 17 test.fixme skipped), as against 2db4bb20. On this round's first run (the tree of 2f50e84f plus the rename label) the team and kachinuki journeys stopped where they close the lineup panel: the panel, made a dialog this round, swallowed every tap for 0.4 s after it opened, so their Close did nothing; fixed in 2db4bb20 (Code review above). Earlier: against 40b1af57 the same 18 passed; against 10386bff the kachinuki journey failed one step (the held clear-mark line moved "× Remove this bout" under the pinned footer), fixed in 63ee0db9; against 51e32fb9 J5 "M1 reopened while M2 holds shiaijo A" passed, which failed before 329000e5.
  • Screenshots added above
  • Docs updated under docs/ (make docs/build and make docs/prose pass on 2db4bb20: 30 files, no prose-rule violations; docs/check_links.py: 39 pages, 2437 internal links and anchors resolve).
  • Docs screenshots recaptured: make docs/screenshots SINCE=main on 10386bff: 29 unchanged, 2 changed (kachinuki-scoring-buttons, the held clear-mark line; kachinuki-knockout-tie-encho, Undo encho's held place, which in that 500 px overlay puts End match on a second row from the start rather than when Undo encho appears), reviewed and copied; the kachinuki-demo video re-recorded for the same two changes (frames checked; its chapter marks still round to 0:02, 0:08, 0:15 and 0:21). Re-run on 63ee0db9: 31 unchanged, 0 changed. Earlier runs recaptured the 4 kachinuki captures and team-lineup.
  • No new console errors or warnings (the only console errors are the requests the checks aborted on purpose, and ERR_CONNECTION_REFUSED while the server restarted for a rebuild)

Operator rulings recorded in this PR (beads and CLAUDE.md): the lineup rule ("by default, a team carries the previous team match lineup. You can have a different team lineup in every team match"), the draft for unsaved lineups (option A), the two pinned bars (option B), a lineup Save that writes only the positions the operator changed (option A), the overlay score editors' backdrop-only guard kept, old round lineups migrated, the score sheet's member writes standing over older lists, the clear-mark line holding its space, and Undo encho keeping its place beside Encho (all 2026-10-06); a lineup save that names the positions it changed ("Only changed positions"), the teams that had a v2.1.1 match lineup converted ("Show what v2.1.1 showed"), team members carrying a server stamp ("Do it in this PR"), and a held lineup save worded as the score editors' held write with a pending icon ("Held-write words, pending icon") (all 2026-10-07).

Known limits, accepted: a mixed competition's round-robin pools and its knockout shared v2.1.1's round numbers, so a knockout match reads a round lineup by its bracket round index, exactly as v2.1.1 did. A team seated in a match after its competition was completed gets no converted lineup there and carries its previous match's (the correction doors refuse a re-seat whose later match was played). After a competition is completed, its round lineups are removed by the next write of the draw or the next start, not at the moment of completion; nothing reads them meanwhile. A legacy match side with no participant id does not carry a lineup. Two devices changing the same position of the same lineup: the later arrival wins (different positions both keep theirs). A Save makes no read of the team's members: a name typed for a member another device added a moment before, on a device that missed the announcement, is refused as a second member of that name and saved by name with the identity warning. A first settlement that finds no v2.1.1 team while a lineup still waits for its team's id records nothing, so the next pass settles first again (only a roster where some teams still lack an id reaches it).

Closes bc-cfbd
Closes bc-tp44
Closes bc-lnrf
Closes bc-tmfd
Closes bc-lpfb
Closes bc-lnul

🤖 Generated with Claude Code

gitrgoliveira and others added 7 commits October 5, 2026 14:54
A double tap's second click landed on the freshly rendered dialog layer:
on the backdrop or Cancel it dismissed the confirm, on Confirm it
confirmed it unasked. DialogHost, Modal and the court console's two
hand-rolled confirms now swallow pointer clicks for TAP_BOUNCE_MS after
they open (tap_guard.jsx); keyboard activation is never swallowed.

Closes bc-cfbd

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Under a coarse pointer the encho label and steppers, the team sheet's
name box, the lineup panel's Rename link and the kachinuki collapse
caret now reach the 44px tap floor through --tap-floor in classes.
The Rename row is a div, so a tap on the position name no longer
activates Rename, and only the caret glyph rotates.

Closes bc-tp44

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A name typed into a bout row's box could be refused, saved with an
identity warning, or fail to save, and all three messages rendered
once for the whole sheet, below every bout and the IV/PW band: on an
iPad the typed name just vanished. The message now renders in that
row and side, red with role=alert when nothing was written and amber
with role=status when the save landed.

Closes bc-lnrf

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On an iPad the IV/PW band rendered after every bout and the footer
actions sat below the fold. The team header and the result band now
form one sticky bar under the admin topbar, whose height the shell
publishes as --topbar-stack-h, and the footer actions are a sticky dock
at the bottom edge (operator decision: two pinned bars). The name list
opens toward the larger room between the two bars and caps its height.

Closes bc-tmfd

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The bar's floor counts its 1px borders, so an empty name box's input
measured 42px on an iPad. The input now carries the floor and the bar
grows by its borders; fine pointers are unchanged.

Refs bc-tp44

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The result band now sits under the team names, and the pinned header
adds a little space before the first bout.

Refs bc-tmfd

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 5, 2026 14:25

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

gitrgoliveira and others added 2 commits October 5, 2026 15:48
Review fixes for the pinned bars:
- An open name list scrolled with the page painted over the pinned
  header and the admin topbar (the editor body is no stacking context,
  so its z-index 60 beat the topbar's 30); inside a score editor it now
  sits under the bars.
- The footer dock sits under the topbar, so the topbar's held-writes
  panel shows above it, and its action row stays visible when a tall
  prompt or banner makes the dock scroll.
- Older Safari without overflow: clip still pins the bars.
- The list measures against the overlay's footer and the iPad's visual
  viewport (the on-screen keyboard), re-measures while it is open, and
  takes its height cap from one place.
- Tapping an empty position's name in the lineup panel focuses its box
  again, without pointing at Rename.

Refs bc-tmfd, bc-tp44

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
useOpenedTapGuard replaces the four hand-written copies of "stamp on
open, swallow the bounce on the backdrop", and the overlay score
editors now use it too: the bounce of the tap that opened an editor
landed on its fresh backdrop and closed it. The editors guard their
backdrop only, so a control inside can be used at once. The row notice
carries data-tone so an error is told from a warning.

Refs bc-cfbd, bc-lnrf

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 5, 2026 14:48

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

gitrgoliveira and others added 5 commits October 5, 2026 16:20
…ouched side

The panel read only the match's exact round, so a pool match (or a
knockout match past round 0) showed empty boxes under "Inheriting round
default" while the score sheet showed the names, and a Save on that
empty side wrote an empty override that wiped them. The panel now
resolves through resolveMatchLineup like the sheet, keeps what it
loaded as a baseline, and enables Save only once a position changed
(changedLineupPositions, shared with the lineup draft).

Refs bc-lpfb

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Operator ruling 2026-10-05: "by default, a team carries the previous
team match lineup. You can have a different team lineup in every team
match." One engine rule (engine.LineupInForce, lineup_in_force.go)
decides the lineup a team fields at a match: the match's own, else the
latest lineup the team had before it in match order (an earlier match's,
or a round's Lineups-page lineup at the start of that round), else its
lowest-round Lineups-page lineup. Match order is the pool-match number,
then the knockout by round and position, the 3rd-place match last.

The kachinuki roster and the Kachinuki Detail export ask it, and so do
the score sheet, the at-court panel, the viewer, the court display and
the streaming overlay, through the new public read GET
.../teams/:tid/lineup-in-force/:matchId. Before this, the score sheet
read a pool match's lineup from its draw round (so a pool's first match
showed the Round 2 lineup) while the engine read Round 1 for every pool
match. The panel says where its lineup comes from.

Refs bc-lpfb

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Lineups page addressed lineups by round, which no longer matches
how a team's lineup works (it carries from the team's previous match).
Its Round box is replaced by "Lineup for": the team's starting lineup
(the round-0 entry its first match uses) or any of its team matches, in
match order. A match shows where its lineup comes from, saves only a
change, and "Use the previous match's lineup" removes its own lineup so
it inherits again; the at-court panel offers the same instead of "Copy
from previous match", which carrying over made redundant. A round
lineup an earlier version saved still applies and can be removed.

Removes what the rule left without callers: resolveRoundIndex, the
?fallback=best round read and FindBestLineup/FindBestLineupAny.

Refs bc-lpfb

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Typed or picked lineup positions lived only in component state, so a
reload, the app's Back or closing the panel lost them silently. Both
lineup editors now keep the difference between the lineup as loaded and
as shown in sessionStorage, and offer it back with "Unsaved lineup
changes restored" and Discard when the same lineup opens again; the
at-court panel and the Lineups page share a match's draft. A draft is
never written to the server, and one whose lineup changed meanwhile is
dropped with a notice naming what was not restored (operator decision:
a draft rather than a beforeunload prompt).

The Lineups page also lists a match whose opponent is not decided yet.

Closes bc-lnul

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Refs bc-lpfb

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 5, 2026 17:53

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@gitrgoliveira gitrgoliveira changed the title fix(bc-tmfd): keep the team sheet's totals, actions and confirms usable on an iPad fix: iPad operator fixes; a team's lineup carries between matches Oct 5, 2026
gitrgoliveira and others added 5 commits October 5, 2026 20:49
Three layers a tap opens, each with a backdrop that dismisses it, still
took the bounce of that tap: the participant Edit dialog, the public
match card and the admin sign-in dialog (where the bounce submitted an
empty password). They now use useOpenedTapGuard like the confirms. The
viewer no longer declares a roundIndex on an opened match, which nothing
reads since lineups stopped deriving a round.

Refs bc-cfbd

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Code review of the lineup rule:
- The rule also matched lineups stored under a team's NAME, a name
  fallback beside an id lookup that CLAUDE.md's id-only rule forbids;
  the match surfaces had always matched the exact team id. It now reads
  by the participant id alone, and an id-less side has no lineup.
- Discarding a draw kept the lineups saved for its matches, and a
  regenerated draw reuses the ids, so a leftover became a team's own
  lineup and carried to its later matches. Discard now removes them
  (round lineups, the starting lineup included, stay).
- An unreadable pool or bracket file no longer fails every lineup read:
  the rule logs it and uses the matches that loaded.
- Reads place matches through a no-copy projection of the cached draw
  (state.DrawMatches) instead of deep-cloning every match per request;
  the kachinuki advance and the export build the rule from the matches
  they already loaded; findTeamMatch walks through findMatchHome.
- The export labels a name held at two positions deterministically.

Refs bc-lpfb

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Code review of the two lineup editors:
- After a failed read both editors showed an empty form with Save live
  (on the starting lineup even untouched), so one tap could write an
  empty or partial lineup that every later match then carried. Save now
  needs a successful read, the boxes wait for it, and Try again re-reads.
- The at-court panel's "Use the previous match's lineup" opened its
  confirm UNDER the panel (z-index 100 vs 1000). Dialogs now stack above
  every overlay (.modal-backdrop--dialog), with toasts above them.
- Removing a lineup is a bounded request, says plainly when the server
  did not answer, and is held while a save of the same lineup is still
  waiting to be sent (a queued save would otherwise replay after it).
  A failed re-read after a removal no longer shows the removed lineup.
- One hook (useLineupForm) and one source line (LineupSourceLine) own
  what both editors repeated; the "not restored" notice clears once the
  lineup is saved or changed; the client's unused fetchMatchLineup is
  gone; byes are no longer offered on the Lineups page; the name list
  measures against the box that clips it.

Refs bc-lpfb, bc-lnul

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The score sheet read each side's lineup quietly (a failure looked like
"nothing saved") and composed every name-box write on that copy, so a
failed or stale read wrote a partial lineup as the match's own, which
every later match of the team now carries. It now reads both sides
together, refuses a lineup write for a side it never read, re-reads
just before each write (falling back to the lineup it holds when the
re-read fails, so an offline write still queues), keeps a queued save's
edit, and follows lineup changes made elsewhere. A row's notice has one
key and also shows on a recorded kachinuki bout.

Refs bc-lpfb, bc-lnrf

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A save of a match's lineup queued from the at-court panel or the
  Lineups page is now the base the team sheet's next name write builds
  on (API.queuedLineupSave returns the queued lineup), so that edit is
  not replaced in the outbox.
- A lineup read the server never answered shows "The lineup could not
  be read: the server did not answer. Check the connection and try
  again." instead of the browser's "Failed to fetch".
- CLAUDE.md records the lineup editors' and the sheet's write rules and
  which layers take the opening-tap guard.

Refs bc-lpfb, bc-cfbd

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 5, 2026 20:47

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Code review, round two:
- A name picked on the team sheet before its first lineup read landed
  was refused with "Close and reopen the match". The pick now reads that
  side itself and is refused only when that read fails ("Check the
  connection and try again."), and a side never read is read again when
  the connection returns. A confirmed write counts as the side's latest
  read, so a read still out from before it cannot put the old lineup
  back.
- The at-court panel and the Lineups page follow a lineup change
  announced for the competition while the form is untouched, and leave
  an edited form alone.
- The name boxes look disabled while a name save is out (dimmed, on the
  muted surface, not-allowed cursor); the 44px floors stand.
- "Not restored, the lineup changed since" stays through the operator's
  edits and goes once the lineup is saved or given up.
- check-imports covers admin_shiaijo.jsx; the EnsureLegacyUpgraded
  comment lists DrawMatches.

Refs bc-lpfb, bc-lnul, bc-lnrf

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 5, 2026 21:54
Copilot AI balanced review requested due to automatic review settings October 6, 2026 15:07

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

gitrgoliveira and others added 3 commits October 6, 2026 16:12
…sult

Found by PR #442's kachinuki journey run against this build: Correct on a
finished kachinuki encounter, Reopen match, then End match with nothing
else changed sent the score write with "changed": [], and the server,
told nothing changed, kept the encounter running with no error.

useChangedGroups judges a group against the server value the editor last
agreed with, seeded when it opened and moved only when it builds a write.
A reopen is the server's own write, so the agreed result stayed the
finished one, and End rebuilt that same verdict. Once the match the editor
renders turns reopenPending, the editor now takes the reopened result as
agreed, and that group alone, so every other group keeps its protection
against putting back a value another device changed.

Two render tests, both seen failing first: End after a reopen names the
result; a held edit when the match turns reopened still names no other
group (it fails if a reopen re-seeds every group).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nges

- lineupPositionLabel moves to lineup_resolver.jsx, beside
  alreadyPlacedNote. The at-court panel named a position by its row label
  ("is already at 1.") where the score sheet and the Lineups page said
  "is already at Position 1."; all three now use it, and the sheet drops
  its window lookup and type check.
- Changing a position takes a refusal down on the panel and the Lineups
  page: the refusal was for the lineup as it was, and the next Save judges
  it again. Found by PR #442's journeys (the refusal stayed up with Save
  off).
- CLAUDE.md records both, and the changed-groups rule for a reopen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 6, 2026 16:13

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

gitrgoliveira added a commit that referenced this pull request Oct 6, 2026
…journeys

Run against the build of PR #458's head (51e32fb), not #442's own: all four
journeys pass there (18 passed, 17 test.fixme skipped).

Lineup UI
- bc-lpfb is a plain test: the at-court panel shows the lineup in force with
  its source line, Shiro's Save lineup is off on a side nothing changed on,
  and the score sheet names the same fighters after Start match.
- J3: Shiro's wrong-side Save becomes "Save lineup is disabled". The V1
  neighbour tap now lands on the clear x of Shiro's third name box (the
  nearest control left of Aka's Save), so the step puts that position back
  first and records it.
- The Lineups page's Round box is the "Lineup for" select: openLineups takes
  lineupFor (default "Starting lineup"); a new J3 step opens a match's own
  lineup through it. The unsaved-draft notice is recorded and asserted.
- panelSave and typeLineup no longer wait for Save lineup to be enabled
  again: it stays off until something changes. The saved side is read from
  its source line.

Tap guard (400 ms)
- fixtures/pace.mjs holds the dwell. login, hastyConfirm, answerDialog,
  finishMatch (now local), armFinish, finishTeam, endMatch, awardBoutIppon,
  the shiaijo confirms and the direct two-tap sites wait it out.

Behaviour that moved
- Kachinuki: the final carries its teams' lineups (no Copy from previous
  match); send back to queue keeps the score, so M2 restarts with Aka's M and
  bout 1 needs only the K; a double tap on Encho can land on Undo encho; a
  double tap on the first ippon can open the fighter list; a bounce no longer
  records M M, the mistake is made with a deliberate second tap.
- Individual: no remaining-matches panel; the barred notice reads "record the
  fusensho" and its button is barred-match-record-fusensho.

J5 test 4 found a product defect and now asserts the fix (a freshly loaded
console lists M1 as completed): End match on a reopened encounter with
nothing else changed sent changed:[] and the server left the match running.
Fixed in #458 (329000e, useChangedGroups agrees with the reopened result).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
gitrgoliveira and others added 4 commits October 6, 2026 20:31
…ollow

Old round lineups (the review of 51e32fb found the move-and-drop design
showed different lineups than v2.1.1: a round lineup "shadowed" by one
match's own lineup was deleted though v2.1.1 still read it at the team's
later matches, and v2.1.1's own pools and leagues are not played in round
order):
- Every match a team with a round lineup is seated in gets a lineup of its
  own equal to what v2.1.1 showed there (the match's own, else the highest
  round at or below the match's round, else the highest), on load and in the
  write that seats the team.
- Each team and match is settled once and recorded in config.md's
  round_lineups_given (team id to match ids), so a lineup the operator
  removes stays removed; discarding the draw clears the record.
- A team with no starting lineup gets v2.1.1's reading at round 0 (its
  highest round), so a match it is seated in later reads the same.
- The round lineups stay, never read, until the competition is completed.
- A roster without ids waits for them; the load repair's copy of the
  competition carries the marker and the record.

Member lists:
- A name typed before the team's members are read waits for them, bounded,
  in the at-court panel, the Lineups page and the team sheet; a wait that
  times out ends for good.
- changeMembers keeps only the read made as the editor opened while no list
  is shown, and asks whether a read's list was shown, not whether the list
  is empty.
- The team sheet: a side is given another team only when its key in the
  match and its team both change; it then drops that team's members,
  pending names and lineup, and stops a pick made for it. A side takes a
  list only once its team is known. The admin sheet reads its members again
  on lineup-updated, and a name it wrote gives way to one given elsewhere.

Operator rulings 2026-10-06: the team sheet's "Tap a scored mark to clear
it" line holds its space, and Undo encho holds its place beside Encho
(.holds-space), so neither moves a control under the finger.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e layer

- The team sheet publishes its pinned header's and footer dock's heights
  (published_height.jsx, now also the owner of the topbar's), and its own
  content controls carry them as scroll margin: a control a keyboard Tab or a
  scrollIntoView reaches stops clear of the topbar, the pinned header and the
  dock (inline), or of the pinned header (overlay); the inline head clears the
  topbar. Margin on the content, never padding on the scroller: a bar's own
  buttons lie in a padded strip, and focusing one scrolled the page by about
  300 px (measured). Found by PR #442's kachinuki journey, whose double tap on
  "x Remove this bout" landed on the footer once the held clear-mark line had
  moved that button 28 px lower.
- Escape on an open fighter list closes the list only; the Scores-page score
  editor used to close with it.
- Docs: the two kachinuki captures that show the held spaces, the
  re-recorded kachinuki demo (chapters unchanged), a sentence on Undo
  encho's kept place, and the completion step of the round-lineup
  conversion.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The court move and "Send back to queue?" confirms are the court console's
own dialogs, and from a keyboard they ignored Escape and dropped focus to
the page when they closed (found in the browser pass). They now do what
DialogHost does for confirmDialog: focus moves into the open confirm (its
Cancel), Escape cancels it while its request is not out (the existing
useEscapeToClose), and focus goes back, without scrolling, to the control
that opened it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CourtPicker gives focus back to its button when its list closes, but its
effect also ran on mount, when the picker is closed too: every queue and
score row carries one, so focus jumped to the last picker mounted (with
its focus ring) on load and whenever a row appeared, and the page scrolled
to it (found in the browser on the court console). Focus now goes back
only on a close that follows an open.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 6, 2026 21:16

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Round lineups (server):
- A Setup roster write that mints the team ids re-keys the lineups stored
  under a team's name before the orphan prune, which used to delete them.
- A draw generated from Setup starts from no lineups of an earlier attempt
  (Store.ClearDrawLineups, shared with DiscardDraw), so a retried draw is
  given its own readings.
- A Swiss or structural bye is not a team match: one owner,
  state.TeamMatches, serves the conversion and engine.LineupInForce.
- The member-id repair runs after the round-lineup step, so the copies
  carry member ids.
- A marked competition costs the settlement nothing (cached gate), and
  LineupInForce copies only the lineup it returns.
- The Kachinuki Detail export orders positions as the roster does
  (domain.ComparePositions); Competition.IsTeam replaces this PR's spelling
  and the identical ones; lineup_updated names the team (and the match).

Lineup editors and the team score sheet:
- The at-court panel follows the match it was opened for, so a side given
  another team gets that team's editor; the Lineups page keeps a chosen
  match the team is no longer in and says so, rather than editing the
  starting lineup; the panel is a dialog (tap guard, Escape, focus) through
  the new dialog_focus.jsx, shared with the court console's confirms.
- A queued name-box pick says it is not sent yet; member writes are bounded;
  Escape with no list drawn reaches the editor; an outside tap keeps focus
  where it went; modals stack above announcements; ending a match after any
  reopen names its result; editors re-read members on a change elsewhere
  even while edited, only for their own team; a draft the saved lineup
  already holds is not reported as lost.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 7, 2026 10:35

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Operator decisions of 2026-10-07 on the code review's open items.

"Only changed positions": both lineup PUTs take `changed`, and the server
lands only those positions, under the competition's lock, on the lineup
stored for that key or, for a match with none, on the lineup in force
there (engine.LineupInForceFrom over what the transaction loaded). Two
devices changing different positions both keep their change in either
order, a save sent later from an offline device included. A save without
`changed` still replaces the whole lineup, which is how a save queued by
the previous build replays. lineup_save.jsx builds every save's body and
joins a save into a queued save of the same lineup position by position;
the editors no longer read the lineup again at Save, the score sheet's
name box sends its one position, and the editors adopt the answered
lineup.

"Show what v2.1.1 showed": a team that had a match lineup when the
competition is first settled is a legacy team too, recorded once in
config.md (round_lineups_legacy), so a lineup saved after the upgrade
never makes a team legacy. Each of its matches is given the lineup
v2.1.1 showed there, or an empty one where it showed none. A discard or
a new draw drops those teams, whose match lineups went with the draw.

"Do it in this PR": every member write stamps the member (modifiedAt,
the server's time or one more than the member's last stamp) and answers
with it; PUT and DELETE members answer 200 with the member, and an add is
announced like the other member writes. The client keeps each member's
copy with the larger stamp, through the one mergeMembers, so the pending
names and the read-order counters are gone, and an own change makes no
read.

Also: the member-id load repair orders positions as the roster does; a
flaky config.md test pinned; the Lineups page locks Use the previous
match's lineup for a match the team is no longer in; _deleteLineup
inlined; the API spec, the per-match lineup spec, the data model page,
the team guide and CLAUDE.md describe all of it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 7, 2026 11:54

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

PR #442's journeys found that the at-court lineup panel, made a dialog in
this round, swallowed every tap for the bounce window after it opened, so
a Close tapped at once did nothing. The panel is an editor like the
overlay score editors, so it now guards only its backdrop against the
opening tap, under the same operator ruling (2026-10-06), and its
controls work at once; its role, Escape and focus stay.

On the Lineups page, the rename box of a member with no name yet was
labelled "Rename ", naming nothing to a screen reader; it is labelled
with the member's slot, as the rest of the page names it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 7, 2026 12:23

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Operator decision 2026-10-07 ("Held-write words, pending icon"): when a
lineup save is held on the device, the Lineups page and the at-court
panel showed "Offline: lineup not saved yet, will retry" beside the
success check every toast that is not an error carried. Both now show the
words the score editors already use for a held write (queuedNotice: "Not
sent yet: saved on this device, and sent when the connection returns.",
or "keep this page open" when the browser could not store it), in a
pending toast, which shows an hourglass and is a polite status like a
success. The team guide says the same.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 7, 2026 13:15

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@gitrgoliveira
gitrgoliveira merged commit 37c5881 into main Oct 7, 2026
13 checks passed
@gitrgoliveira
gitrgoliveira deleted the worktree-bridge-cse_018uNoSEX98xtx8fduygp2Qe branch October 7, 2026 14:18
gitrgoliveira added a commit that referenced this pull request Oct 7, 2026
With the fixme removed the test passes on main: since 51e32fb (PR #458,
"Lineup refusals name a position one way everywhere") the at-court lineup
panel refuses a typed duplicate with "Dai is already at Senpo.", composed by
lineup_resolver.jsx for every surface. The bead itself is still open.

The four fixmes left fail at their own defect assertions with the fixme
removed: bc-otpl (no encho checkbox), bc-kosc (a knockout time before the
last pool time), bc-dhrp (no name box on the daihyosen row), bc-crpn (no
Record bout after Start match, inside the serial flow).

make e2e journeys/knockout-mixed-team: 9 passed, 1 skipped, EXIT 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

2 participants