Skip to content

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 into
mainfrom
worktree-bridge-cse_013NiW6kth4REntdNv1jb7pt
Oct 10, 2026
Merged

gitrgoliveira merged 40 commits into
mainfrom
worktree-bridge-cse_013NiW6kth4REntdNv1jb7pt

Conversation

@gitrgoliveira

@gitrgoliveira gitrgoliveira commented Oct 9, 2026 •

Copy link
Copy Markdown
Owner

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

  • bc-aadv: when Finish + Start Next, or the start after a kiken or fusenpai, cannot start the court's next match, the refusal stays on the refused match (the court console's Up next card or queue row, and the Scores tab's row), not only in a 2.7s toast; a clock-skew refusal no longer shows nothing. Both automatic starts, on the console and on the Scores tab, go through one helper (start_match.jsx) with one start at a time.
  • bc-cdbl: a double tap on Call to court (Up next card or queue row) sends one announcement. A call is withdrawn once its match leaves scheduled, however that happens, as long as the console that made it is open. The server replaces an active announcement with an identical message instead of stacking a second banner, and the id it replaced still withdraws the one that replaced it, so a console holding the older id can take the call down. Operator decisions 2026-10-08: Q1 (a) and Q2 3A.
  • bc-dhrp: the representative bout (daihyosen row) of a knockout team match lets each side pick its representative from the team's members, or type a new name (which adds a member); self-run participants can do the same on the public sheet (operator decision 2026-10-08). The pick is stored as the member id beside the TEAM names, so the Ht placement is unchanged. The TV board, the viewer card and now the exported workbook (a new "Representative Bouts" sheet) name the representative, and the match History names a pick it kept rather than applied.
  • bc-tmwn: the court's Completed list names each winner (accent bold, the bracket card's green tick at the outer edge), and "Kiken"/"Fus." sit beside a team that withdrew or did not appear. A team result decided by daihyosen ("IV 0–0 / PW 0–0") no longer looks like a draw. The public viewer's Recent results carry the same tick.
  • bc-rrdr: a drawn bout (hikiwake) appears in the public viewer's Recent results, as it already did in the operator's Completed list.
  • Toolchain: Go 1.27.2 and golang.org/x/net v0.60.0, which clear the ten go/vuln advisories that also failed on main; the golangci-lint (v2.14.0) and gosec pins move with it.
  • excelize pinned to its master commit (v2.11.1-0.20261010030226-066c84b8f7de, with github.com/xuri/efp v0.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)

  • Representative picks are ordered like every other change. Each side's pick is its own dated change (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.
  • A correction that seats another team clears that side's pick, in the same write, through one owner (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.
  • Who a write seats is judged by the server on what the write LANDS. After the engine writes a score, in the same transaction, the server reads the match back and judges every member id that landed: a representative pick (anyone's, the organiser's included) and a participant's numbered bout fighter must be on the side's team (400 team_member_not_in_team), and a participant's winner member must be one of the bout's two fighters (400 winner_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. A team-members.yaml the server cannot read refuses such a write with 409 team_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.
  • Every score editor agrees what it adopts from another device, per group, so an edit after an adopt is named and an adopted value is never re-sent as this device's own (the withdrawal-removal Undo included); an adopt that takes only part of a group agrees it only when the editor's whole group then equals the server's.
  • A refused Start notice belongs to the refused match: it stays while that match is scheduled (Up next moving on no longer clears it) and goes when the match starts or leaves scheduled. A refusal for a start that was still out becomes a past-tense notice ("Not started: ... was being started when this match was asked for. Start it when the court is free.") once that start lands (only when it actually went out); the match is never started on its own. A start the server answers as superseded because a DIFFERENT result is stored (a send-back, a correction: its heldGroups names result) 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 copy write_result.jsx owns) 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 (no heldGroups: 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 refusal editMatchScore already toasted is not toasted twice.
  • Smaller fixes: a knockout slot (a side or the winner) whose pool standing has no id no longer rewrites the bracket on every pool write; a team rename keeps a representative bout's row names and winner in step (also when the row's own side name is blank, stale or already the new name); the knockout repaint after a pool write (or a removed pool-rank override) records the history line when it clears a pick; the /score handler reads the stored match once per write; winnerSideLR, teamMatchMarks and startPatch are ES imports (a new side_marks.jsx leaf) and window.startPatch is 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.

  • OverrideBracketWinner takes 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: contents and 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 the kachinuki-exhaustion decision). 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 in write_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 (no heldGroups key) 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

  • bc-aadv: the automatic starts were a hand-copied onEditScore call 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.
  • bc-cdbl: the in-flight callingKey was the only guard, and a local announce answers inside a double tap's gap. Nothing tied a call to its match.
  • bc-dhrp: the row was read-only by design, because a name picked there would have been a lineup write for a key ("daihyosen") that does not exist. A pick that rides the bout row as a member id needs no lineup key. Making it one change with the bout's score is what let a pick and a point erase each other, so each became its own group; the workbook's team block skips position -1, so the representatives never reached an export.
  • bc-tmwn: a team row's result is an aggregate (IV/PW) and carries no marks by rule, so nothing on it named the winner.
  • bc-rrdr: the Recent results filter required m.winner.

