Repository navigation
fix: iPad operator fixes; a team's lineup carries between matches - #458
Merged
gitrgoliveira merged 40 commits intoOct 7, 2026
Merged
Conversation
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>
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>
…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>
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>
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>
…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>
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>
…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>
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>
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>
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>
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>
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>
9 of 10 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
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.--tap-floorin classes. Fine pointers are unchanged. The lineup panel's position row is now adiv: as a<label>with nofor, a tap on the position name activated Rename.role=alertwhen nothing was written, amberrole=statuswhen the save landed), instead of below every bout.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 readGET .../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.AdminTopbarpublishes 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).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 withoutchangedreplaces the whole lineup, which is how a save queued by the previous build replays.config.mdrecords 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.round_lineups_convertedmarker. A new competition starts with it set. The Lineups page's later-round note and Remove it are gone, andPUT .../lineups/:roundrefuses a round above 0.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.40b1af57, and the operator's three decisions on it (2026-10-07):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.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.5000ebca).Why
--tap-floor: an inline style that outranked the.btnfloors, 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.overflow: hiddenmade the band's sticky style inert.Files changed
internal/engine/lineup_in_force.go(new)internal/engine/kachinuki.go,kachinuki_export.gofindTeamMatchthroughfindMatchHome; a name at two positions labelled deterministicallyinternal/engine/competition.goStore.ClearDrawLineups);Competition.IsTeam()replaces this PR's and seven identical spellingsinternal/state/draw_matches.go(new)DrawMatch/DrawMatches: a no-copy projection of the cached draw for the lineup rule; a bye is markedHidden, and a pool or league match carries its stored circle round (PoolRound)internal/state/round_lineups.go(new)settleRoundLineups): v2.1.1's reading (legacyReading, at the roundlegacyLineupRoundgives 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 inround_lineups_legacy; where v2.1.1 showed no lineup the match gets an empty one of its owninternal/domain/team_member.go,internal/state/squad.go,store.go,internal/mobileapp/handlers_squad.goModifiedAt(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 announcedinternal/state/match_order.go,draw_matches.go,internal/engine/lineup_in_force.gostate.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.LineupInForceFromover what a transaction loaded;LineupInForcecopies only the lineup it returnsweb-mobile/js/lineup_save.jsx(new)changedLineupSave, every save's body, andjoinQueuedLineupSave, the queue's join of two saves of one lineupweb-mobile/js/dialog_focus.jsx(new),admin_schedule_score_editor.jsx,admin_shiaijo.jsxuseDialogFocus: 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 renderinternal/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)changedsave, the member stamp and answers, the event payload, the v2.1.1 match-lineup conversionjs/__tests__/lineup_save,lineup_save_queue,helpers/lineup_server,helpers/team_members,render/lineup_panel_hosts,render/lineup_panel_overlay(new)internal/state/match_order.go(new)MatchPlace,DrawMatch.Place/Seats), shared by the round-lineup conversion and the engine rule (swiss.go,match_label.godelegate to it)internal/state/transactions.go,bracket.go,pools.go,models.go,competition.goCompetition.RoundLineupsConvertedandRoundLineupsGiven(team id to match ids, deep-copied)internal/mobileapp/handlers_competition.goweb-mobile/js/admin_scoring_autosave.jsxuseChangedGroupsagrees with the result the match holds once it leaves completed (any reopen, a send back to the queue), so End names itweb-mobile/js/lineup_draft.jsx,admin_scoring_team.jsx(members)mergeMembers(each member's newer copy, by its server stamp) andnewMembersWait;useLineupForm.waitForMembers,changeMembersandmemberRenamed(member), which merge the answered member and read nothing; the sheet'stakeSideMembers/wroteSideMemberson the same merge; reads begun in one tick share one requestweb-mobile/js/admin_lineup.jsx,admin_schedule_lineup.jsx(members)web-mobile/js/lineup_resolver.jsx(lineupPositionLabel)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_changesinternal/state/team_lineup.goClearDrawLineups(one transaction: a draw's match lineups, the given record, the legacy list trimmed);FindBestLineup/FindBestLineupAnyremoved (no caller left)internal/state/legacy_upgrade.goupgradeRoundLineupsLocked, 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 callsEnsureLegacyUpgraded, none of them holding the competition's lockinternal/domain/team_lineup.goApplyChanges(a save's changed positions landed on a base) andComparePositions(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 ruleinternal/mobileapp/handlers_lineup.go,deps.go,server.golineup-in-forceread; the dead?fallback=bestbranch removed; a lineup PUT for a round above 0 is refused (400); both PUTs may name theirchangedpositions, landed under the lock onlineupSave.base(the stored lineup, else the one in force); the change event names the team and the matchweb-mobile/js/tap_guard.jsxuseOpenedTapGuard: the one owner of "a layer takes the bounce of the tap that opened it"web-mobile/js/ui.jsxDialogHostandModaltake that bounce; a dialog's backdrop stacks above every overlay; apendingtoast (hourglass, a polite status) for a write the device holdsweb-mobile/js/admin_shiaijo.jsxweb-mobile/js/admin_participants.jsx,viewer_match.jsx,app.jsxweb-mobile/js/admin_scoring_individual.jsx,admin_scoring_engi.jsxweb-mobile/js/admin_scoring_team.jsx.holds-space); caret rotates its glyph only; row-keyed lineup notice; header and band in.team-sheet-pin;useMatchLineupsreads 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 backdropweb-mobile/js/admin_scoring_shared.jsxLineupNameInputplaces 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 editorweb-mobile/js/admin_shell.jsx,published_height.jsx(new)CourtPickertakes no focus as it mounts, and gives it back only for a close made inside it;AdminTopbarpublishes--topbar-stack-hthroughpublishHeight, the one owner of a published bar height, which the team sheet also uses for--team-pin-hand--team-dock-hweb-mobile/package-lock.json,web/package-lock.jsonsource-map-js1.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-backdropstacks above announcement cards (1000 over 900)web-mobile/js/lineup_draft.jsx(new)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 membersweb-mobile/js/lineup_resolver.jsxresolveMatchLineupreads the lineup in force;changedLineupPositions,alreadyPlacedNote,lineupDuplicateNote, the source labels and read-failure wording;buildInlineLineupWritealso returns its one changed positionweb-mobile/js/admin_lineup.jsxtypedNameTarget, the one order for placing a typed name; an unnamed member's rename box is named by its slotweb-mobile/js/admin_schedule_lineup.jsxweb-mobile/js/lineup_rename.jsxweb-mobile/js/admin_schedule.jsx,admin_competition.jsxweb-mobile/js/api_client.jsxfetchLineupInForce; both lineup reads bounded; a lineup save takeschanged, and a save of a lineup with one queued joins it (joinQueuedLineupSave);queuedLineupSaveanswers whether one is queued; member writes bounded (_memberWrite) and answered with the member; a lineup removal bounded (_deleteLineupfolded intodeleteMatchLineup);fetchMatchLineupanddeleteTeamLineupremovedweb-mobile/js/write_result.jsxwithinDeadline/TIMED_OUTmoved here besideFETCH_TIMEOUT_MSweb-mobile/js/match_scoreboard.jsx,display_scoreboard.jsx,streaming_overlay.jsx,viewer_competition.jsx,viewer_utils.jsxweb-mobile/js/admin_helpers.jsxresolveRoundIndexremoved (no caller left)web-mobile/check-imports.mjsdocs/user-guide/organisers/team-tournaments.md,docs/architecture/data-model.md,specs/openapi.yaml,specs/005-per-match-lineup/spec.md,CLAUDE.mdchanged,TeamLineupSave,modifiedAt, the member answers and the DELETE that was missing from the API spec)docs/user-guide/court-operators/recording-decisions.mddocs/screenshots/kachinuki-*.png(4),team-lineup.pngkachinuki-scoring-buttons) and Undo encho's held place (kachinuki-knockout-tie-encho)docs/videos/kachinuki-demo.webminternal/engine/lineup_in_force_test.go(new),kachinuki_test.go,kachinuki_export_test.go,competition_test.go,load_error_propagation_test.gointernal/state/draw_matches_test.go(new),team_lineup_match_test.go,team_lineup_test.go,legacy_upgrade_lineup_ids_test.gointernal/mobileapp/handlers_lineup_test.golineup-in-forceread: tiers, source fields, unknown competition; the PUT refusal above round 0internal/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)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 readjs/__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_reopenjs/__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.jsxjs/__tests__/operator_tap_floor.test.jsx(new),render/team_editor_config_matrixjs/__tests__/render/team_editor_pinned_bars,admin_topbar_stack_height,lineup_name_input_placement(new)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.jsxjs/__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)js/__tests__/api_client_match_lineup,write_result_deadline(new)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_surfacejs/__tests__/pick_copy_source.test.jsx,render/match_lineup_side_editor_order.render.test.jsx(deleted)resolveRoundIndex: () => 0stub droppedThe 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(eachweb-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.

bc-tmfd: before / after (docs capture, kachinuki correction). Before, the band was below the bouts. After, it is under the team names.
bc-tmfd: Scores overlay. The header and band stay pinned at the top of the scrolling body.

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

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

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

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).

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").

