Repository navigation
fix: five operator-review bugs: auto-advance refusal, court-call double tap, daihyosen representative, team winner cue, drawn recent results - #463
Merged
gitrgoliveira merged 40 commits intoOct 10, 2026
Conversation
Route both Finish + Start Next and onAfterDecision auto-advance starts through startMatch() instead of inline try/onEditScore/catch blocks. This ensures the Up next card displays lasting refusal messages (ineligible_competitor, court_busy, clock_skew) that were previously being swallowed silently. - Replace onEditScore call with startMatch call in onSubmitAndNext - Replace onEditScore call with startMatch call in onAfterDecision - Add three new tests: refused start, onAfterDecision refusal, clock_skew refusal - Update docs to mention Up next card displays refusal reasons Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
- Add .shiaijo-qrow__side--win CSS class for winner styling
- Import teamNameMark from match_scoreboard.jsx
- Compute winner side using window.winnerSideLR for completed rows
- Get team marks using window.teamMatchMarks for team matches
- Add bc-winner-tick span before winning side's name
- Add msb-name--labelled class when team marks are present
- Add winnerTick prop to VSchedItem component
- Pass winnerTick={true} to Recent results VSchedItem in viewer_competition.jsx
- Add comprehensive tests for winner display on court and viewer
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Plain Finish does not start the next match; only the court console's Finish + Start Next does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
As a sibling of the name span it took a line of its own above the name (measured at 390x844: tick at y 292, name at y 306, the side 52px tall). Inside the name's flex row it sits at the outer edge, first on Shiro and last on Aka, and the side is 37px tall again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A completed hikiwake has no winner, so the viewer's Recent results filter dropped it while the operator's Completed list showed it. The filter now keys on status and the bye/watched predicates alone. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
…ch starts A double tap on Call to court posted two identical "Now calling" banners, and a call stayed up for its full five minutes after the match had started. - Console: callToCourt takes the tap through acceptTap (tap_guard.jsx), so a bounce of the tap is never sent. Both the Up next card and the queue row pass the event. The callingKey disabled state is unchanged. - Console: the announcement id of each call is kept per match (as a promise, so a start that lands before the answer still withdraws it). startMatch deletes it once the match starts, and a fusensho or both-barred decision recorded from the queue row's notice does the same. A failed delete is logged, never fails the start; the 5-minute expiry backstops it. - Server: AnnouncementStore.Add replaces an active announcement with the identical message (new id, fresh TTL) instead of stacking it, for every announcement, the organiser's Announce modal included. - Docs: run-tournament.md describes the replacement and the court-call withdrawal. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
The daihyosen row of a knockout team match rendered both team names read-only, though the docs and the in-app hint promise a picker. Each side can now pick, type or clear its representative by member id. The id rides the row beside the TEAM names, which placeHt and the wire still need. A typed name goes through the resolver a numbered row uses (mint, or rename a picked blank member), and no lineup is written, since "daihyosen" is not a lineup key. The bout projection names bout:-1 when a representative id is cleared, because the server copies a named row whole. A participant's pick is refused (400 team_member_not_in_team) when it names a member its side's team does not hold; the organiser's pick is not judged. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
A participant's sheet shows the row read-only while the match is not running as well as once the judges decided it, and it shows the picked representative or a dash, never the team name. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The viewer name cell now holds the Recent results tick's slot beside the name, false when no tick is drawn, so its children are an array. The pin is unchanged: a joined string is still the cell's one child and fails. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
console-correct-completed (the court's Completed list names each winner with a tick), viewer-competition (Recent results carry the tick and now list the drawn Pool A bout), viewer-result-encho (the tick). Captured with make docs/screenshots SINCE=main; the other 28 were unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
govulncheck (make go/vuln) failed on main with ten advisories against
go1.27.1's net/http and HTTP/2 and golang.org/x/net v0.58.0, fixed in
go1.27.2 and x/net v0.60.0 (GO-2026-6609 to GO-2026-6617 and others).
Bump the toolchain everywhere it is pinned: go.mod, the three
Dockerfiles' GO_VERSION, Makefile GO_VERSION, actions/setup-go in both
workflows, and the docs prerequisites (plus GEMINI.md and the version
command's sample output).
go1.27.2 writes a newer export-data version than golang.org/x/tools
v0.49 can read, so golangci-lint v2.13.x and gosec v2.29.0 fail to
type-check anything ("export data version 5 is greater than maximum
supported version 4"). golangci-lint goes to v2.14.0 (x/tools v0.50) in
the Makefile and in CI. No gosec release carries a newer x/tools yet,
so the Makefile pins the master commit that does (x/tools v0.51); CI's
gosec action already runs master.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Court calls come down once their match leaves scheduled, from any device or door (one effect over the console's matches), replacing the start and fusensho hooks; docs updated to match. - One WinnerTick (side_cell.jsx) with sr-only text instead of an aria-label on a bare span, used by the bracket card, Recent results and the queue. - Representative picks: the member check runs only when the incoming row is kept (a stored hantei verdict replaces it); the typed-name resolve is shared with the inline lineup path (resolveTypedMemberName), so an unread member list is now warned about; the representative's id is read from its own row. - Shared stale-side guard and notifier in the team sheet, one daihyosen slot key, the buildPatch id stamps written once, slices for the announcement splice, one team_member_not_in_team code, merged CSS rules. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
gitrgoliveira
marked this pull request as ready for review
October 9, 2026 17:14
…13NiW6kth4REntdNv1jb7pt
- The self-run representative check judges only a member id the write introduces: an id the stored row already holds (a correction seated another team) and a side with no team id are not refused. - A side given another team drops its representative pick, and the latest representative pick on a side wins over a typed name still resolving. - Calling a match again withdraws its earlier call, whose text may differ. - An advance that cannot start because another start is still out says so on Up next instead of leaving the court idle. - The queue row's winner tick takes the row's gap, not its own margin. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A queue row's Start tapped while another match's start was still in flight sent the running bout back to the queue and then started nothing. The tap is now refused before the defer, as an open correction already refuses one (bc-crpn), with a notice on the row naming the match being started (startWhileStartingMessage). The advance's Up next note uses the same words. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The one-start guard reads the start in flight from a ref, so an advance that awaited its finish write judges the court as it is now; its refusal is the derived row/Up next notice with a toast, and clears itself. - Calling a match again takes the earlier call down only once the new one is answered, so a failed send leaves the earlier banner up. - Finish + Start Next and the decision advance hand the write's answer back to the editor. - A typed representative holds Finish, Save and Close until it is named. - A participant's representative winner id must name one of the two representatives. - A side given another team clears its representative only when one is picked, so the sheet is not dirtied for nothing. - A read-only unpicked representative side shows its team name, as the scoreboards do. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A side given another team clears its representative the server-following way (no edit stamp, no dirty mark, no autosave, an armed Finish stays armed), so it cannot overwrite a pick made on another device. - A participant's representative winner id is judged whenever the write changes any of the row's ids, so a changed pick cannot leave a winner id that names neither side. - A typed representative still being named holds the representative bout's Add and Remove, and a change of that bout supersedes it. - A participant's unread-members warning names the organiser, not the Lineups page. - The winner tick's screen-reader text is "Winner: ", separated from the name. - A court call re-sent with the same text no longer deletes the earlier id, which the server already replaced. - openapi: PUT /score documents the self-run 400 team_member_not_in_team. - Dropped a return value no caller reads; corrected a stale comment. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ules The reopen history line and the reseat recorder captured which picks a bracket door cleared through two separate snapshot/diff pairs. They now share repPickSnapshot and repPickClears (bracket_seat_audit.go), and markRepPicksCleared is a pure function over that diff, called once per door. No behaviour change. CLAUDE.md records the rules this PR added: the representative row keeps the team names on the server too (seatBracketSide); the reseat history line; a displacement without heldReason; claim.agree moving both per-group baselines; the organiser's representative picks judged; start_match.jsx as the one start helper for the console and the Scores tab. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Export: a "Representative Bouts" sheet in both engine workbooks for a
fixed-order team competition whose draw holds a representative bout,
naming each side's representative from its member id ("DH" row, "(DH)"
centre), drawn by the Kachinuki Detail renderer in the same match order.
- Self-run judge: the pick check asks the merge which picks it applies
(engine.RepPicksApplied) instead of re-deriving its rule; a participant's
numbered bout rows may seat only their own team's members and name one
of the bout's two fighters as winner; an unreadable team-members.yaml is
a terminal 409 (team_members_unreadable), not a 500 the queue retries.
- Announcements: an id replaced by an identical announcement still
withdraws the one that replaced it; the three undocumented announcement
routes are in the spec.
- Court console and Scores tab: a refused start stays on the refused match
until it leaves scheduled or is started, and a refusal for a start still
out becomes a past-tense notice when that start lands; a refusal that
editMatchScore toasted is not toasted again; winnerSideLR, teamMatchMarks
and startPatch are ES imports (side_marks.jsx leaf), window.startPatch is
gone.
- knockout: resolveSlots paints the fields of the side it names.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A participant's numbered bout row is judged only when the merge applies its bout group (engine.AppliedGroups, asked once per write): a stale row is held and kept in the history, and the write's other groups land, instead of the whole write being refused. - The /score handler reads the stored match once per write for every guard (the self-run or pick judge and the correction-reason check). - A knockout slot whose pool standing carries no id converges: the same name with no resolver id no longer repaints and rewrites the bracket on every pool write (the id backfill still runs the other way). - One rule for how long a refused Start notice lives (startRefusalStands), asked by the court console and the Scores tab, so the Scores tab no longer drops it on an empty refetch. - The team editor's withdrawal-removal Undo agrees both representative picks with the board it restores. - Doc: seatBracketSide names the draw-ready rename as its one direct-write exception and why it is safe; historyMemberLabel reuses squadLabelFor; side_marks.jsx's teamMatchMarks doc states what it returns. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Found in the browser check: two consoles calling the same match send the identical announcement twice, so the server keeps one; when the match starts, both consoles withdraw their call, and the second DELETE answers 404 because the first already took the banner down. deleteAnnouncement now reads a 404 as the outcome the caller asked for (the banner is gone), so the console logs no warning and the organiser's Dismiss of an announcement that expired or another console withdrew refreshes the list instead of showing "Announcement not found". docs: console-correct-completed recaptured (the Completed row's winner tick takes the row's gap). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…sories
CI's validate job fails its Anchore image scan on main and on this PR
("vulnerabilities with level 'medium' or higher"). The only matches are
fifteen GHSA advisories against github.com/xuri/excelize/v2 v2.11.0, each
fixed only on excelize's master: no release after v2.11.0 exists yet. The
repo owner chose to pin that master commit here
(v2.11.1-0.20261010030226-066c84b8f7de, which brings github.com/xuri/efp
v0.0.2 with it); a grype scan of the rebuilt binary finds nothing.
The example workbooks are regenerated (make examples): the new excelize
writes each font's properties in a different order and a cell's number to
15 significant digits (Excel's own precision). Every cell value, formula,
style, page break and sheet is otherwise identical in all 17, checked by
comparing their XML with the numbers normalised.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The round-15 change judged a participant's numbered bout row only when a pre-write probe of the payload said the merge would apply it. The engine rewrites a kachinuki write before its merge (it fills blank member ids and the winner member id, then names the rewritten bout in `changed`), so a row the probe skipped landed unjudged: a participant could store another team's member on a kachinuki bout (reproduced, 200 instead of 400). The member judges now run after the engine write, in the same transaction: the handler reads the match back (a transaction sees its own staged write), compares it with the snapshot read before the write, and judges only member ids that landed. A refusal aborts the transaction, so nothing of the write survives, and is answered as before. The probe is gone. The organiser's representative picks are judged the same way, and bulk-score now judges each entry's picks in that entry's own transaction. Also: - A knockout Winner slot whose pool standing has no id no longer repaints and loses its id on every pool write (as the sides already). - A team rename seats a side through seatBracketSide, so a representative bout's row names and winner follow (reachable when the auto-start after a representative bout is added fails). - ResolveQualifiedPools records a reseat history line when its repaint clears a representative pick (DELETE .../overrides reaches it). - The individual editor agrees a group only when its whole value then equals the server's, so a later edit is never silently unnamed. - The past-tense "Not started" notice appears only when the blocking start actually went out. - Comments and the spec say what is judged and what is reported; CLAUDE.md records the rules, the excelize pin and the court-call 404. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The post-write member judge reads the match back only when a payload row introduces a member id against the match as it stood before the write (introducesJudgedMember, a pure predicate): an echo of the stored ids, which the team editor stamps on every row with a lineup, no longer re-parses the competition's files under its write lock on each autosave. The engine never lands a member id the judge would refuse that the payload did not introduce. - A team rename carries a representative row's winner when it names the side by the match's old name too, not only the row's own (a row's side name can be blank or stale). - A start the server answers as superseded is not a start that went out: no past-tense "was being started" notice for a match refused meanwhile, and a pick does not pin the panel on a match the server did not start. - forEachBracketSideWithID inlined into its one caller; the bulk-score comment names team_members_unreadable; a bulk-score test sends two entries for one match in one request. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A start the server answers as superseded (a newer change to the match is stored; nothing of the start was written) is now a refused start on the court console and the Scores tab alike: "Not started: a newer change to this match was recorded first. Check the match before starting it." is stored on the match and toasted once, a pick does not pin the panel, and the Scores tab no longer turns a waiting refusal into a false "was being started" notice. Before, it gave no feedback at all. - The post-write member gate and the judge read one selection of rows (judgedRows), and an engine-level test pins the invariant the gate relies on: the engine never lands a member id the judge would refuse that the payload did not introduce. - A rename carries a representative row's winner even when the row already reads the new name. - One first-row-at-position finder (state.SubResultAt). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The post-write member gate's invariant listed only two id fillers; the merge's copies of held groups and keepQueuedScore land stored ids too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
round 19) - A start the server holds only as an echo (superseded with no heldGroups: another device's start of the same match landed first, the match is running) now counts as a start that went out on the court console and the Scores tab: no "Not started" sentence or toast, a pick pins the panel, a refusal waiting on it reads in the past tense. A start superseded by a real newer change (heldGroups names result) stays a refused start. - That refusal's sentence is composed by notSavedText from write_result.jsx's SUPERSEDED_REASON and SUPERSEDED_ADVICE under the "Not started" lead, instead of a second hand-written superseded copy. - The engine pin test now runs kachinuki bouts on both store branches and the representative bout on a knockout match, with stamped and older-stamped writes (held groups copied back) and saved lineups and team members whose ids no row carries; mutation-checked against every stored-id copy path. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
gitrgoliveira
deleted the
worktree-bridge-cse_013NiW6kth4REntdNv1jb7pt
branch
October 10, 2026 22:50
gitrgoliveira
added a commit
that referenced
this pull request
Oct 10, 2026
Main merged #463 (bc-aadv, bc-cdbl, bc-dhrp, bc-tmwn, bc-rrdr), #464 and #465, with Go 1.27.2 and excelize pinned to a commit carrying the fixes for the advisories that reddened the image scan. - bc-dhrp passes with the fixme removed: the daihyosen row now has a name box on each side. Renamed to what it checks. No fixme is left. - J7 reads the viewer's bracket through innerText, which now includes the screen-reader "Winner:" label beside the winner's tick (side_cell.jsx); the pattern allows it, and still asserts the tick beside the corrected winner. make e2e on the merged tree: 25 passed before the J7 fix (individual, kachinuki, smoke and the team tests before J7); the team spec after both changes: 10 passed. make go/test: EXIT 0 (golangci-lint v2.14.0 and gosec installed at the Makefile's pins, built with go1.27.2). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Merged
15 tasks done
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
Five bugs from the operator review on PR #442 (parent mp-yqxn.2), the Go toolchain bump they needed, and what nineteen review rounds and the browser tests found on the way.
The five beads
start_match.jsx) with one start at a time.go/vulnadvisories that also failed on main; the golangci-lint (v2.14.0) and gosec pins move with it.v2.11.1-0.20261010030226-066c84b8f7de, withgithub.com/xuri/efpv0.0.2): CI's Anchore image scan failed on main and here on fifteen advisories against excelize v2.11.0, each fixed only on excelize's master, with no release after v2.11.0 (the repo owner chose to pin it here, 2026-10-10). A grype scan of the rebuilt binary finds nothing. The 17 example workbooks were regenerated: the new excelize writes each font's properties in another order and numbers to 15 significant digits (Excel's own precision); every cell, formula, style and page break is otherwise identical, checked by comparing their XML.What the review rounds changed (code review rounds 1 to 19, the last six also run through Antigravity, plus three gap waves)
repPickA,repPickB), apart from the bout's score (bout:-1): two captains picking at once both land, a point struck elsewhere never erases a pick and a pick never erases a point, and a pick alone never re-sends the bout's score (found in a real-browser test). A pick made after its bout was removed is moved to the match's history in both arrival orders.seatBracketSide), which also keeps the representative row's team names equal to the match's. Every door that re-seats a side without reopening the match leaves a "reseat" line in that match's history; a reopened match's own line names the pick.team_member_not_in_team), and a participant's winner member must be one of the bout's two fighters (400winner_member_not_in_bout). A refusal rolls the whole write back (no match write, no history line, no eligibility record). A change the merge holds as stale is kept in the history and never refused. Bulk-score judges each entry's picks the same way, entry by entry. The match is read back only when the write introduces a member id against the match as it was (an echo of the stored ids, which the team editor sends on every row with a lineup, reads nothing), and an engine-level test pins why that is safe: every id the engine lands that the payload did not carry is the stored one at that position, or a winner derived from the row's own fighters. Ateam-members.yamlthe server cannot read refuses such a write with 409team_members_unreadable, never a 500 the offline queue would retry forever. Round 16 moved the judge after the write: a pre-write probe of the payload (removed) let a participant land another team's member on a kachinuki bout, because the engine fills a bout's member ids and names it as changed before its merge.heldGroupsnamesresult) is a refused start on both screens: "Not started: a newer change to the same thing was recorded first, so this one was kept in the match's history and nothing is lost. Check the match and its history before entering anything again: entering it again would replace the newer change." (composed from the superseded copywrite_result.jsxowns) is stored on the match and toasted once, and a pick does not pin the panel on it. A start held only as an echo (noheldGroups: another device's start of the same match landed first and the match is running) counts as a start that went out. The console and the Scores tab ask one rule (startRefusalStands), and a refusaleditMatchScorealready toasted is not toasted twice./scorehandler reads the stored match once per write;winnerSideLR,teamMatchMarksandstartPatchare ES imports (a newside_marks.jsxleaf) andwindow.startPatchis gone; the three undocumented announcement routes are in the spec; withdrawing an announcement that is already gone (another console took it down, or it expired) is not reported as a failure, on the console or the organiser's Dismiss.Not changed, raised for a decision
Tagging calls with their match on the server (the console-side design was the operator's choice, Q2 3A). The replaced-id withdrawal above is not match tagging.
A refused Start notice whose cause has since gone (e.g. "Sato is fighting on shiaijo B") stays on that match until it is started or leaves scheduled; tapping Start asks the server again. That is the cost of binding the notice to its match.
The console and the Scores tab each keep ONE stored refusal: starting a different match from the same screen clears the earlier match's notice (found in the browser check). Keeping one per match would need the past-tense notice reworked, which relies on the single slot.
OverrideBracketWinnertakes a winner NAME, so an override between two same-named sides (individuals from different dojos) is ambiguous; this predates the PR. The fix direction is an id or a side on the override request.A history entry stores the values of the changes it HELD, not of those it applied, so an applied pick's line names the group ("changed Aka's pick for the representative bout") and only a kept or moved pick names the representative; likewise the "reseat" line names the pick group it cleared, not the member. Naming applied values would change the history's schema.
Generalizing the team editor's per-part adopt of the representative row into a per-group adopt for every row: no defect drives it (numbered rows have one group each), so it is left as it is.
A pool qualifier repainted outside the requalification planner records no history line; that needs a bracket from before draw labels existed or a hand-edited file.
Found while testing, present on main before this PR (checked side by side on a main build): on the public self-run KACHINUKI score sheet the recorded bouts collapse and overlap the open bout, blocking taps on its buttons (two CSS rules from 2026-09-13 and 2026-05-18: the compact editor's
display: contentsand the fought rows'min-height: var(--tap-floor), which is 0 on a mouse); and a participant cannot end a kachinuki encounter there (the self-run rules refuse thekachinuki-exhaustiondecision). A kachinuki write that echoes a stale bout whose member ids the engine fills names that bout as changed at the write's stamp, so an organiser's write can overwrite a newer change to it (noteServerBoutChanges, also on main).A team rename in a draw-ready competition leaves a fixed-order numbered row's winner (stored as the team's name) on the old name; only the representative bout's row is kept in step.
Two different matches whose court calls have identical text share one banner, so withdrawing one match's call takes the other's down.
Found in the last review rounds, minor, not changed here: the editors' own Start match shows "Not applied" for the same echo answer the court console and Scores tab now treat as started (the echo rule lives in
classifyStartOutcome, not yet inwrite_result.jsx); in self-run, an organiser's start that echoes a participant's start of the same match reads as a refused start, because the result group also compares who recorded it (ResultSource); no server-side test pins the echo answer's shape (noheldGroupskey) that the client now reads; a start sent for a match that is already finished is answered as applied, so the console pins the finished match (predates this PR).bc-cfcl, the sixth bead picked for this PR, was already fixed on main by #453 (e851e4d); it was closed and bc-rrdr took its place.
Why
onEditScorecall that swallowed the error (catch (_s) { /* gate */ }); the lasting refusal lived only in a manual Start's path, and the Scores tab had no start guard at all.callingKeywas the only guard, and a local announce answers inside a double tap's gap. Nothing tied a call to its match.m.winner.Files changed
internal/engine/bracket_seat.go(new)seatBracketSide, the one writer of a knockout side: keeps the representative row's names equal to the match's, clears a re-seated side's pick, dated after itinternal/engine/bracket_seat_audit.go(new)repPickSnapshot/repPickClears/recordRepPickClears: the "reseat" history line for a pick a door clearedinternal/engine/representative_export.go(new)RepresentativeBoutMatches)internal/engine/match_merge.gointernal/engine/scoring.go,scoring_tx.go,engi.go,kachinuki.go,pool_requalify.go,match_history.goseatBracketSideand records its reseat lines; the withdrawal probe reads the stored pairinginternal/engine/daihyosen.goAddDaihyosenstores the team names on the rowinternal/engine/knockout.goresolveSlotspaints through the side it names and converges when the resolver has no id (sides and the winner);ResolveQualifiedPoolsrecords its reseat history lines in its own transactioninternal/engine/replace_participant.goseatBracketSide, keeping a representative row's names and winner in stepinternal/engine/kachinuki_export.go,workbook.go,export.go,bracket_result.goBracketMatchAsResultfor the judgesinternal/export/builder.gointernal/helper/excel_kachinuki.go,constants.go,excel.goWriteRepresentativeBoutsSheet, the sheet name and the "DH" row labelinternal/mobileapp/handlers_match.golandedMembersRefusal), for/scoreand each bulk-score entry, read back only when the write introduces an id (introducesJudgedMember; both read one row selection,judgedRows);memberJudge(one members read, 409 when unreadable); one snapshot per writeinternal/mobileapp/handlers_lineup.gomemberJudgeinternal/mobileapp/handlers_daihyosen.gointernal/state/match_groups.go,models.go,testdata/match_groups.jsonrepPickA/repPickBgroups,ReconcileWinnerMemberID, the shared group table,SubResultAtinternal/state/announcement_store.gospecs/openapi.yamlweb-mobile/js/start_match.jsx(new)START_SUPERSEDED_MESSAGE), the one-start guard,startRefusalStandsweb-mobile/js/side_marks.jsx(new)sideMarks,placeMarks,winnerSideLR,teamMatchMarksas a leaf the console importsweb-mobile/js/admin_shiaijo.jsxweb-mobile/js/admin_schedule_score_editor.jsx,admin_schedule.jsx,admin_competition.jsxadmin_competition.jsxpasses itshowToast);window.startPatchremovedweb-mobile/js/admin_scoring_team.jsx,admin_scoring_shared.jsx,lineup_resolver.jsxweb-mobile/js/admin_scoring_autosave.jsx,admin_scoring_individual.jsx,admin_scoring_engi.jsx,match_groups.jsxclaim.agree), pick groups on the wireweb-mobile/js/write_result.jsx,admin.jsx,app.jsx,api_client.jsxmarkToasted, the past-tense start sentence;deleteAnnouncementreads a 404 as already goneweb-mobile/js/match_history_view.jsxweb-mobile/js/bracket.jsx,side_cell.jsx,viewer_match.jsx,viewer_competition.jsxWinnerTick; Recent results carry it and list draws; re-exports fromside_marks.jsxweb-mobile/css/styles.cssweb-mobile/check-imports.mjs,check-write-result.mjsside_marks.jsxand the Scores tab's imports checked;writeDisplacedForWinneradded to the write-result names that must not be read offwindowdocs/user-guide/court-operators/scoring-a-match.md,recording-decisions.md,docs/user-guide/organisers/run-tournament.md,team-tournaments.md,docs/architecture/data-model.md,network-architecture.mddocs/screenshots/*.png,realtime-update.webmCLAUDE.mdgo.mod,go.sum,Makefile,Dockerfile*,.github/workflows/*.yaml,GEMINI.md,docs/dev-guide/contributing.md,docs/user-guide/install/install.md,docs/user-guide/commands/version.md*-example-*.xlsx(17)internal/engine/*_test.go(bracket_seat, bracket_seat_audit, bracket_reseat_rep_picks, k3_refused_before_reseat, match_merge_rep_picks, representative_export, knockout, daihyosen, scoring, sub_hantei_preserve, landed_member_ids_pin: kachinuki on both store branches and the representative bout on a knockout, stamped and older-stamped writes, saved lineups and members, mutation-checked)internal/mobileapp/*_test.go(organiser_rep_pick_judge, self_run_bout_members, self_run_daihyosen, daihyosen_row_names, handlers_announcement)internal/state/*_test.go,internal/export/*_test.go,internal/helper/excel_kachinuki_test.goweb-mobile/js/__tests__/**(start_match, side_marks_leaf, delete_announcement, team_daihyosen_representative, admin_scores_auto_start_refusal, admin_shiaijo, call_to_court_double_tap, edit_match_score_toast_once, changed_groups_every_editor, match_history_view, winner_tick_reads_in_order, and the suites that stubbedwindow.startPatch)Screenshots
Captured in Chromium through Playwright. The
r15-captures were taken on 419331c's build; the two commits after it change only how an already-gone announcement's withdrawal is read and the excelize version, and each was re-checked on its own build (see the test plan). The other captures are from earlier rounds of this PR, of screens the later rounds did not change. The operator console runs at 1180x820 with touch emulation, the public pages at 390x844. A refusal cannot be staged from one browser (the start gates are per competition, and a court-busy or barred start needs a second device's timing), so each refused start below was answered by a route intercept with the server's own 409 body (respondCourtBusy's shape); everything else is the real server.bc-aadv, Scores tab: Finish + Start Next, the next start refused. After the toast has gone and the editor is closed, the refused match's row still says why.
bc-aadv, court console: a queue Start refused for Match 4; Up next then moved (Match 3 now) and the notice stayed on Match 4's row. While Match 3's start was held in flight, Start on Match 4 was refused as "still being started"; when that start landed, the notice turned past tense and stayed until Match 4 is started. The Up next card's notice from the first capture of this PR still holds:
A start held because a different result is stored (a superseded answer naming
resultinheldGroups, answered by a route intercept), on the court console's Up next card with its toast, and on the Scores tab's row after Finish + Start Next:bc-cdbl: a double tap on Call to court gave one announcement and one banner; Start match took it down.
The same match called from two consoles (identical text, so the server kept one, under the second id); Start on the FIRST console withdrew it through the replaced id (
DELETEof that id answered 204) and the phone's banner went:bc-dhrp: the representative row with Shiro's pick from the list (an unnamed slot, S2.4) and Aka's typed new member (Mika Tan, S1.6); Aka's point on the bout; the public card naming both representatives:
Self-run, from the PUBLIC page with no password: the participant picked and typed representatives; the member add answered 201 and every score write 200.
The History names a pick the server kept rather than applied (here a stale write's picks, staged by replaying the editor's own payload with an older stamp); an applied pick is listed as the group it changed:
The exported results workbook's new Representative Bouts sheet (rendered from the downloaded .xlsx with LibreOffice; "DH" as the bout, "(DH)" in the centre, both representatives named):
bc-tmwn: the court's Completed list, a team result decided by daihyosen (IV 0–0, PW 0–0) and one by kiken; the winner in accent bold with the tick at the outer edge, "Kiken" beside the withdrawn team:
Viewer Recent results, before and after fixing where the tick sits (it took a line of its own; it now sits in the name row):
bc-rrdr: the recaptured docs screenshot lists the drawn Pool A bout ("K X M") in Recent results, with no tick:
Test plan
make go/testpasses (lint + security scan + tests), exit 0 on the final tree (5b0c7d4). CI on 5b0c7d4: every check passes,validate(with its Anchore image scan) included. Local gate: 342 JS unit test files with 6713 tests, golangci-lint "0 issues", gosec clean,govulncheck"No vulnerabilities found.", the npm audit, and every Go package with tests at 85% or more (engine 90.0%, state 89.9%, mobileapp 88.4%, export 90.5%, helper 92.5%, domain 90.3%, cmd 88.5%; the rootmainpackage reads 0% as it does on main). The full render suite, which the gate runs only in part, passes on its own: 144 files, 1812 tests.make docs/buildpasses. All on 5b0c7d4's tree.expected: 400 actual: 200before, the foreign id stored), a held numbered row is not refused (expected: 200 actual: 400before), a slot the resolver knows by name only converges (expected: 0 actual: 1before), the Scores tab keeps a refusal across an empty list (expected null to be truthybefore), the Undo agrees the picks (expected [ 'result', 'repPickA' ] to not include 'repPickA'before), an already-gone announcement is not a failed withdrawal (promise rejectedbefore).heldGroups: ["result"](route intercept) shows the "Not started…" sentence on the court console's Up next card with one toast and leaves the panel unpinned; on the Scores tab, Finish + Start Next lands the editor on the next match and its row carries the same sentence after the editor closes; the same answer withoutheldGroups(an echo) shows no notice and no toast and the panel opens on the match. No console errors.changedwas refused 400team_member_not_in_team, with the stored match and its history unchanged. The kachinuki sheet's layout defect (under "Not changed") was reproduced on a main build with identical measurements and byte-identical screenshots.["repPickA"]and["repPickB"], and both picks were stored).GET .../export, which no UI button calls, fetched from the signed-in page) and Download blank template all answered 200 and opened in LibreOffice; both stored workbooks carry the Representative Bouts sheet naming both representatives; Export PDFs (Full bracket, All) produced their PDFs; no console errors.PUT /score(curl, since no UI sends it) with an empty password naming the other team's member on bout 1 was refused 400team_member_not_in_team; the same write with the side's own member answered 200, and a wrong non-empty password 401.docs/(make docs/buildpasses)make docs/screenshots SINCE=mainon 419331c changed one (console-correct-completed: the Completed row's tick gap), reviewed and copied; the three recaptured earlier andrealtime-update.webmare in the PR.Closes bc-aadv, bc-cdbl, bc-dhrp, bc-tmwn, bc-rrdr
🤖 Generated with Claude Code