Files changed

File What
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 it
internal/engine/bracket_seat_audit.go (new) repPickSnapshot/repPickClears/recordRepPickClears: the "reseat" history line for a pick a door cleared
internal/engine/representative_export.go (new) The Representative Bouts sheet's sections (RepresentativeBoutMatches)
internal/engine/match_merge.go Per-side pick groups, the pick-after-removal displacement, held picks with no row
internal/engine/scoring.go, scoring_tx.go, engi.go, kachinuki.go, pool_requalify.go, match_history.go Every bracket door seats through seatBracketSide and records its reseat lines; the withdrawal probe reads the stored pairing
internal/engine/daihyosen.go AddDaihyosen stores the team names on the row
internal/engine/knockout.go resolveSlots paints through the side it names and converges when the resolver has no id (sides and the winner); ResolveQualifiedPools records its reseat history lines in its own transaction
internal/engine/replace_participant.go A rename seats a side through seatBracketSide, keeping a representative row's names and winner in step
internal/engine/kachinuki_export.go, workbook.go, export.go, bracket_result.go The detail-sheet walk shared by the new sheet; BracketMatchAsResult for the judges
internal/export/builder.go The results workbook carries the Representative Bouts sheet
internal/helper/excel_kachinuki.go, constants.go, excel.go WriteRepresentativeBoutsSheet, the sheet name and the "DH" row label
internal/mobileapp/handlers_match.go The member judge run on what a write lands, after the engine write, aborting the transaction on a refusal (landedMembersRefusal), for /score and 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 write
internal/mobileapp/handlers_lineup.go The lineup member check through memberJudge
internal/mobileapp/handlers_daihyosen.go The add and remove date both picks
internal/state/match_groups.go, models.go, testdata/match_groups.json repPickA/repPickB groups, ReconcileWinnerMemberID, the shared group table, SubResultAt
internal/state/announcement_store.go An identical announcement replaces the active one; the replaced id withdraws it
specs/openapi.yaml The judges' codes and rules, the reseat door, the announcement replacement, and the three announcement routes that were missing
web-mobile/js/start_match.jsx (new) One start helper for the console and the Scores tab: the start patch, the outcome (a superseded start is refused, START_SUPERSEDED_MESSAGE), the one-start guard, startRefusalStands
web-mobile/js/side_marks.jsx (new) sideMarks, placeMarks, winnerSideLR, teamMatchMarks as a leaf the console imports
web-mobile/js/admin_shiaijo.jsx bc-aadv/bc-cdbl/bc-tmwn on the court console; the refusal bound to its match and its past-tense form
web-mobile/js/admin_schedule_score_editor.jsx, admin_schedule.jsx, admin_competition.jsx The Scores tab's automatic start through the helper, its row notice and toast (admin_competition.jsx passes it showToast); window.startPatch removed
web-mobile/js/admin_scoring_team.jsx, admin_scoring_shared.jsx, lineup_resolver.jsx The representative picker, the per-part adopt, typed names, the History's member names, the Undo agreeing the picks
web-mobile/js/admin_scoring_autosave.jsx, admin_scoring_individual.jsx, admin_scoring_engi.jsx, match_groups.jsx Per-group agreed baselines (claim.agree), pick groups on the wire
web-mobile/js/write_result.jsx, admin.jsx, app.jsx, api_client.jsx The displaced-change wording, markToasted, the past-tense start sentence; deleteAnnouncement reads a 404 as already gone
web-mobile/js/match_history_view.jsx Door words for reseat and displaced changes; picks named
web-mobile/js/bracket.jsx, side_cell.jsx, viewer_match.jsx, viewer_competition.jsx WinnerTick; Recent results carry it and list draws; re-exports from side_marks.jsx
web-mobile/css/styles.css The Completed row's winner and tick
web-mobile/check-imports.mjs, check-write-result.mjs side_marks.jsx and the Scores tab's imports checked; writeDisplacedForWinner added to the write-result names that must not be read off window
docs/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.md The refused-start notice, the representative picker and sheet, the announcement replacement, the stored picks; the write-ordering table names each side's pick group and a pick moved to the history by a removal
docs/screenshots/*.png, realtime-update.webm Recaptured where this PR changed what they show
CLAUDE.md The rules this PR added
go.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 Go 1.27.2, x/net v0.60.0, excelize pinned to its master commit, golangci-lint v2.14.0 and the gosec pin
*-example-*.xlsx (17) Regenerated with the pinned excelize (serialization only: font property order, 15-digit numbers)
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) Seat, reseat history, per-side pick ordering in both arrival orders, the probe agreeing with the merge, the sheet, slot convergence
internal/mobileapp/*_test.go (organiser_rep_pick_judge, self_run_bout_members, self_run_daihyosen, daihyosen_row_names, handlers_announcement) Both judges, held rows and picks not refused, unreadable members, row names at add, the announcement alias
internal/state/*_test.go, internal/export/*_test.go, internal/helper/excel_kachinuki_test.go The group table, models, team result, the store's replacement, the sheet through both exports
web-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 stubbed window.startPatch) The start helper and refusal rule, the picker and its adopt, the call guard, the toast-once rule, per-group agreement, the History, the winner tick

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.

Scores tab row keeps the refusal

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: Up next keeps the refusal

Refused queue Start, Up next moved on Held start in flight After it landed
kept still being started past tense

A start held because a different result is stored (a superseded answer naming result in heldGroups, 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:

Court console Scores tab
console scores

bc-cdbl: a double tap on Call to court gave one announcement and one banner; Start match took it down.

After the double tap After Start match
one banner no banner

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 (DELETE of that id answered 204) and the phone's banner went:

Called from two consoles After Start on the first
one banner no banner

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:

representative row

Aka's point on the representative bout Public match card
scored viewer card

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.

self-run representative

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:

History names the picks

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

Representative Bouts sheet

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:

Completed rows

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

Before the fix After
before after

bc-rrdr: the recaptured docs screenshot lists the drawn Pool A bout ("K X M") in Recent results, with no tick:

drawn bout in Recent results

Test plan

  • make go/test passes (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 root main package 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/build passes. All on 5b0c7d4's tree.
  • New/updated unit tests cover the change. Each fix's test was run red before its fix (the failure recorded) and green after; tests that are green by design are kept as pins and named as such. Examples from the last rounds: a participant's kachinuki write landing another team's member is refused and stores nothing (expected: 400 actual: 200 before, the foreign id stored), a held numbered row is not refused (expected: 200 actual: 400 before), a slot the resolver knows by name only converges (expected: 0 actual: 1 before), the Scores tab keeps a refusal across an empty list (expected null to be truthy before), the Undo agrees the picks (expected [ 'result', 'repPickA' ] to not include 'repPickA' before), an already-gone announcement is not a failed withdrawal (promise rejected before).
  • Manual browser verification on 419331c's build, then each later change on its own build (a self-run tournament, team knockouts of 4 and 8 teams on one court):
    • Round 19 (5b0c7d4's build, from a clean tree): a start answered superseded with 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 without heldGroups (an echo) shows no notice and no toast and the panel opens on the match. No console errors.
    • Round 16 (5219f2b's build), from the PUBLIC page with no password: a kachinuki match scored by a participant (fighters picked from the list and one typed, five bouts recorded, every write 200, the picked fighters stored with their member ids); a fixed-order final with the representative bout added, both representatives picked and the match finished (every write 200, the picks stored). A crafted write (curl, since no UI sends it) naming another team's member on a kachinuki bout it left out of changed was refused 400 team_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.
    • bc-aadv, Scores tab: Finish + Start Next with the next start refused (route intercept); the row keeps the notice after the toast is gone and the editor is closed.
    • bc-aadv, court console: a refused queue Start keeps its notice when Up next moves on (Move down) and loses it when that match is started from another device. A Start while another start is held in flight shows "still being started", then the past-tense notice once it lands. Starting a DIFFERENT match from the same console clears the earlier notice (one stored refusal per console, by design; tapping Start on the refused match asks the server again).
    • bc-cdbl: a double tap on Call to court posted one announcement; the same match called from two consoles kept one banner; Start on the first console withdrew it through the replaced id (204) and the phone's banner went. The second console's own withdrawal then found it gone (404) and logged "Could not withdraw the court call"; 5a38536 reads that 404 as already gone. Re-checked on 5a38536: tab A's DELETE of the replaced id 204, tab B's 404, the banner gone, and neither console logged the warning; the organiser's Dismiss of an announcement another console had already taken down refreshed the list with no error toast.
    • bc-dhrp: picked from the list on one side and typed a new member on the other, scored the bout, finished the match by it; the History names kept picks; the downloaded workbook carries the Representative Bouts sheet naming both representatives. Earlier rounds did the same as a self-run participant from the public page, and checked the organiser and a participant picking at once, the participant's sheet kept stale by blocking its live updates (each write named only its own side's group, ["repPickA"] and ["repPickB"], and both picks were stored).
    • After the excelize pin (a3f28c6's binary): Download results (.xlsx), the stored-draw export (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.
    • bc-tmwn: Completed rows for a daihyosen result and a kiken (winner in accent bold, tick at the outer edge, "Kiken" beside the withdrawn team).
    • Self-run judge: a crafted PUT /score (curl, since no UI sends it) with an empty password naming the other team's member on bout 1 was refused 400 team_member_not_in_team; the same write with the side's own member answered 200, and a wrong non-empty password 401.
    • bc-rrdr: seen in the recaptured docs screenshot (drawn bout listed).
  • Screenshots added above
  • Docs updated under docs/ (make docs/build passes)
  • Docs screenshots and videos recaptured: make docs/screenshots SINCE=main on 419331c changed one (console-correct-completed: the Completed row's tick gap), reviewed and copied; the three recaptured earlier and realtime-update.webm are in the PR.
  • No new console errors or warnings from the app: the only console lines in the browser checks were the intercepted 409s (staged) and, on the console that withdraws a court call second, the browser's own "Failed to load resource ... 404" line for that DELETE (the browser logs a 404 response itself; the app's warning beside it is gone since 5a38536, re-checked on that build).

Closes bc-aadv, bc-cdbl, bc-dhrp, bc-tmwn, bc-rrdr

🤖 Generated with Claude Code

gitrgoliveira and others added 14 commits October 8, 2026 16:33
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
gitrgoliveira marked this pull request as ready for review October 9, 2026 17:14
Copilot AI balanced review requested due to automatic review settings October 9, 2026 17:14

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

Copilot AI balanced review requested due to automatic review settings October 9, 2026 17:27

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

- 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>
Copilot AI balanced review requested due to automatic review settings October 9, 2026 18:04

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

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>
Copilot AI balanced review requested due to automatic review settings October 9, 2026 18:23

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

- 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>
Copilot AI balanced review requested due to automatic review settings October 9, 2026 19:00

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

- 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>
Copilot AI balanced review requested due to automatic review settings October 10, 2026 08:51

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

- 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>
Copilot AI balanced review requested due to automatic review settings October 10, 2026 10:02

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

- 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>
Copilot AI balanced review requested due to automatic review settings October 10, 2026 11:15

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

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>
Copilot AI balanced review requested due to automatic review settings October 10, 2026 11:41

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

…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>
Copilot AI balanced review requested due to automatic review settings October 10, 2026 12:21

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

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>
Copilot AI balanced review requested due to automatic review settings October 10, 2026 19:17

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

gitrgoliveira and others added 3 commits October 10, 2026 21:25
- 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>
Copilot AI balanced review requested due to automatic review settings October 10, 2026 21:39

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

 round 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>
Copilot AI balanced review requested due to automatic review settings October 10, 2026 22:25

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

@gitrgoliveira
gitrgoliveira merged commit 5e7a81f into main Oct 10, 2026
13 checks passed
@gitrgoliveira
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants