Files
fujarna/docs/DOCUMENTATION.md
Jan Novak 90162bb14f
All checks were successful
Build and Push / build (push) Successful in 44s
Use mirror.gcr.io for Docker base images to avoid Docker Hub rate limits
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 15:07:57 +01:00

29 KiB

Disc Agenda -- Technical Documentation

Detailed developer reference for the Disc Agenda frisbee tournament platform. For quick start and project overview, see the README. For project motivation and feature descriptions, see the Project Description.


Table of Contents

  1. Architecture Overview
  2. Data Model Reference
  3. API Reference
  4. WebSocket Protocol
  5. Storage Format
  6. How-To Guides
  7. Development Setup
  8. Docker Deployment
  9. CI/CD Pipeline
  10. Frontend Architecture
  11. Troubleshooting

1. Architecture Overview

Disc Agenda is a single-binary Go server that serves both a REST API and a compiled React SPA. There is no separate frontend server in production.

Browser
  │
  ├── GET /tournament/... ──────► SPA Handler ──► index.html (React Router takes over)
  ├── GET/POST /api/...   ──────► REST Handlers ──► Storage (JSON files)
  └── WS /ws/...          ──────► WebSocket Hubs ──► Broadcast to all clients
                                        │
                                        ▼
                              File System (data/)
                              ├── tournaments.json
                              └── tournaments/{id}/
                                  ├── schedule.json
                                  ├── games/{gid}_score.json
                                  ├── games/{gid}_audit.jsonl
                                  ├── questionnaire_config.json
                                  └── questionnaire_responses/

Key design decisions:

  • No database -- flat JSON files are sufficient for small tournament scale (10-20 teams). Human-readable and easily backed up.
  • No authentication -- trust-based model for community use. Anyone with the URL can update scores.
  • SPA fallback -- any unmatched request serves index.html, letting React Router handle client-side routing.
  • WebSocket hubs -- per-game hubs manage scoring clients; a separate tournament hub broadcasts all game updates to schedule viewers.

Source files

File Purpose
backend/cmd/server/main.go Entry point, router setup, SPA handler
backend/internal/handlers/handlers.go REST + WebSocket HTTP handlers
backend/internal/models/models.go Domain types
backend/internal/storage/storage.go File-based persistence layer
backend/internal/websocket/hub.go WebSocket hub management and scoring logic

2. Data Model Reference

All types are defined in backend/internal/models/models.go.

2.1 Tournament

Field JSON Type Description
ID id string URL-safe unique identifier (e.g., fujarna-14-3-2026)
Name name string Display name
Status status string upcoming, in_progress, or completed
Location location string City/country
Venue venue string Specific venue name
StartDate start_date string YYYY-MM-DD format
EndDate end_date string YYYY-MM-DD format
Description description string Short description
Teams teams []Team Array of participating teams
ImageURL image_url string Optional banner image URL
Rules rules string Optional rules text

Example:

{
  "id": "fujarna-14-3-2026",
  "name": "Fujarna - 14.3.2026",
  "status": "upcoming",
  "location": "Praha, Czech Republic",
  "venue": "Víceúčelové sportoviště \"Kotlářka\"",
  "start_date": "2026-03-14",
  "end_date": "2026-03-14",
  "description": "Fujarna turnaj v ultimate frisbee. 10 týmů ve 2 skupinách, crossover pavouk.",
  "teams": [
    {"id": "fuj-1", "name": "FUJ 1"},
    {"id": "kocicaci", "name": "Kočičáci"}
  ],
  "rules": "Hra 20 min. Bez pauzy. Bez TO. Remíza není možná."
}

2.2 Team

Field JSON Type Description
ID id string URL-safe identifier
Name name string Display name
Logo logo string Optional logo URL

2.3 Game

Field JSON Type Description
ID id string Game identifier (e.g., g01, p5, final)
TourneyID tourney_id string Parent tournament ID
HomeTeam home_team string Home team display name (not ID)
AwayTeam away_team string Away team display name (not ID)
HomeScore home_score int Current home score
AwayScore away_score int Current away score
StartTime start_time string ISO 8601 datetime
Field field string Field name/number
Round round string Round name (e.g., Pool A - Round 1, Grand Final)
Status status string scheduled, live, or final

Note: home_team and away_team store display names rather than team IDs. For bracket/placement games, placeholders like 1A, 3B are used until teams are determined.

2.4 Schedule

