Skip to content

fix(bc-dtfn, bc-dtip, bc-fsnp, bc-emsl, bc-sync): a bounced tap, a fusensho or a reload no longer changes a recorded result - #450

Merged
gitrgoliveira merged 14 commits into
mainfrom
bc-p2-scoring-fixes
Sep 27, 2026
Merged

gitrgoliveira merged 14 commits into
mainfrom
bc-p2-scoring-fixes

Conversation

@gitrgoliveira

@gitrgoliveira gitrgoliveira commented Sep 26, 2026 •

Copy link
Copy Markdown
Owner

Summary

Fixes five of the P2 operator bugs filed from the mp-yqxn.2 clumsy-operator review. In each one a single tap or a reload silently changed or lost a recorded result. They are the P2s filed since 2026-09-23 that are not waiting on an operator decision and not already fixed (bc-unfx was fixed by #444). Each follows the plan recorded on its bead on 2026-09-25, checked again against main after #447.

  • bc-dtfn, a double tap passed the two-tap Finish / End match guard. A bouncing thumb finished the bout, and with Finish + Start Next started the next one. The confirming tap now counts only once the button has been armed for 400ms. A bounce only arms it, and a deliberate second tap still commits. Keyboard activation is unchanged. The engi editor's Save result / Finish + Start Next now works the same way (operator ruling: the editors behave alike); it used to save and start the next match in one tap.
  • bc-dtip, a double tap recorded two ippons. A bounce recorded M M and the bout read as won 2-0. A repeat tap on the same side of a bout within 400ms is now ignored, on the individual board, every team bout row and kachinuki. The foul + is guarded the same way: its second foul awarded an H to the opponent. The engi flag +, the overtime counter's + and removal taps (a scored mark, the engi −) are deliberately not guarded (operator rulings: a flag count is not an ippon call, a stray overtime period changes no result, and a bounced removal is re-entered). The other side is never blocked, and taking a mark back off lets the right letter in at once. A device clock stepped back is never read as a bounce, on either guard.
  • bc-fsnp, a per-bout fusensho erased the other side's points. FIK Article 32 says points already scored stay valid, and the docs say so; the server already kept them for a match-level default win. Now the winner gets its circles and the other side keeps what it struck.
    • Undoing it after a reload removes only the circles. Before, they stayed as two ordinary points.
    • Taking a mark off the other side, to correct it, keeps the fusensho. Any other scoring edit on that row ends it the same way.
    • Switching the fusensho to the other side on a reopened bout, then undoing it, leaves no circles behind as points.
    • Fusensho is refused when the opponent has already won the bout with two points: the button is greyed out, and tapping it shows the reason under that side until the next edit (a tooltip never shows on a touchscreen). Circles against two points would be a 2-2, which the server rejects on every save. When that opponent holds the fusensho itself, the reason names its undo first.
  • bc-emsl, a tap on an empty mark slot cleared the bout's Tie or Fusensho and saved it. A tap on an empty slot or a default-win circle now does nothing, as in the individual editor. The pressed Fusensho button is the undo for its circles.
  • bc-sync, the sync pill read "Synced" over an unsent edit, and a reload then lost it. The pill now reads Syncing… from the first tap until the save has gone out.
    • If the page reloads or the tab is hidden within the 300ms autosave delay, the edit goes into the saved offline queue instead of being lost.
    • This also applies to an iPad switching apps mid-bout: the pending edit is queued and sent as soon as it can be.
    • Every way out of the editor keeps an edit still inside the autosave delay: closing it, Prev and Next (buttons and arrow keys), picking another match, a court switch, or the court view moving on by itself. Before, only closing kept it. Each autosave carries the time of the tap, not the time it is sent, so a tap made before another device finished the match can never reopen it (operator ruling 2026-09-27).
    • Prev and Next now ask first, as closing does, before dropping what the autosave does not carry: a Mark draw on a running match, or unsaved changes to a finished match (operator ruling 2026-09-27). Before, they moved on and the change was lost.
    • Every change on a running match is saved as it is made. The overtime counter, the rep-player pickers of a tie-break bout and a team's daihyosen hantei pick were only saved with the next point, closing or Finish, and a reload lost them.
    • A save already sent but not yet answered when the page goes away (up to 12s on bad Wi-Fi) is put into the saved offline queue too, since the page going away cancels the request. A newer queued write for that match, such as a Finish, is kept. A tab that is only hidden does not send that save a second time.
    • The saved offline queue is shared by every tab of the app. A tab saving its own queue used to write over it, dropping a write another tab had queued there. It now merges: it adds its own writes, keeps the newer write where two tabs hold one for the same match, and removes only the ones it settled. Each tab also takes up the writes the others store, so a Finish queued offline in a tab that is then closed is still sent by a tab left open. A password reset removes the old password from every stored write, every tab's included.

Why

  • bc-dtfn / bc-dtip: Preact renders on a microtask, so the second click of a bounce always lands on a fresh render: the guard reads itself armed, and the button appends again. Only time tells a bounce from a second tap. web-mobile/js/tap_guard.jsx is now the one owner of that question (TAP_BOUNCE_MS = 400, bc-kbrw's value; a click with detail === 0 comes from the keyboard and is never a bounce). The five arm sites use useArmedConfirm. The ippon and foul buttons use acceptTap, keyed by side (idx:side on the team sheet). bc-kbrw's private bout-list guard moves onto the same leaf.
  • bc-fsnp: applyFusenshoToggle modelled a default win as "replace the scoreline". It now takes the loser's points from the pre-fusensho snapshot through struckIppons (result_slot.jsx, the JS twin of engine.struckIppons, beside DEFAULT_WIN_IPPON). clearFusensho strips only the winner's circles. Every scoring edit on a bout row now goes through updateSubScore, whose applyBoutScoreEdit applies the edit first and ends the fusensho through clearFusensho last, except taking a mark off the side the fusensho went against: that is a correction, it keeps the fusensho, and the snapshot its undo restores follows it; before, seven places cleared the flag by hand. Without that, keeping the loser's points would bring the defect back on the next ordinary edit. fusenshoAllowed is the refusal, and it disables the button, so a refused tap sends no write. No Go change: the server already stores and counts this row shape (checked in the browser: ipponsA: [○, ○], ipponsB: [K], decision: fusensho).
  • bc-emsl: the team sheet's slot onClick called setPts for every slot, and setPts always ends a Tie or Fusensho and marks the editor dirty.
  • bc-sync: the debounce stage was invisible to api_client, whose sync status counted only in-flight and queued writes. And a fetch started as the page unloads dies with it before its failure path can queue it.
    • API.notePendingEdit adds the pending stage to the status; it is still syncing, with no new value.
    • A running write whose patch carries durable: true goes straight into the saved offline queue, exactly the state a failed fetch leaves. recordScore reads the flag from the patch and never sends it.
    • useDebouncedRunningWrite registers the pending edit on the first tap and releases it only after the write is dispatched, so the pill never reads "Synced" in between. On pagehide or the tab being hidden, it writes a pending edit at once, marked durable.
    • This works only because every score-editor host reaches API.recordScore before the page goes, with no await on I/O in front of it. score_write_same_tick.test.jsx pins the whole chain: attemptScoreWrite, editMatchScore and each of the five hosts' onSubmit.
    • The editor's unmount used to cancel a pending edit, so only the exits that saved first (closing, Prev/Next) kept it. It now writes it. That needed the new stamp: the court view shows running[0] unless the operator picked a match, so another device finishing the match re-keys the editor by itself, and a write stamped when sent would be newer than that finish and reopen it. useDebouncedRunningWrite records performance.now() at the tap (editedPerf, never sent) and recordScore takes its age off the server-relative stamp (_editAge). Discard cancels before closing. flushPending and leaveAfterSaving are gone.
    • A timestamp built from a monotonic age is rounded up to whole milliseconds (_perfAgeMs). The server reads modifiedAt as an int64 and refused a fraction with a 400, which the browser check caught; the existing clock-skew re-stamp (_restampFor) had the same fraction.
    • Cross-tab: _onStoredQueueChanged follows other tabs' writes through the storage event, applying the rehydrate's checks (_storedEntryProblem). A write two tabs hold can be sent by both, which the server's last-write-wins makes harmless.

Notes:

  • PR test(bc-e2eh, mp-yqxn.2): e2e harness and a clumsy-operator review of knockout and mixed #442 (e2e harness) must adapt: this PR merges first (operator decision). Its fixtures tap an armed Finish / End match immediately, and its audit rows double-tap ippons 250ms apart. Both need a wait above TAP_BOUNCE_MS, as this PR gives the screenshot harness (TAP_DWELL_MS, sameSideTapPacer). Its test.fixme entries for bc-fsnp, bc-emsl and bc-sync can then be switched on, and its double-tap audit rows become assertions.
  • Left unguarded (operator ruling 2026-09-27): the overtime counter's +. A bounce saves one period too many. The count shows only as ×N in the editor row, never in a result, the bracket, the TV board or the export, and one tap on − takes it back. Checked on this branch: two taps 47ms apart took ×1 to ×3. CLAUDE.md lists it with the other taps left unguarded on purpose.
  • Not in this PR: the plans suggested bundling bc-offl and bc-rboq with bc-sync. bc-offl now has its decisions (2026-09-27) and is planned on its own bead; bc-rboq was not among the five.
  • Review round 3 (14 findings): all fixed except two left on purpose. The tap guard still times the gap with Date.now() when the handler runs, not the event's timeStamp: that helps only where a browser stamps a tap's click at the touch, and the bout-list guard stamps with no event, so mixing them would compare two clocks (recorded in CLAUDE.md). Reading the stored queue on every save is not material: it holds at most one entry per match.
  • Found while checking: the team editor's re-seed read the previous board inside its setSubs updater. Preact, which the app runs on, runs the updater at once, so production was not affected; React, which the render tests run on, runs it a render later, after the board has moved on. It now reads it when the adopt fires.

Files changed

File What
web-mobile/js/tap_guard.jsx New leaf: TAP_BOUNCE_MS, isPointerTap, stampTap/clearTap/tapIsBounce, acceptTap, swallowBounce, useArmedConfirm; one withinBounce time test, which ignores a clock stepped back
web-mobile/js/admin_scoring_individual.jsx Finish and Finish + Start Next use useArmedConfirm; ippon buttons guarded per side (tapIppon), removePt clears the guard; Close and Prev/Next share one leave rule (ask before dropping a Mark draw or an unsaved correction; Discard cancels the pending edit); the overtime counter and the rep pickers save as they change, and the rep adopt keeps a pick until the server holds it
web-mobile/js/admin_scoring_team.jsx Finish, Finish + Start Next and End match use useArmedConfirm; ippon and foul + guarded per bout side; bc-kbrw's bout-list guard on the leaf; updateSubScore, through which every scoring edit ends a fusensho; Fusensho refused with fusenshoButtonTitle; empty and circle slots inert; Close and Prev/Next share one leave rule; the overtime counter and the daihyosen hantei pick save as they change, and their adopts keep the operator's value until the server holds it; a refused Fusensho's reason clears on the next bout edit; the re-seed reads the previous board when it fires
web-mobile/js/admin_scoring_shared.jsx applyFusenshoToggle keeps the loser's struck points; new fusenshoAllowed, clearFusensho, applyBoutScoreEdit; a reopened row's snapshot base has no circles; FoulCounter's + guarded
web-mobile/js/result_slot.jsx DEFAULT_WIN_IPPON, struckIppons; enchoOn and defaultWinMaru, moved from bracket.jsx
web-mobile/js/bracket.jsx Imports enchoOn and defaultWinMaru from result_slot.jsx, and still exports and window-mirrors them
web-mobile/js/match_scoreboard.jsx Imports defaultWinMaru instead of reading window.defaultWinMaru with a fallback that gave two circles in overtime
web-mobile/js/api_client.jsx API.notePendingEdit; the pending stage counts as syncing; recordScore queues a durable running write instead of fetching it; _persistQueue merges into the stored queue, and every removal goes through _dequeue (no other _writeQueue.delete remains); a key two tabs hold keeps the newer write; _onStoredQueueChanged follows other tabs' stored writes; _parkStoredEntries; the page-hide copy of a write still being sent is dated when sent and skipped while its request is open (_heldByOpenFetch); an autosave's stamp is the time of its edit (_editAge); a monotonic age is whole milliseconds (_perfAgeMs, used by _restampFor too); _storedEntryProblem is shared with the rehydrate
web-mobile/js/admin_scoring_engi.jsx Save result / Finish + Start Next use useArmedConfirm, disarmed by a flag change or a failed save; Close and Prev/Next share one leave rule; a key at a bound writes nothing
web-mobile/css/styles.css .btn[aria-disabled="true"] looks disabled and does not press; .tsm-fusensho-why
web-mobile/js/admin_scoring_autosave.jsx The pending-edit token, released after dispatch; the unmount writes a pending edit; every write carries the tap's editedPerf; the pagehide / hidden flush, marked durable; flushPending and leaveAfterSaving removed
web-mobile/check-imports.mjs Also verifies admin_scoring_shared.jsx, bracket.jsx and match_scoreboard.jsx, which gained sibling imports
web-mobile/js/__tests__/tap_guard.test.jsx New: window edges, keyed stamps, keyboard exemption, a clock stepped back, acceptTap, swallowBounce
web-mobile/js/__tests__/helpers/tap_events.js New: pointerTap (detail 1) and keyboardClick (detail 0), shared by the three tap-guard render tests
web-mobile/js/__tests__/render/finish_arm_dwell.render.test.jsx New: all five arm sites; a double tap only arms, a later tap commits once, a tap after a clock step back commits, a keyboard click commits at once
web-mobile/js/__tests__/render/ippon_double_tap.render.test.jsx New: individual, team and kachinuki ippons, the foul +, the other side, the keyboard, a mark taken back
web-mobile/js/__tests__/render/kachinuki_done_bout_double_tap.render.test.jsx Imports TAP_BOUNCE_MS from the leaf and its taps from helpers/tap_events.js
web-mobile/js/__tests__/admin_scoring_modal.test.jsx Fusensho keeps struck points, encho, the refusal, a switch-then-undo on a reopened row, clearFusensho, applyBoutScoreEdit; the two cases that pinned the defect are changed
web-mobile/js/__tests__/result_slot.test.jsx struckIppons
web-mobile/js/__tests__/match_scoreboard.test.jsx A default-win bout in encho shows one circle with no window.defaultWinMaru set
web-mobile/js/__tests__/restamp_rule.test.jsx A fractional monotonic reading gives a whole-millisecond stamp
web-mobile/js/__tests__/render/team_fusensho_keeps_points.render.test.jsx New: the wire row after a fusensho, the undo after a remount, a corrected point keeps the fusensho and stays gone after the undo, the refusal sends nothing and names the undo first when needed
web-mobile/js/__tests__/render/autosave_debounce.render.test.jsx bc-emsl: an empty slot and a circle send nothing, a real mark still clears; Next (button and key) within the delay writes the tap, in both editors
web-mobile/js/__tests__/sync_queue.test.jsx The pending stage, two editors, the durable write persisted before any fetch answers, the in-flight count balanced; two tabs sharing the stored queue (their writes kept, a landed one pruned, a newer same-match entry kept, pruning after a failed save, an unreadable drop leaving storage); a tab writing back an older copy keeps the newer write; a revoked password scrubbed from every stored entry; a tab taking up and dropping other tabs' writes (5 cases); the page-hide copy not sent twice, removed once answered, still sent after a failure, dated when sent; the edit-time stamp, in whole milliseconds
web-mobile/js/__tests__/render/admin_scoring_engi.render.test.jsx The two-tap Save (a double tap only arms, a later tap saves once, a flag change disarms, Enter saves directly); Prev/Next (button and key) save a pending change; the three single-tap save cases now arm first; running patches carry editedPerf, and closing unmounts the editor as a host does
web-mobile/js/__tests__/render/autosave_pending_edit.render.test.jsx New: registered on the first tap, released after dispatch; the unmount writes it, then releases it; never on a scheduled match; pagehide and hidden write it at once, marked durable
web-mobile/js/__tests__/render/autosave_every_scoring_edit.render.test.jsx New: the overtime count, rep picks and the daihyosen verdict save on their own; their adopts keep a second change made before the first save comes back; an engi key at a bound writes nothing
web-mobile/js/__tests__/render/editor_every_way_out.render.test.jsx New: the unmount writes a pending tap in all three editors, carrying the tap's time; Discard writes nothing; Prev/Next ask in all three editors; the team re-seed adopts a bout moved together with the overtime count
web-mobile/js/__tests__/render/team_fusensho_refusal_clears.render.test.jsx New: a refused Fusensho's reason clears on the next bout edit and does not come back untapped
web-mobile/js/__tests__/render/{score_editor_dismiss_prompt,kachinuki_cleared_bout_reaches_server}.render.test.jsx Closing unmounts the editor as a host does, which is when the pending tap is written
web-mobile/js/__tests__/score_write_same_tick.test.jsx New: attemptScoreWrite reaches recordScore in the same tick, and editMatchScore and every host's onSubmit reach the write before their first await
scripts/screenshots/lib/editor.mjs TAP_DWELL_MS, sameSideTapPacer; finishMatch waits out the bounce, counted from the arming tap, before its confirming tap
scripts/screenshots/recipes/{editors,knockout,scored,public,videos}.mjs Same-side ippon taps and the videos' Finish paced past the bounce window; engi saves through finishMatch
docs/user-guide/court-operators/recording-decisions.md How a per-bout fusensho is undone and corrected, and when it is refused
docs/architecture/network-architecture.md The pending-edit stage, every way out of the editor, the tap-time stamp, and the outbox shared by every tab, in the client resilience table; a password reset parks every stored write
docs/user-guide/court-operators/scoring-a-match.md What closing and Prev/Next keep, and when they ask first
docs/user-guide/organisers/naginata.md Engi Save takes two taps; the armed label for both buttons
CLAUDE.md The three new owners: tap_guard.jsx, struckIppons/clearFusensho/updateSubScore/applyBoutScoreEdit, notePendingEdit and the durable flag; the rulings of 2026-09-27 (every way out saves, the tap-time stamp, Prev/Next ask as Close does, the overtime + stays unguarded); the shared outbox rules; why the tap guard keeps Date.now(); enchoOn now in result_slot.jsx

Screenshots

Court console, 1180x820 with touch emulation. "Before" is a build of main (b78acf1), "after" this branch, driven with the same CDP touch taps.

bc-dtip: two taps ~70ms apart on Aka's M. Before: M M, the bout reads as won 2-0. After: one M.

Before (main) After
Before: a double tap recorded M M for Aka After: a double tap recorded one M for Aka

bc-dtfn: two taps on Finish + Start Next. After: the button is only armed ("Tap again to finish →") and the match is still running. On main the same double tap completed the match and started the next one; that result is in the test plan, since the finished match leaves the screen.

After: a double tap on Finish + Start Next leaves it armed

bc-dtfn: the engi editor after a bounced double tap on Finish + Start Next. It is armed, not saved; three quick + taps gave three flags.

After: the engi Save armed by a double tap

bc-fsnp: Shiro kote on bout 1, then Fusensho for Aka. Before: Shiro's K is erased. After: Aka gets its two circles and Shiro keeps the K.

Before (main) After
Before: the fusensho erased Shiro's kote After: Shiro keeps the kote beside Aka's two circles

bc-fsnp: with Shiro at two points, Aka's Fusensho is refused. It is greyed out; a tap on it shows the reason under Aka's side and saves nothing.

After: Aka's refused Fusensho shows why after a tap

bc-fsnp: Shiro kote, Fusensho for Aka, then a tap on Shiro's K to remove it. The fusensho stays: Aka keeps its circles and the button stays pressed. The server holds ipponsA [○, ○], ipponsB [], decision: fusensho.

After: removing Shiro's kote keeps Aka's fusensho

bc-sync (operator ruling 2026-09-27): Next with a Mark draw not yet saved asks first, with the prompt Close already shows. Scores page, a running individual match.

After: Next with an unsaved Mark draw asks Discard unsaved scoring changes?

Not pictured, because nothing changes on screen:

  • bc-emsl: the tap now does nothing. It was verified by the network log (no PUT) and the Tie staying pressed.
  • bc-sync: the "Syncing…" pill lasts about 300ms, shorter than a screenshot round trip. It was verified by sampling the pill's text every 20ms: Synced → Syncing… at the tap → Synced when the write landed.
  • bc-sync, leaving keeps the tap, and the tabs share their writes: checked against a build of main instead (test plan).

Test plan

  • make go/test passes (lint + security scan + tests), run on the exact tree of each commit before it was made
  • New/updated unit tests cover the change. Each new test was run with its fix removed and went red: 6 unit and 3 render for bc-fsnp; 2 render for bc-emsl; all 5 arm sites for bc-dtfn; 6 render for bc-dtip; 4 separate mutations for bc-sync. The review fixes were checked the same way: 11 reverts, each red (the reopened switch-then-undo, the clock step in the guard and at all 5 arm sites, the correction and its snapshot, the refusal wording, Prev/Next in both editors and the arrow key). The follow-ups likewise: 3 reverts for the engi Prev/Next and key, 2 for the refusal tap; 3 for the engi two-tap Save (the guard, the disarm, the armed label); 4 for the in-flight save (not registered, no newer-entry check, not cleared once settled, no hidden-tab path); 6 for the shared outbox (no merge, a removal not recorded, no stamp guard, an expired or unreadable drop not recorded, the record cleared before a failed save). Review round 3: 12 for the shared outbox and the page-hide copy (the newer-write merge, the password scrub, taking up, dropping settled copies, a removed key, an older copy, the rehydrate's checks, the open-request skip, the copy's removal once answered, its send date, the edit stamp, the retry after a failure); 2 for the host chain; 1 for the import gate; 1 for the scoreboard's overtime circle; 9 for the autosave gaps (overtime in both editors, the rep pickers, the engi bound keys, the refusal note twice, the two echo races, the per-side rep adopt); 3 for the daihyosen verdict; 7 for leaving the editor (the unmount write, the tap time, Discard, Prev/Next in the three editors, the re-seed); 1 for whole milliseconds
  • Manual browser verification at 1180x820 with touch emulation, on /admin/shiaijo/:court, with real CDP touch taps. A build of main was the control for the double taps, the fusensho and the reload.
    • Individual: two taps ~70ms apart on Aka M give one M (main: M M)
    • Two taps on Finish + Start Next only arm it (main: completed and started the next match); a tap after the dwell commits
    • Sync pill: "Synced" → "Syncing…" at the tap → "Synced" when the write lands (sampled every 20ms)
    • A point tapped 40ms before a reload is on the server after it (main: lost)
    • Team: Shiro K + Fusensho for Aka is stored as ipponsA [○, ○], ipponsB [K] (main: Shiro's K erased); after a reload the undo leaves ipponsB [K] and no circles
    • With Shiro at two points, Aka's Fusensho is greyed out; a touch tap on it shows "SHIRO already won this bout: clear their points first" under Aka and sends no write
    • Engi: three quick + taps give three flags; a bounced double tap on Finish + Start Next only arms it (nothing saved); a later tap saves 3-0
    • Two tabs: a write another tab stored stays beside this tab's page-hide save, and only this tab's entry leaves storage once it lands (server holds the point)
    • Team: Shiro K, Fusensho for Aka, then a tap on Shiro's K: the fusensho stays, stored as ipponsA [○, ○], ipponsB []
    • A Tie, then a tap on an empty slot: no PUT, the Tie stays
    • Team, court view: a K tapped about 50-70ms before switching court is on the server after a reload (main: lost). This check also caught the fractional-stamp 400, fixed and checked again with no 400 since
    • Two tabs: a Finish queued offline in a tab that is then closed is sent by the tab left open when the connection returns, and the match reads finished there (main: the Finish stays in storage, unsent)
    • Scores page, a running individual match: Next with a Mark draw not yet saved asks "Discard unsaved scoring changes?"; Cancel stays on the match
  • Screenshots added above
  • Docs updated under docs/ (make docs/build passes)
  • Docs screenshots and videos recaptured with SINCE=main: all 31 screenshots unchanged, the videos complete; none changed. After the engi Save became two taps, swiss-standings-engi recaptured unchanged through it. Recaptured again after review round 3 (make docs/media SINCE=main): 31 unchanged, 0 changed, the 3 videos complete. The page errors it reports are the lineup 404s on the team and kachinuki surfaces, which main logs too (bc-k404)
  • No new console errors or warnings. The errors seen are the lineup 404s for teams with no lineup set, which main shows too (tracked as bc-k404), the network errors the offline check and a server restart cause on purpose, and the one 400 the fractional stamp caused before its fix

Closes bc-dtfn
Closes bc-dtip
Closes bc-fsnp
Closes bc-emsl
Closes bc-sync

🤖 Generated with Claude Code

gitrgoliveira and others added 14 commits September 27, 2026 00:06
bc-sync. The score editors hold each edit for a 300ms autosave debounce, and
nothing outside the editor knew about that stage: the sync pill read "Synced"
over it, and a reload inside the window cancelled the timer and lost the edit.

- api_client: API.notePendingEdit adds the pending stage to the sync status
  (still 'syncing', no new value); API.runDurably makes a running score write
  go straight into the persisted outbox instead of a fetch the browser would
  cancel with the page.
- useDebouncedRunningWrite registers the pending edit from the first tap and
  releases it only once the write is dispatched, so the status never reads
  "synced" in between. On pagehide or the tab being hidden it writes a
  pending edit through runDurably. Unmount still cancels.
- runDurably works only because every host reaches API.recordScore with no
  await in front of it; pinned at attemptScoreWrite.

Browser, 1180x820 with touch: the pill reads "Syncing…" from the tap until
the write lands, and a point tapped 40ms before a reload is on the server
after it. The same sequence on a main build loses it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
bc-dtfn, bc-dtip. A bouncing thumb delivers two complete clicks 16-44ms
apart, and Preact renders between them, so the second one passed the
two-tap Finish / End match guard (finishing the bout and, with Finish +
Start Next, starting the next) and recorded a second ippon or foul.
web-mobile/js/tap_guard.jsx is now the one owner of "is this pointer tap the
bounce of the previous one" (TAP_BOUNCE_MS 400; a keyboard click, detail 0,
never is). The five arm sites use useArmedConfirm; the ippon and foul "+"
buttons guard per bout side; bc-kbrw's bout-list guard moves onto the leaf.
The capture harness now waits TAP_DWELL_MS between such taps.

bc-fsnp. A per-bout fusensho replaced the scoreline, erasing the points the
other side had struck (FIK Art. 32 keeps them), and after a reload its undo
left the two default-win circles behind as ordinary points. It now keeps the
other side's struck points (struckIppons, the JS twin of engine.struckIppons),
every other edit on the row ends it through clearFusensho, and a fusensho
against a side that already won the bout is refused, since circles against
two points are a 2-2 the server rejects.

bc-emsl. A tap on an empty mark slot, or on a default-win circle, went
through setPts and cleared the bout's Tie or Fusensho with a fresh autosave.
It is now inert, as in the individual editor.

Browser, 1180x820 with touch, against a main build as control: two taps
~70ms apart give one M (main: M M) and only arm Finish (main: finished and
started the next match); the fusensho row keeps Shiro's K and its undo after
a reload leaves just the K; an empty-slot tap sends nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From a /simplify pass (reuse, simplification, efficiency, altitude). No
behaviour change; every reworked mechanism was re-checked by removing it and
watching its tests go red.

- bc-sync: the pagehide flush now marks its write `durable: true` on the
  patch instead of setting a module-level depth counter around the call
  (API.runDurably and _durableDepth are gone). recordScore reads the flag
  from the patch, never sends it, and no longer touches the in-flight count
  for a queued write. The hook drops a ref it did not need.
- bc-dtip / bc-dtfn: tap_guard.jsx gains acceptTap, the check-then-stamp
  every guarded button ran by hand at four sites; the team rows compute
  their tap key once; useArmedConfirm keeps one ref instead of two.
- bc-fsnp: the team editor's updateSubScore owns "a scoring edit ends the
  fusensho and the draw, clear applied last", which six setters and the
  keyboard waza path used to spell out each. setFusenshoFor loses a check
  its disabled button made unreachable, and applyFusenshoToggle calls
  defaultWinMaru directly instead of through window with a dead fallback.
- api_client: notePendingEdit sits after unsentWrites, whose doc comment it
  had split off.
- Tests: pointerTap/keyboardClick live in __tests__/helpers/tap_events.js.
- Screenshot harness: finishMatch counts the wait for the arm label towards
  the bounce window, and editors.mjs no longer waits after its last tap.

Browser (1180x820, touch): a double tap still records one M; the pill still
reads Syncing… from the tap until the write lands; a point tapped 40ms before
a reload is on the server after it; correcting a point on a fusensho row
leaves no circles. make docs/screenshots SINCE=main: 31 unchanged.

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

Review findings on this PR:

- Undoing a switched fusensho on a bout reopened from saved state no
  longer restores the default-win circles as ordinary points: the base
  of a row with no snapshot is the row without its circles.
- Taking a mark off the side a fusensho went against is a correction and
  keeps the fusensho (applyBoutScoreEdit); the snapshot its undo restores
  follows the edit. Adding a point, a foul or a winner-side edit still
  ends it.
- Prev/Next (buttons and arrow keys) save an edit still inside the
  autosave window before they unmount the editor, as closing does.
- A wall clock stepped back is never read as a bounce, in one helper
  shared by the tap guard and the armed confirm.
- A refused Fusensho names the undo first when the side that won the
  bout holds the fusensho itself, and reads the other side's label.
- The scoreboard's fallback circles come from DEFAULT_WIN_IPPON rather
  than a hand-typed literal.

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

- The engi flag "+" buttons guard per side through acceptTap: a bounced
  double tap adds one flag, as the ippon and foul "+" already do. The a/s
  keys are never a bounce. The screenshot harness waits TAP_DWELL_MS
  between same-side flag taps.
- The engi editor's Prev/Next (buttons and arrow keys) save flags still
  inside the autosave window before they unmount it, as the individual and
  team editors now do.
- A refused per-bout Fusensho is aria-disabled instead of disabled: it
  still looks greyed out, and a tap shows the reason under that side. A
  title alone never shows on a touchscreen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The persisted outbox (bc_write_queue) is shared by every tab of the app,
but each save wrote the tab's own queue over it, and an emptied queue
removed the key. A result queued in one tab was dropped from storage when
another tab saved or finished its writes, and lost if the first tab then
closed. A page hide now queues a write whenever an edit is pending, so
this happens far more often than it used to.

_persistQueue now merges into what storage holds: this tab's entries are
written over it, the entries it settled since the last save are taken
out, and everything else is left alone. Every removal goes through
_dequeue, which records it; a settled key is pruned only when the stored
entry is not newer than the one removed, so another tab's later write for
the same match stays. Entries dropped on load (expired or unreadable) are
recorded too, so they are announced once. clearQueue stays a hard wipe.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s Prev/Next

The individual, team and engi editors each carried the same save-then-go
closure; the shared autosave hook now owns it, with the running check it
already holds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… survives a page close

Operator rulings 2026-09-27:

- The editors behave alike: the engi editor's Save result / Finish + Start
  Next is the same two-tap guard as Finish (useArmedConfirm). A bounced
  double tap only arms it, so it can no longer save AND start the next
  match. A flag change disarms it; a correction and keyboard Enter save
  directly, as in the other editors. The capture harness saves engi through
  finishMatch.
- The engi flag "+" is not an ippon call, so it has no bounce guard: the
  guard from d4f422f is removed, with its tests and the recipe pacing.
  Removal taps (a scored mark, the engi "-") stay unguarded too. CLAUDE.md
  records both so neither guard is added back.
- A running write already sent but not yet answered when the page goes
  away (pagehide, or the tab hidden) goes into the persisted outbox, as a
  failed fetch would put it: the page going away cancels that request. A
  queued entry for the match added after the write was sent is newer and
  is kept. The server takes an equal rev again and drops a lower one, so a
  copy that did land is harmless.

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

Review round 3 of PR 450, with the operator's rulings of 2026-09-27.

- An autosave is stamped with the time of the edit it carries
  (editedPerf), and the editor's unmount writes a pending edit, whatever
  caused it. Discard cancels first. A tap made before another device
  finished the match is older than that finish, so it cannot reopen it.
  Close and Prev/Next share one leave rule: Prev/Next ask before dropping
  a Mark draw or an unsaved correction.
- Every running-match edit arms the autosave. The overtime counter, the
  rep-player pickers and the team daihyosen verdict did not. Their adopts
  keep the operator's value until the server holds it.
- The stored outbox keeps the newer write per match. A tab takes up the
  writes other tabs store and drops the ones they settled. A revoked
  password is scrubbed from every stored entry.
- The page-hide copy of a running write still being sent is dated when
  it was sent, and is not sent twice while its request is open.
- A timestamp built from a monotonic age is whole milliseconds: the
  server refused a fractional one with a 400 (the edit stamp, and the
  existing clock-skew re-stamp).
- enchoOn and defaultWinMaru move to result_slot.jsx, so the scoreboard's
  default-win count no longer depends on a window global.
- A refused Fusensho's explanation clears on the next bout edit. An engi
  key at a bound writes nothing. The team re-seed reads the previous
  board when the adopt fires, not inside the updater.
- Tests pin the whole host chain before the first await.
- Docs: the shared outbox, closing and Prev/Next, the engi armed label.

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

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

A bounce on the EnchoControl stepper's + saves one period too many. The
count shows only as xN in that editor row: everything else reads only
whether overtime happened (enchoOn), so no result, bracket, board or
export changes, and one tap on - takes it back. Listed beside the engi
flag + and the removal taps so a later review does not re-raise it.
Kachinuki bout mode's Encho button stays an open question on bc-kheb.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@gitrgoliveira
gitrgoliveira merged commit e7cb3af into main Sep 27, 2026
13 of 14 checks passed
@gitrgoliveira
gitrgoliveira deleted the bc-p2-scoring-fixes branch September 27, 2026 13:34
gitrgoliveira added a commit that referenced this pull request Oct 7, 2026
…nl's

Main now carries #447, #450 and #453. Their beads' fixmes were run unskipped
on the merged build:

- Plain tests now, passing as written: bc-kfup, bc-htcr, bc-plcl (individual),
  bc-fsnp, bc-dhas (team), bc-emsl, bc-kheb, bc-sync (kachinuki).
- bc-offl: the court header reads "Offline: 1 result not sent" while the
  Finish is queued (captured offline); the test looked for "Not saved yet".
- bc-sbq: #447 made Send back to queue keep the score, and the confirm now
  says "Any score entered is kept". The test asserts that, then starts the
  bout again and finds its point still on the board.
- bc-kpnl's two tests are removed: the panel they wait for was taken out by
  the 2026-09-26 ruling (recording a withdrawal changes only that match).

Follow-on edits:
- The two 800 ms waits that kept bc-dhas from erasing the daihyosen row are
  gone (J7 and bc-dhrp); bc-dhas's own test proves the fix.
- bc-fsnp waits for the sync pill to read Synced instead of 1.5 s: since
  bc-sync the pill reads Syncing from the first tap.
- J5 final plays on from the first unfought bout: bc-kheb, which runs before
  it in this serial file, records the final's bout 1.

Five fixmes remain, all on open beads: bc-otpl, bc-kosc, bc-crpn, bc-lprf,
bc-dhrp. make e2e: 28 passed, 5 skipped, EXIT 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
gitrgoliveira added a commit that referenced this pull request Oct 8, 2026
…-crpn are plain tests

Main merged #462 (bc-dsid, bc-otpl, bc-htsd, bc-dlck, bc-kosc, bc-crpn).
The journeys now walk what an operator meets on that build.

- bc-crpn: Correct is refused at the court console while a bout is live.
  J1, J2, J4 and J5 record the refusal (the row notice and the toast), then
  take the route the notice names: send the live bout back to the queue,
  correct, start it again. J5's court-busy panel is reached from the Scores
  tab, where a reopen can still meet a busy court, with its V1 and V4
  variants kept. The bc-crpn test asserts the new refusal: Start match while
  a correction is open names the correction and starts nothing.
- bc-dsid: the withdrawal and no-show prompt picks no side and Record is off
  until one is picked. The hasty variants record that, then the operator
  picks; fixtures/individual.mjs recordDecision now takes the side.
- bc-htsd: the individual hantei buttons pick the side and Finish records it.
- bc-dlck: the re-recorded kiken asserts no "Proceed anyway?" confirm shows.
- bc-otpl and bc-kosc pass as written and are plain tests.
- J5's M1 points won read 7-3 and then 6-3: bout 5's double tap scores one M
  since bc-dtip (#450). The old 8-3 and 7-3 counted the bounce's second M.
- A banned phrase about the outcome of a withdrawal is gone from three audit
  labels, a J3 step title and the bc-kfup test title.

One fixme left, on an open bead: bc-dhrp, which still fails at its own
assertion. make e2e: 32 passed, 1 skipped, EXIT 0 (kachinuki re-run after
the points fix: 11 passed). make go/test: EXIT 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant