Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
6347560
fix(bc-aadv): auto-advance keeps a refused start on the Up next card
gitrgoliveira Oct 8, 2026
e8e52ce
fix(bc-tmwn): the Completed list names the winner
gitrgoliveira Oct 8, 2026
67143a5
docs(bc-aadv): the sentence names Finish + Start Next, not Finish
gitrgoliveira Oct 8, 2026
ceebcf5
fix(bc-tmwn): the viewer's winner tick rides the name row
gitrgoliveira Oct 9, 2026
057ed37
fix(bc-rrdr): a drawn bout appears in Recent results
gitrgoliveira Oct 9, 2026
a10944c
fix(bc-cdbl): one announcement per court call, withdrawn when the mat…
gitrgoliveira Oct 9, 2026
08974f9
fix(bc-dhrp): pick each team's representative on the daihyosen row
gitrgoliveira Oct 9, 2026
a3306ca
docs(bc-dhrp): the read-only representative row comments name every case
gitrgoliveira Oct 9, 2026
377e803
test(bc-tmwn): the truncation pin reads past the winner tick's slot
gitrgoliveira Oct 9, 2026
7302c96
docs: recapture the three screenshots this change alters
gitrgoliveira Oct 9, 2026
f38a158
docs: recapture realtime-update, whose Recent results now carry the tick
gitrgoliveira Oct 9, 2026
a2bc884
Merge branch 'main' into worktree-bridge-cse_013NiW6kth4REntdNv1jb7pt
gitrgoliveira Oct 9, 2026
90a6a5e
build(deps): update Go to 1.27.2 and golang.org/x/net to v0.60.0
gitrgoliveira Oct 9, 2026
c36e64b
refactor: polish the PR #463 fixes after review
gitrgoliveira Oct 9, 2026
14ea658
Merge remote-tracking branch 'origin/main' into worktree-bridge-cse_0…
gitrgoliveira Oct 9, 2026
79ed8e1
fix: address the PR #463 code review
gitrgoliveira Oct 9, 2026
fce5866
fix: refuse a queue Start while another start is out, before the defer
gitrgoliveira Oct 9, 2026
b30edc2
fix: address code review round 2 on PR #463
gitrgoliveira Oct 9, 2026
e870350
fix: address code review round 3 on PR #463
gitrgoliveira Oct 9, 2026
5b297dc
fix: address code review round 4 on PR #463
gitrgoliveira Oct 9, 2026
7bf0c9e
fix: address code review round 5 on PR #463
gitrgoliveira Oct 9, 2026
195e5a3
fix: address code review round 6 on PR #463
gitrgoliveira Oct 9, 2026
1800345
fix: address code review round 7 on PR #463
gitrgoliveira Oct 9, 2026
cabf470
fix: address code review round 8 on PR #463
gitrgoliveira Oct 9, 2026
aa0fb3b
fix: polish after code review round 9 on PR #463
gitrgoliveira Oct 10, 2026
0c2aaa4
fix: order each representative pick on its own (PR #463 round 10)
gitrgoliveira Oct 10, 2026
0329bda
chore: drop the dead repPicks stamp migration; correct the re-seat note
gitrgoliveira Oct 10, 2026
899e614
fix: a representative pick no longer re-sends the bout's score
gitrgoliveira Oct 10, 2026
f2d3c01
fix: close the detected gaps, wave 1 (PR #463)
gitrgoliveira Oct 10, 2026
94af3bd
fix: close the detected gaps, wave 2 (PR #463)
gitrgoliveira Oct 10, 2026
7180e74
refactor: one owner for the representative-pick snapshot; CLAUDE.md r…
gitrgoliveira Oct 10, 2026
9414c4b
fix: close the session's remaining review items (PR #463)
gitrgoliveira Oct 10, 2026
419331c
fix: final review round on PR #463 (Claude and Antigravity)
gitrgoliveira Oct 10, 2026
5a38536
fix: an announcement already taken down is not a failed withdrawal
gitrgoliveira Oct 10, 2026
a3f28c6
build(deps): pin excelize to the master commit that fixes its 15 advi…
gitrgoliveira Oct 10, 2026
5219f2b
fix: judge who a write seats on what it lands (PR #463 round 16)
gitrgoliveira Oct 10, 2026
87c0498
fix: polish after the round-16 review (PR #463 round 17)
gitrgoliveira Oct 10, 2026
a1d2205
fix: a superseded start is a refused start (PR #463 round 18)
gitrgoliveira Oct 10, 2026
a9b8946
docs(CLAUDE.md): every member id the write lands unasked is a stored one
gitrgoliveira Oct 10, 2026
5b0c7d4
fix: an echo of another device's start is a start that went out (PR #…
gitrgoliveira Oct 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v7
with:
go-version: "1.27.1"
go-version: "1.27.2"
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v7
with:
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/validate.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v7
with:
go-version: "1.27.1"
go-version: "1.27.2"

- name: Init project
run: |
Expand Down Expand Up @@ -62,7 +62,7 @@ jobs:
# (the Makefile's pin, used by `make go/lint` locally) did not.
# Keep this in lockstep with the `go install` version in
# Makefile's go/lint target so a local green run predicts CI.
version: v2.13.1
version: v2.14.0

# ____ _
# | _ \ ___ ___| | _____ _ __
Expand Down Expand Up @@ -111,7 +111,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v7
with:
go-version: "1.27.1"
go-version: "1.27.2"
cache: true

- name: Init project
Expand Down Expand Up @@ -193,7 +193,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v7
with:
go-version: "1.27.1"
go-version: "1.27.2"
cache: true

- name: Install LibreOffice headless
Expand Down
34 changes: 19 additions & 15 deletions CLAUDE.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Build stage
ARG GO_VERSION=1.27.1
ARG GO_VERSION=1.27.2

# Build stage
FROM golang:${GO_VERSION} AS builder
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile.mobile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Build stage
ARG GO_VERSION=1.27.1
ARG GO_VERSION=1.27.2

# Build stage
FROM golang:${GO_VERSION} AS builder
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile.mobile-pdf
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
# libreoffice-nogui). The Go binary's LocateSoffice() probes $PATH first, so no
# additional env vars are required.

ARG GO_VERSION=1.27.1
ARG GO_VERSION=1.27.2

# ---------------------------------------------------------------------------
# Build stage, identical to Dockerfile.mobile
Expand Down
2 changes: 1 addition & 1 deletion GEMINI.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ On tree and knockout brackets, the player/team on the top of the bracket is alwa
## Building and Running

### Prerequisites
- Go 1.27.1+
- Go 1.27.2+
- Make

### Key Commands
Expand Down
12 changes: 9 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ BIN_NAME := bracket-creator
GH_REPOSITORY ?= gitrgoliveira/bracket-creator
IMAGE_NAME := ghcr.io/$(GH_REPOSITORY)
BIN_PATH := ./bin
GO_VERSION := 1.27.1
GO_VERSION := 1.27.2
GO_SOURCES := $(shell find . -name "*.go" -type f)
EMBEDDED_ASSETS := $(shell find ./web ./web-mobile -type f 2>/dev/null)

Expand Down Expand Up @@ -51,8 +51,14 @@ local/deps: hooks/install js/deps ## Install project dependencies
# golangci-lint is lower than the targeted Go version"). `go install pkg@ver`
# runs outside this module, so it would otherwise use whatever `go` is on
# PATH rather than the toolchain go.mod asks for.
GOTOOLCHAIN=go$(GO_VERSION) go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.13.1
GOTOOLCHAIN=go$(GO_VERSION) go install github.com/securego/gosec/v2/cmd/gosec@v2.29.0
# The version also matters on its own: each analyser reads the standard
# library's export data through its bundled golang.org/x/tools, and go1.27.2
# writes a newer export-data version than x/tools v0.49 can read ("export
# data version 5 is greater than maximum supported version 4"). golangci-lint
# v2.14.0 bundles x/tools v0.50. No gosec release does yet, so gosec is pinned
# to the master commit that does (x/tools v0.51); move it to the next tag.
GOTOOLCHAIN=go$(GO_VERSION) go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.14.0
GOTOOLCHAIN=go$(GO_VERSION) go install github.com/securego/gosec/v2/cmd/gosec@v2.29.1-0.20261009120814-7b1b5cebe007
GOTOOLCHAIN=go$(GO_VERSION) go install golang.org/x/vuln/cmd/govulncheck@latest
python3 -m pip install -r docs/requirements.txt

Expand Down
23 changes: 20 additions & 3 deletions docs/architecture/data-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -383,7 +383,23 @@ That applies one level further down as well. A team bout carries its two fighter
MEMBER ids beside their names, and the winner's id beside the winner's name. Without them
a bout between two people who happen to share a display name could not be attributed at
all, because the name identifies neither of them, and competitors are allowed to share
one.
one. The representative bout is the one row whose sides are the TEAM names: it keeps them
and carries the two representatives' member ids beside them, the members picked for each
side. Each side's id is a change of its own, ordered apart from the other side's and from
the bout's score and result: a point struck on the representative bout never alters who was
picked, a pick never alters the score, and a pick for one side never alters the other side's
pick. So two captains picking at the same moment both land, and so do two boards doing one
each. The winner's member id
stays with the bout, and after a merge it always names one of the two picked members (or is
empty): when a pick changes under it, it is worked out again from the side that won. A pick
is a member of the team seated on its side, so when a knockout match is given another team on
a side (a winner corrected upstream, a slot taken back to its placeholder, a qualifier
repainted), that side's pick is cleared and dated at that moment, later than the pick had
been. The write that made the old pick, replayed from a queue, is then kept in the match's
history instead of seating the old team's member on the new team. A side whose team does not
change (the same winner stored again, or the team only renamed) keeps its pick. A side taken
back to its placeholder loses its pick, even when the match that feeds it is finished again
with the same winner: the pick is picked again.

Those are two different namespaces, and the field names keep them apart on purpose. A
match side carries a PARTICIPANT id, which names a competitor or a whole team; a bout side
Expand Down Expand Up @@ -575,8 +591,9 @@ sequenceDiagram

**Concurrent writes are merged, never dropped.** A match result is split into groups of
fields that change together: the scoreline, the verdict (status, winner, decision and the
notes that describe it), the overtime, the engi flags, the representative players, and each
bout of a team match on its own. A write names the groups it changes, and each group records
notes that describe it), the overtime, the engi flags, the representative players, each side's
fighter picked for a representative bout (one group per side), and each bout of a team match on
its own. A write names the groups it changes, and each group records
the time of the last change applied to it. A change applies only when it was made no earlier
than the stored change to the same group, so two operators changing different things (one
records the overtime while another enters a point, or two bouts of a team match are scored on
Expand Down
2 changes: 1 addition & 1 deletion docs/architecture/network-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ Key client mechanisms (all in `web-mobile/js/api_client.jsx` + consumers):
| Half-open / stalled sockets | 12s write timeouts, 35s SSE silence watchdog (armed at connect, not only `onopen`) |
| Reconnect storms | exponential backoff + jitter (vs. a fixed delay) |
| Lost writes | durable outbox persisted to `localStorage`, retried until it lands, with no age limit; survives tab refresh. Each pending write is its own entry, and each is sent on its own. A flush pass sends the entries in the order they were made (their `modifiedAt`), but one that does not land (offline, or a 5xx that keeps coming back) holds back nothing after it, and a new write goes straight to the server whatever is queued, so holding it back would gain nothing (refer to **Order of writes** in this table) and would let one failing write stop every write after it. A later running update never takes the place of a queued Finish or decision, and a decision and a score for the same match are both kept; only a score write folds into the score write queued just before it (a running update into a running update, a Finish into either, as when the editor re-sends a held Finish), naming every group either changed. A lineup save is the other write that folds: it takes the place of a queued save of the same lineup, because a lineup save restates the whole lineup and carries no time the server could order it by. A refusal on replay is always reported, never taken as success. Every tab of the app shares the stored outbox. A tab saving its queue merges into it: it adds its own writes, keeps the newer copy where two tabs hold the same entry, and removes only the writes it settled. Each tab also takes up the writes another tab stores, so a write queued in a tab that then closes is still sent by a tab left open. A write two tabs hold can be sent by both, which is harmless, because the server orders writes by their stamps (refer to **Order of writes** in this table) |
| Order of writes | nothing a device sends is thrown away, and changes are put in the order they were made (the write's `modifiedAt`), never the order they arrive. A device stamps a write with its estimate of the server's time, and never earlier than the match it was made against: two devices' estimates can differ by up to the 5 s the server accepts, so a change made a moment after a write stamped by a device running ahead would otherwise read as older than the write it changed. The stamp is therefore the later of the device's time and the stamp of the match as the operator saw it plus 1 ms (for an autosave, as it was at the tap), never more than 4 s past the device's time. A score write names the groups of the match it changes (`changed`: `points`, `result`, `encho`, `flags`, `rep`, or one bout row, `bout:<position>`). The score editor works this out by comparing what it sends with the match it was showing. The server applies each named group only if the write is not older than the last change applied to that group, and never touches a group the write does not name, so two devices changing different things both land whatever order they arrive in. A named group a newer change outranks is not applied: its value is kept in the match's history (`GET /api/competitions/:id/matches/:mid/history`), which the score editors show under **History**. A write applied in part answers with `heldGroups`; one none of which applied answers `{"applied": false, "reason": "superseded"}` with the same list, and the editor says "Not applied" rather than "Not saved". A change that would leave a finished match without the winner it needs (a knockout match left tied, an engi match with no valid flag count) is not applied either: the match keeps its recorded result, the change is kept in the history, and the answer adds `heldReason: "needs_winner"`, on which the editor tells the operator to correct the result with a winner. A change from a board still scoring a match, or its overtime, that a kiken, fusenpai, or fusensho (a withdrawal, or a win recorded for a competitor barred elsewhere) already closed is held as well: that decision stands, and the history gives the reason. The other arrival order is answered applied: an older finish that arrives after a newer scoring change which would leave it without a winner is recorded, that later change is moved to the history (an entry of its own, at its own time), and the answer names it in `displacedGroups` with the same `heldReason` and no `heldGroups`; the editor says "Saved" and what was moved, and a held finish replayed this way raises one information alert. Where the outbox folds a queued running update into a later one, the later one names every group either changed. The same holds for the writes that are not score sheets: an older save the same device had already followed with a newer one (an out-of-order delivery after a reconnect) is kept whole in the history as an "older revision of this board"; a winner set by hand that a newer result outranks is kept in the history with the winner it named, and answered like a held score; an engi result is ordered by when it was saved, its flags and the winner they decide kept together. A queued decision whose first send landed but whose answer was lost is answered as recorded when it is sent again |
| Order of writes | nothing a device sends is thrown away, and changes are put in the order they were made (the write's `modifiedAt`), never the order they arrive. A device stamps a write with its estimate of the server's time, and never earlier than the match it was made against: two devices' estimates can differ by up to the 5 s the server accepts, so a change made a moment after a write stamped by a device running ahead would otherwise read as older than the write it changed. The stamp is therefore the later of the device's time and the stamp of the match as the operator saw it plus 1 ms (for an autosave, as it was at the tap), never more than 4 s past the device's time. A score write names the groups of the match it changes (`changed`: `points`, `result`, `encho`, `flags`, `rep`, each side's representative pick, `repPickA` and `repPickB`, or one bout row, `bout:<position>`). The score editor works this out by comparing what it sends with the match it was showing. The server applies each named group only if the write is not older than the last change applied to that group, and never touches a group the write does not name, so two devices changing different things both land whatever order they arrive in. A named group a newer change outranks is not applied: its value is kept in the match's history (`GET /api/competitions/:id/matches/:mid/history`), which the score editors show under **History**. A write applied in part answers with `heldGroups`; one none of which applied answers `{"applied": false, "reason": "superseded"}` with the same list, and the editor says "Not applied" rather than "Not saved". A change that would leave a finished match without the winner it needs (a knockout match left tied, an engi match with no valid flag count) is not applied either: the match keeps its recorded result, the change is kept in the history, and the answer adds `heldReason: "needs_winner"`, on which the editor tells the operator to correct the result with a winner. A change from a board still scoring a match, or its overtime, that a kiken, fusenpai, or fusensho (a withdrawal, or a win recorded for a competitor barred elsewhere) already closed is held as well: that decision stands, and the history gives the reason. The other arrival order is answered applied: an older finish that arrives after a newer scoring change which would leave it without a winner is recorded, that later change is moved to the history (an entry of its own, at its own time), and the answer names it in `displacedGroups` with `heldReason: "needs_winner"` and no `heldGroups`; the editor says "Saved" and what was moved, and a held finish replayed this way raises one information alert. A representative pick made after its representative bout was removed is moved to the history the same way when the removal arrives, and that answer names it in `displacedGroups` with no `heldReason`. Where the outbox folds a queued running update into a later one, the later one names every group either changed. The same holds for the writes that are not score sheets: an older save the same device had already followed with a newer one (an out-of-order delivery after a reconnect) is kept whole in the history as an "older revision of this board"; a winner set by hand that a newer result outranks is kept in the history with the winner it named, and answered like a held score; an engi result is ordered by when it was saved, its flags and the winner they decide kept together. A queued decision whose first send landed but whose answer was lost is answered as recorded when it is sent again |
| Edits not yet sent | a score editor holds each edit for a short autosave window (300ms) before it writes it. The sync pill reads "Syncing…" from the first tap, not "Synced". If the editor goes away inside that window (closed, Prev/Next, another match picked, a court switch), the edit is written at once. If the tab is hidden or unloaded inside it, the edit is written straight into the outbox, so a reload does not lose it. An autosave carries the time of the tap, not the time it is sent, so it never outranks a result another device recorded after the tap. A running write already sent but not yet answered is also kept when the page goes away, which cancels its request; while the request is still open, that copy is not sent |
| Server errors (5xx / 429), and 403 | write stays queued and keeps retrying until it lands, holding back no other write; after 10 consecutive rejections the operator gets a notice and the sync pill shows "Not saving", and the match's score editor offers **Discard held result**, which discards that write and nothing else after a confirm. The held-writes count in the admin topbar opens a list of every held write (score updates, results, decisions, winners set by hand, lineups), where any write that keeps failing can be discarded the same way, one at a time; a write only waiting for the connection cannot be discarded there. On this server 403 is never a bad credential (that is 401): it means the tournament is not configured yet, or is missing its password. Only an admin fixing the server state clears it, so signing in again cannot help and the write keeps retrying instead of parking |
| Invalid credential (401) | the one 4xx a retry can fix: the write is parked, not discarded, stops retrying, and shows "Sign in to save"; signing in again re-sends it with the new credential, together with every other queued write still carrying the refused password (one queued after it, before its own 401 came back), so one sign-in is enough. A write queued without a password (a competitor's in a self-run event) is never given one |
Expand Down
2 changes: 1 addition & 1 deletion docs/dev-guide/contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ By participating in this project, you agree to abide by our

Prerequisites:

- [Go 1.27.1+](https://golang.org/doc/install)
- [Go 1.27.2+](https://golang.org/doc/install)

Other things you might need to run the tests:

Expand Down
Binary file modified docs/screenshots/console-correct-completed.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/screenshots/realtime-update.webm
Binary file not shown.
Binary file modified docs/screenshots/viewer-competition.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/screenshots/viewer-result-encho.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion docs/user-guide/commands/version.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@ bracket-creator - v1.1.0

Git Commit: ee8538fa83f36ccf9910fe6fc0c42e56afaf07ca
Build date: 2026-09-04 13:14:21 +0100
Go version: go1.27.1
Go version: go1.27.2
OS / Arch: darwin arm64
```
2 changes: 2 additions & 0 deletions docs/user-guide/court-operators/recording-decisions.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ A tie that does not affect advancement is left as a shared rank with no extra bo

The bout is a single-point ippon-shobu with no time limit, between one representative from each tied team. Because it runs until someone scores, a daihyosen has no encho. The score editor lets you pick each team's representative from its roster. On the court console, the bout appears with a **(DH)** mark in the centre of the score. In pool and league standings, the team that won its daihyosen carries a **DH** badge.

When a knockout team encounter is settled by a representative bout, the results workbook (**Export & print**, then **Download results (.xlsx)**) lists it on a **Representative Bouts** sheet, one section for each such encounter. A section is titled as that encounter is on the **Elimination Matches** sheet, followed by (Representative bout), with White (Shiro) on the left and Red (Aka) on the right. The bout's row shows **DH** where a numbered bout shows its number, then each team's representative with their number and name, the score, **(DH)** in the centre column, and the result mark (Ht, Kiken or Fus.) beside the side it names. A side whose representative was not picked shows no name. The sheet appears only when at least one encounter had a representative bout. The tie-break daihyosen of a team pool or league is not on it.

## Chusen (drawing lots)

Chusen is the last resort when two or more tied teams have played a round of daihyosen and the bouts still do not produce a strict order. Examples are a cycle where each team beats another, an all-drawn round, or two teams finishing level on daihyosen wins.
Expand Down
Loading
Loading