Field JSON Type Description
TourneyID tourney_id string Tournament ID
Games games []Game Array of all games

2.5 ScoreState

Represents the current state of a game's score. Returned by score endpoints and WebSocket messages.

Field JSON Type Description
GameID game_id string Game identifier
HomeScore home_score int Current home score
AwayScore away_score int Current away score
HomeTeam home_team string Home team name
AwayTeam away_team string Away team name
Status status string scheduled, live, or final

2.6 ScoreUpdate

Sent by clients to modify a game's score.

Field JSON Type Description
Action action string increment, decrement, set, or set_status
Team team string home or away (for score actions); scheduled, live, or final (for set_status)
Value value int Target value (used only with set action)
Timestamp timestamp time Set server-side
UserID user_id string Set server-side from WebSocket query param

2.7 AuditEntry

One entry per score change, appended to the audit log.

Field JSON Type Description
Timestamp timestamp time When the change occurred
Action action string The action performed
Team team string Which team/status was affected
Value value int Value for set actions
OldHome old_home int Home score before change
OldAway old_away int Away score before change
NewHome new_home int Home score after change
NewAway new_away int Away score after change
UserID user_id string Who made the change

2.8 QuestionnaireConfig

Field JSON Type Description
TourneyID tourney_id string Tournament ID
CustomQuestions custom_questions []Question Custom survey questions

2.9 Question

Field JSON Type Description
ID id string Unique question identifier
Text text string Question text displayed to user
Type type string text, select, or radio
Options options []string Choices (for select and radio types)
Required required bool Whether answer is mandatory

2.10 QuestionnaireResponse

Field JSON Type Description
ID id string Auto-generated: resp_{unix_nano}
TourneyID tourney_id string Set server-side from URL
MyTeam my_team string Respondent's team name
SpiritWinner spirit_winner string Team voted for best spirit
AttendNext attend_next bool Will attend next tournament
CustomAnswers custom_answers map[string]string Key: question ID, value: answer
SubmittedAt submitted_at time Set server-side

2.11 FinalResults / Standing

FinalResults:

Field JSON Type Description
TourneyID tourney_id string Tournament ID
Standings standings []Standing Ordered standings

Standing:

Field JSON Type Description
Position position int Final placement (1 = winner)
TeamID team_id string Team identifier
TeamName team_name string Team display name
Wins wins int Number of wins
Losses losses int Number of losses
Draws draws int Number of draws
PointsFor points_for int Total points scored
PointsAgainst points_against int Total points conceded
SpiritScore spirit_score float64 Optional spirit rating

3. API Reference

All endpoints return JSON. Error responses use the format {"error": "message"}.

3.1 GET /api/tournaments

Returns all tournaments.

Response 200:

[
  {
    "id": "fujarna-14-3-2026",
    "name": "Fujarna - 14.3.2026",
    "status": "upcoming",
    "location": "Praha, Czech Republic",
    "venue": "Víceúčelové sportoviště \"Kotlářka\"",
    "start_date": "2026-03-14",
    "end_date": "2026-03-14",
    "description": "Fujarna turnaj v ultimate frisbee...",
    "teams": [{"id": "fuj-1", "name": "FUJ 1"}, ...],
    "rules": "Hra 20 min. Bez pauzy. Bez TO. Remíza není možná."
  }
]

3.2 GET /api/tournaments/{id}

Returns a single tournament.

Response 200: Tournament object Response 404: {"error": "tournament fujarna-xyz not found"}

3.3 GET /api/tournaments/{id}/schedule

Returns the game schedule with current scores merged in. The handler reads each game's persisted score state and overwrites the schedule's default home_score, away_score, and status fields.

Response 200:

{
  "tourney_id": "fujarna-14-3-2026",
  "games": [
    {
      "id": "g01",
      "tourney_id": "fujarna-14-3-2026",
      "home_team": "FUJ 1",
      "away_team": "Kočičáci",
      "home_score": 8,
      "away_score": 6,
      "start_time": "2026-03-14T08:30:00",
      "field": "Field 1",
      "round": "Pool A - Round 1",
      "status": "live"
    }
  ]
}

3.4 GET /api/tournaments/{id}/games/{gid}/score

Returns current score state for a specific game.

Response 200:

{
  "game_id": "g01",
  "home_score": 8,
  "away_score": 6,
  "home_team": "FUJ 1",
  "away_team": "Kočičáci",
  "status": "live"
}

If no score file exists yet, returns zeroed state with empty status.

3.5 POST /api/tournaments/{id}/games/{gid}/score

Updates a game's score via REST (alternative to WebSocket). The update is also broadcast to all WebSocket clients on the game and tournament hubs.

Request body examples:

{"action": "increment", "team": "home"}
{"action": "decrement", "team": "away"}
{"action": "set", "team": "home", "value": 12}
{"action": "set_status", "team": "final"}

Response 200: Updated ScoreState object Response 400: {"error": "invalid request body"}

Behavior notes:

  • increment, decrement, set are no-ops when game status is final
  • decrement will not go below 0
  • Auto-transitions from scheduled to live when score becomes non-zero

3.6 GET /api/tournaments/{id}/games/{gid}/audit

Returns the complete audit trail for a game.

Response 200:

[
  {
    "timestamp": "2026-03-14T09:15:32.123Z",
    "action": "increment",
    "team": "home",
    "value": 0,
    "old_home": 7,
    "old_away": 6,
    "new_home": 8,
    "new_away": 6,
    "user_id": "user_abc123"
  }
]

Returns [] (empty array) if no audit entries exist.

3.7 GET /api/tournaments/{id}/questionnaire

Returns the questionnaire config and team list for the tournament.

Response 200:

{
  "config": {
    "tourney_id": "fujarna-14-3-2026",
    "custom_questions": [
      {
        "id": "q_food",
        "text": "How would you rate the food at the tournament?",
        "type": "select",
        "options": ["Excellent", "Good", "Average", "Poor"],
        "required": false
      },
      {
        "id": "q_feedback",
        "text": "Any other feedback or suggestions?",
        "type": "text",
        "required": false
      }
    ]
  },
  "teams": [
    {"id": "fuj-1", "name": "FUJ 1"},
    {"id": "kocicaci", "name": "Kočičáci"}
  ]
}

3.8 POST /api/tournaments/{id}/questionnaire

Submits a questionnaire response. The server auto-generates id (as resp_{unix_nano}) and submitted_at.

Request body:

{
  "my_team": "FUJ 1",
  "spirit_winner": "Kočičáci",
  "attend_next": true,
  "custom_answers": {
    "q_food": "Good",
    "q_fields": "Acceptable",
    "q_feedback": "Great tournament!"
  }
}

Response 201:

{"status": "ok", "id": "resp_1710423600000000000"}

3.9 GET /api/tournaments/{id}/questionnaire/results

Returns all submitted questionnaire responses. Intended for organizer/admin use.

Response 200: Array of QuestionnaireResponse objects. Returns [] if none exist.

3.10 GET /api/tournaments/{id}/results

Returns final standings.

Response 200:

{
  "tourney_id": "fujarna-14-3-2026",
  "standings": [
    {
      "position": 1,
      "team_id": "fuj-1",
      "team_name": "FUJ 1",
      "wins": 6,
      "losses": 1,
      "draws": 0,
      "points_for": 85,
      "points_against": 52,
      "spirit_score": 4.2
    }
  ]
}

4. WebSocket Protocol

4.1 Game WebSocket

Endpoint: ws://host/ws/game/{tourneyId}/{gameId}?user_id=alice

The user_id query parameter identifies the scorer. If omitted, defaults to "anonymous".

Connection lifecycle:

  1. Client connects
  2. Server immediately sends current score state:
    {"type": "score_state", "state": {"game_id": "g01", "home_score": 8, ...}}
    
  3. Client sends score updates:
    {"action": "increment", "team": "home"}
    {"action": "decrement", "team": "away"}
    {"action": "set", "team": "home", "value": 12}
    {"action": "set_status", "team": "final"}
    
  4. Server broadcasts to ALL connected clients (including sender):
    {
      "type": "score_update",
      "state": {"game_id": "g01", "home_score": 9, "away_score": 6, ...},
      "audit": {"action": "increment", "team": "home", "old_home": 8, "new_home": 9, ...}
    }
    

Score update rules:

  • increment / decrement / set are ignored when status == "final"
  • decrement will not go below 0
  • Auto-transition: scheduled -> live when any score becomes non-zero
  • set_status accepts scheduled, live, or final as the team field value

Keepalive:

  • Server sends ping every 30 seconds
  • Read deadline: 60 seconds (client must respond with pong)
  • Write deadline: 10 seconds per message
  • Read limit: 4096 bytes

