Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
c4c1a7e
feat(bc-e2eh): scripts/e2e Playwright harness with UI-only fixtures a…
gitrgoliveira Sep 23, 2026
2ace9f5
fix(bc-e2eh): let concurrent e2e runs use separate results folders
gitrgoliveira Sep 23, 2026
d12ad9c
feat(mp-yqxn.2): knockout/mixed operator journeys (individual, fixed-…
gitrgoliveira Sep 24, 2026
4aeda72
test(mp-yqxn.2): assert a coarse pointer before every tap-target meas…
gitrgoliveira Sep 24, 2026
b7182b6
test(mp-yqxn.2): name each journey fixme by its filed bead
gitrgoliveira Sep 24, 2026
548a936
test(mp-yqxn.2): encode the every-bout-is-fought ruling as bc-tmfn
gitrgoliveira Sep 24, 2026
bae2b33
test(mp-yqxn.2): say team match, not fixed-order (operator ruling)
gitrgoliveira Sep 24, 2026
c366c50
Merge remote-tracking branch 'origin/main' into bc-e2eh-operator-review
gitrgoliveira Sep 24, 2026
4487d89
Merge remote-tracking branch 'origin/main' into bc-e2eh-operator-review
gitrgoliveira Sep 25, 2026
afc2d2f
test(mp-yqxn.2): follow main's #444 in the journeys; switch on bc-tmf…
gitrgoliveira Sep 25, 2026
31a1c66
test(mp-yqxn.2): follow PR #458's lineup UI and tap guard in the e2e …
gitrgoliveira Oct 6, 2026
00b507b
Merge remote-tracking branch 'origin/main' into bc-e2eh-operator-review
gitrgoliveira Oct 7, 2026
8398418
test(mp-yqxn.2): switch on the fixmes main's fixes closed; drop bc-kp…
gitrgoliveira Oct 7, 2026
d2e7d93
Merge branch 'main' into bc-e2eh-operator-review
gitrgoliveira Oct 7, 2026
0427335
test(mp-yqxn.2): bc-lprf's panel refusal is a plain test
gitrgoliveira Oct 7, 2026
f308676
Merge remote-tracking branch 'origin/bc-e2eh-operator-review' into bc…
gitrgoliveira Oct 7, 2026
ad32243
Merge remote-tracking branch 'origin/main' into bc-e2eh-operator-review
gitrgoliveira Oct 8, 2026
beecb6d
test(mp-yqxn.2): follow PR #462 in the journeys; bc-otpl, bc-kosc, bc…
gitrgoliveira Oct 8, 2026
4c879e7
Merge remote-tracking branch 'origin/main' into bc-e2eh-operator-review
gitrgoliveira Oct 10, 2026
d906054
test(mp-yqxn.2): bc-dhrp is a plain test; follow #463's winner label
gitrgoliveira Oct 10, 2026
0254429
Merge branch 'main' into bc-e2eh-operator-review
gitrgoliveira Oct 11, 2026
b333b7d
Merge remote-tracking branch 'origin/main' into bc-e2eh-operator-review
gitrgoliveira Oct 11, 2026
29e71bb
fix(mp-yqxn.2): address the code review of the e2e harness
gitrgoliveira Oct 11, 2026
485706d
test(bc-e2eh, bc-shcr, bc-mvsj): harness review fixes, console policy…
gitrgoliveira Oct 11, 2026
d3894b4
test(bc-shcr, bc-mvsj): fixmes for the shared-court rulings; stale jo…
gitrgoliveira Oct 12, 2026
7a5607b
test(bc-e2eh): J1 lets a mark removal land before the next strike
gitrgoliveira Oct 12, 2026
e644527
fix(bc-ltap): a mark tapped back while its removal is saving is kept
gitrgoliveira Oct 12, 2026
34e3a75
test(bc-e2eh): make the journeys' assertions able to fail
gitrgoliveira Oct 12, 2026
2021f3c
fix(bc-sdtp): a double tap on Start match starts one match
gitrgoliveira Oct 12, 2026
c64a120
fix(bc-xrpl): answer an exact resend of a recorded finish as recorded
gitrgoliveira Oct 12, 2026
9ef805f
Merge branch 'main' into bc-e2eh-operator-review
gitrgoliveira Oct 12, 2026
dfb552d
feat(bc-cmpw): one admin password; delete asks for it again; self-run…
gitrgoliveira Oct 13, 2026
3b45f33
Merge remote-tracking branch 'origin/main' into bc-e2eh-operator-review
gitrgoliveira Oct 13, 2026
ac60a87
fix(bc-cmpw): reset only from the server machine; round-3 review fixes
gitrgoliveira Oct 13, 2026
512e38f
fix(bc-cmpw): reset also requires a loopback Host
gitrgoliveira Oct 13, 2026
9d03ba2
test(bc-cmpw): use the house test password in the delete dialog test
gitrgoliveira Oct 13, 2026
e7ff13d
fix(bc-cmpw): call the one password the admin password everywhere
gitrgoliveira Oct 13, 2026
7f63382
test(bc-e2eh): harness review round 1 fixes
gitrgoliveira Oct 13, 2026
61a488c
test(bc-e2eh): harness review round 2 fixes; keyboard docs
gitrgoliveira Oct 13, 2026
83b6dbe
test(bc-e2eh): independent review fixes for the harness
gitrgoliveira Oct 13, 2026
303daf6
test(bc-e2eh): independent review round 2 fixes for the harness
gitrgoliveira Oct 13, 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
8 changes: 4 additions & 4 deletions .claude/rules/match-writes.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ read the diff. See CONTRIBUTING and CLAUDE.md.
- [ ] `make go/test` passes (lint + security scan + tests)
- [ ] New/updated unit tests cover the change
- [ ] Manual browser verification (for `web-mobile/` or `web/` changes): describe what you exercised
- [ ] `make e2e` green locally (any `web-mobile/` change)
- [ ] Screenshots added above (REQUIRED for any UI-affecting change)
- [ ] Docs updated under `docs/` for any new or changed user-facing feature, flag, command, or behavior (`make docs/build` passes)
- [ ] Docs screenshots and videos recaptured for any change to what a user sees (`make docs/media SINCE=main`; the changed ones reviewed and copied into `docs/`, or "none changed")
Expand Down
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ Thumbs.db
!web/package-lock.json
!scripts/screenshots/package.json
!scripts/screenshots/package-lock.json
!scripts/e2e/package.json
!scripts/e2e/package-lock.json
# Go testdata fixtures. These are test INPUTS, not build output: the blanket
# *.json above would silently drop them, and a golden table that never lands
# fails CI on a checkout that works locally. /**/ matches zero or more
Expand Down Expand Up @@ -154,3 +156,7 @@ mp-*/

# Docs capture harness: staged output, compared by eye before it replaces a committed file.
scripts/screenshots/out/

# E2E journeys (make e2e): per-run screenshots and Playwright's own artifacts.
# Its node_modules/ is covered by the bare node_modules/ rule above.
scripts/e2e/output/
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Detailed domain rulings (match results and marks, match writes and the offline q
- **API Documentation:** The OpenAPI specification for the web API is located in `specs/openapi.yaml` and is fully synchronized with the backend implementation.
- **Mobile App (`mobile-app` command):** A real-time tournament management server serving a Preact/JSX UI from `web-mobile/`. State is file-backed: `tournament-data/tournament.md` (YAML) and `tournament-data/competitions/<id>/config.md` + `participants.csv`. Backend in `internal/mobileapp/` (Gin handlers) and `internal/state/` (store). Real-time updates via SSE. Admin actions require `X-Tournament-Password` header. Run with `make run-mobile`.
- **Real-Time Updates:** Real-time updates via SSE. Admin actions require `X-Tournament-Password` header. Run with `make run-mobile`.
- **Admin Auth Modes:** Two startup-selected modes (`internal/mobileapp/auth_source.go`). **File mode** (default) stores the password plaintext in `tournament.md`; a public `POST /api/tournament/reset` resets it without the old password. **Locked mode** (`--lock-password` + `TOURNAMENT_PASSWORD_HASH` env var) compares the header against a bcrypt hash and disables the reset API (`POST /api/tournament/reset` returns 404; the SPA `/reset` page still loads but shows an operator-disabled message). The public `GET /api/auth-config` reports the active mode so the SPA can hide reset UI when locked. Generate hashes with `bracket-creator hash-password`.
- **Admin Auth Modes:** Two startup-selected modes (`internal/mobileapp/auth_source.go`). **File mode** (default) stores the password plaintext in `tournament.md`; `POST /api/tournament/reset` resets it without the old password, for loopback callers only (403 `reset_local_only` otherwise). **Locked mode** (`--lock-password` + `TOURNAMENT_PASSWORD_HASH` env var) compares the header against a bcrypt hash and disables the reset API (`POST /api/tournament/reset` returns 404; the SPA `/reset` page still loads but shows an operator-disabled message). The public `GET /api/auth-config` reports the active mode so the SPA can hide reset UI when locked. Generate hashes with `bracket-creator hash-password`.
- **Pool Scoring Rules:**
- **Individual:** 1. Fights Won, 2. Fights Lost, 3. Hikiwake, 4. Points Scored, 5. Points Lost.
- **Team:** 1. Team W, 2. Team L, 3. Team T, 4. Individual Winners (IV), 5. Individual Losses (IL), 6. Individual Ties (IT), 7. Points Won (PW), 8. Points Lost (PL).
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ go test -cover ./internal/helper/...
- **`internal/excel/`**: Excel file lifecycle (`Client`), the from-scratch workbook template (`NewFileFromScratch`), style definitions.
- **`internal/service/`**: Service layer abstraction over helper logic.
- **`internal/resources/`**: Embedded file management. Resources flow: `main.go` embeds → `resources.NewResources()` → `cmd.ExecuteWithResources()`.
- **`internal/mobileapp/`**: Gin HTTP handlers for the tournament app (`mobile-app` command). Routes: `handlers_competition.go` (including `POST /api/competitions/:id/generate-draw` [status `draw-ready`] and `DELETE /api/competitions/:id/draw` to discard the draw; a draw-ready competition is listed on the shiaijo operator views and its FIRST RECORDED result starts it -- a match left running or completed, NEVER a write that clears a result back to scheduled (the engine's result gate, which also discounts the byes a knockout draw resolves at generation time) -- via `engine.MaybeAutoCompletePools`'s `AutoCompleteStarted` outcome, which `tryAutoCompletePools` broadcasts as `competition_started`, so the desk's `POST .../start` is one of two doors, bc-prow ruling), `handlers_match.go`, `handlers_participants.go` (including single participant PUT updates, individual/bulk check-ins `POST /api/competitions/:id/participants/checkin-bulk`), `handlers_tournament.go`, `handlers_swiss.go` (`generate-round`, `standings`), `handlers_decision.go` (kiken/fusenpai/fusensho/daihyosen: `POST /api/competitions/:id/matches/:mid/decision`; `handleBothSidesBarredHikiwake` runs before the endpoint's own `Validate()` -- which otherwise 400s `hikiwake` outright with "use /score for fought/hikiwake" -- as the one carve-out that accepts `decision: "hikiwake"`, no `decisionBy` needed, when both sides of a SCHEDULED pool or league match (`engine.IsPoolMatchID`, league reuses the same `"Pool "`-prefixed ids; Swiss ids never match and are not covered) are already barred; the write records `Status: completed, Decision: "hikiwake"` with no new `CompetitorStatus`. The same both-barred knockout match instead gets `ineligible_competitor` naming both sides and pointing at the earlier withdrawal or the draw), `handlers_eligibility.go` (`/api/competitions/:id/competitor-status`), `handlers_lineup.go` (team lineups), `handlers_schedule.go` (`GET /api/schedule/estimate`, public), `handlers_reset.go` (`POST /api/tournament/reset`, public, for forgotten admin passwords; 404s in locked mode), `handlers_auth_config.go` (`GET /api/auth-config`, public, reports auth mode to the SPA). Real-time push via SSE (`hub.go`) with events: `match_updated`, `competitor_status_updated`, `competition_completed`, `swiss_round_generated`, etc. Auth via `X-Tournament-Password` header (`middleware.go`), with two modes selected at startup by a `PasswordVerifier` (`auth_source.go`): **file mode** (default, plaintext compare against `tournament.md`) or **locked mode** (`--lock-password` flag + `TOURNAMENT_PASSWORD_HASH` env var, bcrypt compare, `POST /api/tournament/reset` returns 404; the SPA `/reset` page still renders an operator-disabled message). Consumer-boundary interfaces live in `deps.go` (NFR-002).
- **`internal/mobileapp/`**: Gin HTTP handlers for the tournament app (`mobile-app` command). Routes: `handlers_competition.go` (including `POST /api/competitions/:id/generate-draw` [status `draw-ready`] and `DELETE /api/competitions/:id/draw` to discard the draw; a draw-ready competition is listed on the shiaijo operator views and its FIRST RECORDED result starts it -- a match left running or completed, NEVER a write that clears a result back to scheduled (the engine's result gate, which also discounts the byes a knockout draw resolves at generation time) -- via `engine.MaybeAutoCompletePools`'s `AutoCompleteStarted` outcome, which `tryAutoCompletePools` broadcasts as `competition_started`, so the desk's `POST .../start` is one of two doors, bc-prow ruling), `handlers_match.go`, `handlers_participants.go` (including single participant PUT updates, individual/bulk check-ins `POST /api/competitions/:id/participants/checkin-bulk`; self-run mode has no check-in, so the three check-in writes answer 409 `check_in_not_in_self_run` for every caller, and `state.Competition.CheckInApplies` is the one owner the draw, the Swiss field and the estimate ask, never `CheckInEnabled` itself), `handlers_tournament.go`, `handlers_swiss.go` (`generate-round`, `standings`), `handlers_decision.go` (kiken/fusenpai/fusensho/daihyosen: `POST /api/competitions/:id/matches/:mid/decision`; `handleBothSidesBarredHikiwake` runs before the endpoint's own `Validate()` -- which otherwise 400s `hikiwake` outright with "use /score for fought/hikiwake" -- as the one carve-out that accepts `decision: "hikiwake"`, no `decisionBy` needed, when both sides of a SCHEDULED pool or league match (`engine.IsPoolMatchID`, league reuses the same `"Pool "`-prefixed ids; Swiss ids never match and are not covered) are already barred; the write records `Status: completed, Decision: "hikiwake"` with no new `CompetitorStatus`. The same both-barred knockout match instead gets `ineligible_competitor` naming both sides and pointing at the earlier withdrawal or the draw), `handlers_eligibility.go` (`/api/competitions/:id/competitor-status`), `handlers_lineup.go` (team lineups), `handlers_schedule.go` (`GET /api/schedule/estimate`, public), `handlers_reset.go` (`POST /api/tournament/reset`, for forgotten admin passwords; in file mode it answers loopback callers only, judged on `RemoteAddr` and never on forwarded headers, 403 `reset_local_only` otherwise; 404s in locked mode), `handlers_auth_config.go` (`GET /api/auth-config`, public, reports auth mode to the SPA). Real-time push via SSE (`hub.go`) with events: `match_updated`, `competitor_status_updated`, `competition_completed`, `swiss_round_generated`, etc. Auth via `X-Tournament-Password` header (`middleware.go`), with two modes selected at startup by a `PasswordVerifier` (`auth_source.go`): **file mode** (default, plaintext compare against `tournament.md`) or **locked mode** (`--lock-password` flag + `TOURNAMENT_PASSWORD_HASH` env var, bcrypt compare, `POST /api/tournament/reset` returns 404; the SPA `/reset` page still renders an operator-disabled message). Consumer-boundary interfaces live in `deps.go` (NFR-002).
- **`internal/state/`**: File-backed state store for the mobile app. Tournament and competition config lives in `tournament-data/tournament.md` and `tournament-data/competitions/<id>/config.md` (YAML front-matter). Participants are in `participants.csv` alongside each config.
- **`internal/engine/`**: Thin adapter that drives `internal/helper` pool/bracket generation from a `state.Competition`. Called by the `POST /api/competitions/:id/start` handler.
- **`web-mobile/`**: Preact/JSX frontend for the mobile app, served embedded in the binary. Entry point: `web-mobile/index.html`. JS modules in `web-mobile/js/` are grouped by prefix: `admin_*.jsx` (the operator console: setup, participants, pools, scoring, scheduling, lineups, etc.), `viewer_*.jsx` and `display_*.jsx` (public attendee/spectator surfaces), and shared infrastructure (`app.jsx`, `api_client.jsx`, `api_serializers.jsx`, `bracket.jsx`, `data.jsx`, `patch.jsx`, `router.jsx`, `ui.jsx`, `glossary.jsx`). Run `ls web-mobile/js/` for the current set rather than relying on an enumerated list here. Note `admin_participants.jsx` holds the `LinedTextarea` gutter participant paste box; check-in is NOT there, it is owned by `admin_registration_desk.jsx`. CSS in `web-mobile/css/styles.css`. Pre-compiled to `web-mobile/dist/` by esbuild (run automatically as part of `make go/build`).
Expand Down
28 changes: 27 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ else
endif

# Define phony targets
.PHONY: default help clean local/deps hooks/install go/fmt go/generate go/test go/build go/lint go/sec go/sec-tests go/vuln go/security js/deps js/lint js/sec js/outdated js/security js/check-imports js/validate examples docker/build docker/run pre-commit docs/deps docs/serve docs/open docs/build docs/linkcheck docs/prose docs/clean docs/screenshots docs/videos docs/media run run-mobile esbuild-jsx goreleaser/test release version
.PHONY: default help clean local/deps hooks/install go/fmt go/generate go/test go/build go/lint go/sec go/sec-tests go/vuln go/security js/deps js/lint js/sec js/outdated js/security js/check-imports js/validate examples docker/build docker/run pre-commit docs/deps docs/serve docs/open docs/build docs/linkcheck docs/prose docs/clean docs/screenshots docs/videos docs/media e2e e2e/deps run run-mobile esbuild-jsx goreleaser/test release version

default: help ## Show help information (default)

Expand Down Expand Up @@ -377,6 +377,32 @@ docs/media: go/build $(SHOTS_BROWSER_STAMP) ## Regenerate every captured screens
@# those would start node and a browser twice over two disjoint halves.
@node $(SHOTS_DIR)/run.mjs $(if $(KIND),'KIND=$(KIND)',) $(SHOTS_ARGS)

# Local e2e journeys (scripts/e2e, see its README.md). Like the capture harness
# above, Playwright lives in its own package.json rather than web-mobile's, and
# both pin the same Playwright release so they share one downloaded Chromium
# under ~/.cache/ms-playwright. Deliberately NOT part of go/test and not run in
# CI: a journey drives the real app through a browser, setup wizard and all.
E2E_DIR := scripts/e2e
E2E_DEPS_STAMP := $(E2E_DIR)/node_modules/.package-lock.json
E2E_BROWSER_STAMP := $(E2E_DIR)/node_modules/.chromium-installed

$(E2E_DEPS_STAMP): $(E2E_DIR)/package.json $(E2E_DIR)/package-lock.json
@echo "Installing the e2e harness dependencies..."
@npm --prefix $(E2E_DIR) ci --no-audit --no-fund
@touch $@

$(E2E_BROWSER_STAMP): $(E2E_DEPS_STAMP)
@echo "Downloading the e2e browser..."
@cd $(E2E_DIR) && npx playwright install chromium
@touch $@

e2e/deps: $(E2E_BROWSER_STAMP) ## Install the e2e harness and its Chromium (once per worktree)

# E2E_ARGS is handed to `playwright test` as is: a spec path, --grep, --headed,
# --project, ... e.g. make e2e E2E_ARGS='journeys/smoke.spec.mjs --headed'
e2e: go/build $(E2E_BROWSER_STAMP) ## Run the local e2e journeys (E2E_ARGS= passes a spec, --grep or --headed to playwright)
@cd $(E2E_DIR) && npx playwright test $(E2E_ARGS)

run: go/build ## Run the application locally
@echo "Running $(BIN_NAME)..."
$(BIN_PATH)/$(BIN_NAME) serve
Expand Down
8 changes: 6 additions & 2 deletions docs/architecture/data-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,12 @@ Inspectability, repairability and diffability matter more here than normalisatio
* **Repairable.** An organiser can correct a wrong cell by hand, and the app picks it up on reload.
* **Diffable.** The whole tournament state can be committed to version control or copied to
a USB stick as a backup. Two copies can then be compared line by line.
One exception: `tournament.md` holds the tournament and admin passwords in plain text, so
strip or change them before sharing a copy.
One exception: `tournament.md` holds the admin password in plain text, so strip or
change it before sharing a copy. A `tournament.md` that carries an `admin_password` field
may hold another password in plain text there. The app ignores that field and drops it the
next time it writes the file; remove it by hand before sharing a copy that still has it.
Refer to
[Upgrading from an earlier release](../user-guide/organisers/operating-modes.md#upgrading-from-an-earlier-release).

The trade this makes is described honestly in [section 6](#6-how-the-model-maps-onto-rows).

Expand Down
2 changes: 1 addition & 1 deletion docs/architecture/infrastructure-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ flowchart TB

| Device | What it runs | Notes |
|---|---|---|
| Operator console (1 per court) | admin scoring SPA | tablet/desktop surface; authenticates with the tournament password; scores its own shiai-jo |
| Operator console (1 per court) | admin scoring SPA | tablet/desktop surface; authenticates with the admin password; scores its own shiai-jo |
| Display screen (1 per court, optional) | public display / scoreboard view | a browser at a display URL; read-only, no auth. **Preferred: drive it from the operator console's own machine** via an HDMI cable to a TV or monitor, so the board survives a Wi-Fi outage (refer to [Keep the court scoreboard alive on the same machine](#keep-the-court-scoreboard-alive-on-the-same-machine-hdmi)). A standalone smart-TV browser or separate mini-PC also works but loses that offline path |
| Spectator phones | public viewer (mobile-first) | can be on cellular; they don't need venue Wi-Fi when the app is cloud-hosted |

Expand Down
Loading
Loading