bc-lpfb: Lineups page before / after (docs capture).
bc-lnul: after a reload, the panel offers the unsaved name back.

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

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

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

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.

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

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

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.

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.

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.
A refusal names where the member was picked, not the box their name was typed into (Noda picked at 1, typed at 3).

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".
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.
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.
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.
One new name typed at two positions is refused before anything is written ("No lineup saved yet" stays).

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).

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.

The same lineup read on device 2 after the reconnect: Senpo T6.1 (device 1) and Taisho T6.2 (device 2), both 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.

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):

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.

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.

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.

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.

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 sheetandGuard every tap-opened layer through one tap_guard helper:container-type, which in Chrome creates no stacking context, so the list'sz-index: 60beat the topbar's 30. Inside a score editor the list is nowz-index: 8, under the pinned header (9), and slides under the bars like its input.z-index: 20, under the topbar (30).overflow: clipbefore Safari 16: an@supportsfallback keeps the bars pinned (only the rounded-corner clip is lost).divrow lost tap-to-focus on an empty position. The position name is now a<label htmlFor>pointed at the name box only.useOpenedTapGuardreplaces 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):Round 2 (
66b860dd):Round 3 (
933cc9f9):Round 4 (
4d4e3cb5), no major finding:Round 5 (
4d4e3cb5, major problems only): none found. Its three minor observations were then fixed (below).Gaps closed after the loop (
5b1962c9,87ce8801)source-map-jswas 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.5b1962c9for 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 in87ce8801: 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
dfdf0526, checked on data the real v2.1.1 release wrote (Test plan).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.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.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).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 turnsreopenPending, 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 in51e32fb9.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:
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:
round_lineups_given).config.mdnow lists match ids under each team's id.Code review of
40b1af57(2026-10-07):ec422925,2f50e84f,2db4bb20,5000ebcaThe review verified 15 defects and listed 26 smaller findings; each was fixed, put to the operator, or weighed here.
ec422925; every fix red first): the list under "Code review of40b1af57" 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.ClearDrawLineupsnow runs before every draw's first write, as at a discard);state.TeamMatchesis 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.2f50e84f).2f50e84f).2f50e84f).5000ebca).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.2db4bb20):Weighed and not changed
InForceLineup.Sourcerepeats the stored entry'sMatchID/Round: kept. The read answer spells the source out (sourceMatchId/sourceRound) because, when nothing is saved, the embedded lineup's ownmatchIdechoes the match asked about.--topbar-stack-his published on every admin page: kept. It costs one ResizeObserver on the topbar, which stays the one owner of its own height.lineup_draft.jsxkeeps its own sessionStorage helper: importingviewer_watchlist_core's would pull a viewer module into a leaf.Test plan
make go/testpasses (lint + security scan + tests) on5000ebca: Go lint 0 issues, gosec 0 issues, npm audit passed, every Go packageok(domain 90.6%, state 89.9%, engine 90.9%, mobileapp 90.2% coverage), JS unit 328 files and 6457 tests, plus theweb/suite (4 files, 76 tests). The render suite (npm run test:render, not in the gate) on the same code: 135 files, 1621 tests./admin/shiaijo/A:role=alert, and the box stays empty.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.role=alert) and no PUT was sent; the next pick, once the reads were let through, read the side and saved.config.mdlisted 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.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).lineup_save_queue.test.jsxagainst a server that follows the contract.){"positions":{"senpo":""},"memberIds":{"senpo":"…"},"changed":["senpo"]}, and the answer kept the four carried positions, so the server composed it on the lineup in force.config.mdlists A, B and C underround_lineups_legacy; E is not one;modifiedAt; members no write touched carry none.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).5000ebca): smoke, individual, team and kachinuki all pass (18 passed, 17test.fixmeskipped), as against2db4bb20. On this round's first run (the tree of2f50e84fplus 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 in2db4bb20(Code review above). Earlier: against40b1af57the same 18 passed; against10386bffthe kachinuki journey failed one step (the held clear-mark line moved "× Remove this bout" under the pinned footer), fixed in63ee0db9; against51e32fb9J5 "M1 reopened while M2 holds shiaijo A" passed, which failed before329000e5.docs/(make docs/buildandmake docs/prosepass on2db4bb20: 30 files, no prose-rule violations;docs/check_links.py: 39 pages, 2437 internal links and anchors resolve).make docs/screenshots SINCE=mainon10386bff: 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; thekachinuki-demovideo 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 on63ee0db9: 31 unchanged, 0 changed. Earlier runs recaptured the 4 kachinuki captures andteam-lineup.ERR_CONNECTION_REFUSEDwhile 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