4.2 Tournament WebSocket

Endpoint: ws://host/ws/tournament/{tourneyId}

Read-only connection for schedule viewers. Receives all score_update broadcasts from every game hub in the tournament.

  • Client sends nothing (the read pump only processes pong frames)
  • Receives the same score_update messages as game WebSocket clients
  • Read limit: 512 bytes
  • Used by the SchedulePage for live score updates across all games

4.3 Hub Architecture

HubManager
├── hubs (map: "{tourneyId}/{gameId}" -> Hub)
│   ├── Hub "fujarna.../g01"
│   │   ├── clients[] (connected scorers)
│   │   ├── register/unregister channels
│   │   └── broadcast channel
│   └── Hub "fujarna.../g02" ...
└── tournamentHubs (map: "{tourneyId}" -> TournamentHub)
    └── TournamentHub "fujarna..."
        ├── clients[] (schedule viewers)
        └── broadcast channel
  • Lazy creation: hubs are created on first connection via GetOrCreateHub / GetOrCreateTournamentHub
  • Game -> Tournament forwarding: after every score update, the game hub calls BroadcastToTournament() to push the same message to all tournament hub clients
  • On client registration: the game hub immediately sends the current score_state to the new client
  • Thread safety: sync.RWMutex protects hub maps and client maps

5. Storage Format

5.1 Directory Layout

data/
├── tournaments.json                              # Array of all tournaments
└── tournaments/
    └── {tournament-id}/
        ├── schedule.json                         # Game schedule
        ├── questionnaire_config.json             # Survey configuration
        ├── results.json                          # Final standings
        ├── games/
        │   ├── {game-id}_score.json              # Current score state
        │   └── {game-id}_audit.jsonl             # Append-only audit log
        └── questionnaire_responses/
            └── resp_{unix_nano}.json             # One file per response

5.2 File Formats

Score state ({gid}_score.json): Pretty-printed JSON of ScoreState:

{
  "game_id": "g01",
  "home_score": 8,
  "away_score": 6,
  "home_team": "FUJ 1",
  "away_team": "Kočičáci",
  "status": "live"
}

Audit log ({gid}_audit.jsonl): One JSON object per line, appended atomically:

{"timestamp":"2026-03-14T09:00:15Z","action":"increment","team":"home","value":0,"old_home":0,"old_away":0,"new_home":1,"new_away":0,"user_id":"user_abc"}
{"timestamp":"2026-03-14T09:01:22Z","action":"increment","team":"away","value":0,"old_home":1,"old_away":0,"new_home":1,"new_away":1,"user_id":"user_def"}

5.3 Concurrency Model

  • A single sync.RWMutex on the Store protects all file reads and writes
  • Audit log uses os.O_APPEND for atomic line appends
  • Directories are auto-created on write via os.MkdirAll
  • All JSON files use json.MarshalIndent with 2-space indentation

6. How-To Guides

6.1 Adding a New Tournament

  1. Edit data/tournaments.json -- add a new tournament object to the array:
{
  "id": "my-tournament-2026",
  "name": "My Tournament 2026",
  "status": "upcoming",
  "location": "City, Country",
  "venue": "Venue Name",
  "start_date": "2026-06-15",
  "end_date": "2026-06-15",
  "description": "Description of the tournament.",
  "teams": [
    {"id": "team-a", "name": "Team A"},
    {"id": "team-b", "name": "Team B"},
    {"id": "team-c", "name": "Team C"},
    {"id": "team-d", "name": "Team D"}
  ],
  "rules": "Game rules here."
}
  1. Create the tournament directory:
mkdir -p data/tournaments/my-tournament-2026/games
  1. Create data/tournaments/my-tournament-2026/schedule.json:
{
  "tourney_id": "my-tournament-2026",
  "games": [
    {
      "id": "g01",
      "tourney_id": "my-tournament-2026",
      "home_team": "Team A",
      "away_team": "Team B",
      "home_score": 0,
      "away_score": 0,
      "start_time": "2026-06-15T09:00:00",
      "field": "Field 1",
      "round": "Pool - Round 1",
      "status": "scheduled"
    }
  ]
}
  1. Restart the server (the server reads files on each request, but a restart ensures clean state for WebSocket hubs).

6.2 Configuring the Questionnaire

Create data/tournaments/{id}/questionnaire_config.json:

{
  "tourney_id": "my-tournament-2026",
  "custom_questions": [
    {
      "id": "q_food",
      "text": "How would you rate the food?",
      "type": "select",
      "options": ["Excellent", "Good", "Average", "Poor"],
      "required": false
    },
    {
      "id": "q_feedback",
      "text": "Any suggestions?",
      "type": "text",
      "required": false
    }
  ]
}

Built-in fields (always present in the UI, not configured here):

  • My Team -- select from tournament team list
  • Spirit Winner -- select from tournament team list
  • Attend Next Tournament -- checkbox

Custom question types:

  • select -- dropdown with predefined options
  • radio -- radio button group
  • text -- free-text input

6.3 Adding Results After Tournament

Create data/tournaments/{id}/results.json:

{
  "tourney_id": "my-tournament-2026",
  "standings": [
    {
      "position": 1,
      "team_id": "team-a",
      "team_name": "Team A",
      "wins": 5,
      "losses": 0,
      "draws": 0,
      "points_for": 75,
      "points_against": 30,
      "spirit_score": 4.5
    },
    {
      "position": 2,
      "team_id": "team-b",
      "team_name": "Team B",
      "wins": 3,
      "losses": 2,
      "draws": 0,
      "points_for": 55,
      "points_against": 42
    }
  ]
}

Note: spirit_score is optional. The results page highlights the team with the highest spirit score.


7. Development Setup

7.1 Prerequisites

  • Go 1.26+ -- backend
  • Node 22+ -- frontend build and dev server
  • npm -- comes with Node

7.2 Backend

cd backend
go mod tidy
go run ./cmd/server -data ../data -static ../frontend/dist -port 8080

Server flags:

Flag Env Var Default Description
-port PORT 8080 HTTP listen port
-data DATA_DIR ./data Data directory path
-static -- ./static Frontend static files directory

The SPA handler serves index.html for any request that doesn't match a static file, /api/, or /ws/ path. This enables React Router's client-side routing.

CORS is configured to allow all origins (*), all common methods, and all headers.

7.3 Frontend

cd frontend
npm install
npm run dev

Vite dev server runs on http://localhost:5173 and proxies:

  • /api/* -> http://localhost:8080
  • /ws/* -> ws://localhost:8080

This allows hot-reload development while the Go backend handles API and WebSocket connections.

7.4 Makefile Targets

Target Description
make help Show all available targets
make dev Print local development instructions
make tidy Run go mod tidy
make dev-backend Run Go backend dev server
make dev-frontend Run Vite frontend dev server
make build-frontend Production frontend build (npm ci && npx vite build)
make docker Build Docker images via docker compose
make image Build standalone Docker image
make run Start Docker containers (detached)
make stop Stop Docker containers
make logs Follow Docker container logs
make clean Remove frontend/dist, node_modules, and Go build cache

8. Docker Deployment

8.1 Multi-Stage Build

The Dockerfile uses three stages for a minimal production image:

All base images are pulled from mirror.gcr.io (Google's public Docker Hub mirror) to avoid Docker Hub rate limits.

Stage 1 -- Frontend build (Node 22 Alpine):

  • Installs npm dependencies (npm ci)
  • Builds React app with Vite (npx vite build)
  • Output: frontend/dist/

Stage 2 -- Backend build (Go 1.26 Alpine):

  • Downloads Go modules
  • Builds static binary with CGO_ENABLED=0 and stripped debug symbols (-ldflags="-s -w")
  • Output: /server binary

Stage 3 -- Runtime (Alpine 3.21):

  • Copies server binary, built frontend, and seed data
  • Installs only ca-certificates and tzdata
  • Exposes port 8080

8.2 Entrypoint & Data Seeding

The entrypoint script seeds initial data on first run:

if [ ! -f /app/data/tournaments.json ]; then
  echo "Seeding initial data..."
  cp -r /app/data-seed/* /app/data/
fi
exec /app/server -static /app/static -data /app/data

Seed data is baked into the image at /app/data-seed/. On first run (when the volume is empty), it's copied to /app/data/. On subsequent runs, existing data in the volume is preserved.

8.3 Docker Compose

services:
  frisbee:
    build: .
    ports:
      - "8080:8080"
    volumes:
      - tournament-data:/app/data
    environment:
      - PORT=8080
      - TZ=Europe/Prague
    restart: unless-stopped

volumes:
  tournament-data:
  • Volume tournament-data persists all game scores, audit logs, and questionnaire responses across container restarts
  • Timezone is set to Europe/Prague for correct timestamps in audit logs
  • Restart policy ensures the server comes back after crashes or host reboots

8.4 Production Considerations

  • No TLS termination -- use a reverse proxy (nginx, Caddy, Traefik) for HTTPS
  • No authentication -- anyone with the URL can update scores; consider network-level access control
  • WebSocket origin check is disabled (CheckOrigin returns true for all requests)
  • Data backup -- the Docker volume can be backed up by copying its contents or using docker cp

9. CI/CD Pipeline

Defined in .gitea/workflows/build.yaml.

Triggers:

  • Manual dispatch (workflow_dispatch) with optional tag input
  • Tag push (push: tags)

Steps:

  1. Checkout repository
  2. Login to Gitea container registry at gitea.home.hrajfrisbee.cz
  3. Build Docker image using the multi-stage Dockerfile
  4. Push to registry as gitea.home.hrajfrisbee.cz/{owner}/{repo}:{tag}

Tag resolution: uses the manual input tag, or falls back to github.ref_name (the pushed tag or branch name).

Secrets required:

  • REGISTRY_TOKEN -- authentication token for the Gitea container registry

10. Frontend Architecture

10.1 Routes

Path Component Description
/ -- Redirects to /tournament/fujarna-14-3-2026
/tournament/:id TournamentPage Tournament detail with info cards
/tournament/:id/schedule SchedulePage Live game schedule
/tournament/:id/game/:gid GamePage Live scoreboard
/tournament/:id/questionnaire QuestionnairePage Post-tournament survey
/tournament/:id/results ResultsPage Final standings
/past PastPage Archive of completed tournaments

10.2 API Client (frontend/src/api.js)

  • fetchJSON(path) / postJSON(path, body) -- wrappers around fetch with error handling
  • All API functions are thin wrappers: getTournaments(), getTournament(id), getSchedule(id), etc.
  • createGameWebSocket(tourneyId, gameId, userId) -- creates WebSocket with auto protocol detection (ws: or wss: based on page protocol)
  • createTournamentWebSocket(tourneyId) -- read-only WebSocket for schedule updates

10.3 Key Components

GamePage -- live scoreboard:

  • Connects via game WebSocket with a session-scoped user_id (random, stored in sessionStorage)
  • +/- buttons send increment/decrement actions
  • SET button opens a modal for direct value entry
  • Displays audit log (expandable)
  • Generates QR code linking to the questionnaire page
  • Auto-reconnects on WebSocket disconnect (3-second delay)

SchedulePage -- tournament schedule:

  • Connects via tournament WebSocket for live updates across all games
  • Groups games by round (Pool A, Pool B, placement bracket)
  • Shows status badges and live scores
  • Auto-reconnects on disconnect

QuestionnairePage -- post-tournament survey:

  • Loads questionnaire config and team list from API
  • Renders built-in fields (my team, spirit winner, attend next) and custom questions
  • Czech-language UI labels
  • Shows success message after submission

ResultsPage -- final standings:

  • Renders standings table with W/L/D, points for/against, point differential
  • Medal emojis for top 3 positions
  • Highlights team with highest spirit_score

11. Troubleshooting

WebSocket not connecting:

  • Ensure the backend is running and accessible
  • Check that your reverse proxy forwards WebSocket connections (upgrade headers)
  • Verify the /ws/ path prefix is not blocked

Scores not persisting:

  • Check data/ directory permissions (needs read/write)
  • Verify the Docker volume is mounted correctly
  • Check server logs for write errors

Frontend shows 404 on page refresh:

  • The SPA handler should serve index.html for unknown paths
  • Ensure -static flag points to the directory containing index.html

Schedule shows stale scores:

  • The schedule endpoint merges from individual score files on every request
  • Check that WebSocket connection is active (green indicator in browser dev tools)
  • Try refreshing the page to re-establish the tournament WebSocket

Audit log growing large:

  • Audit files are JSONL, one line per score change
  • Safe to truncate or archive old .jsonl files when not needed

Docker volume data reset:

  • Data only seeds when tournaments.json is missing from the volume
  • To force a reseed: docker compose down -v (removes volumes), then docker compose up

CORS errors in development:

  • The backend allows all origins (*)
  • In dev mode, use the Vite dev server (port 5173) which proxies API calls to the backend