From 431f0a66403b33b95b5a7ad246f47c7b5c98d277 Mon Sep 17 00:00:00 2001 From: Malaber Date: Thu, 30 Apr 2026 14:09:45 +0200 Subject: [PATCH] docs: document transaction log API --- ...ate_tournament_transaction_log_entries.rb} | 0 db/schema.rb | 2 +- doc/tournament_transaction_log_api.md | 281 ++++++++++++++++++ 3 files changed, 282 insertions(+), 1 deletion(-) rename db/migrate/{20260430120000_create_tournament_transaction_log_entries.rb => 20260430121000_create_tournament_transaction_log_entries.rb} (100%) create mode 100644 doc/tournament_transaction_log_api.md diff --git a/db/migrate/20260430120000_create_tournament_transaction_log_entries.rb b/db/migrate/20260430121000_create_tournament_transaction_log_entries.rb similarity index 100% rename from db/migrate/20260430120000_create_tournament_transaction_log_entries.rb rename to db/migrate/20260430121000_create_tournament_transaction_log_entries.rb diff --git a/db/schema.rb b/db/schema.rb index 8471d00..773e654 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.1].define(version: 2026_04_30_120000) do +ActiveRecord::Schema[8.1].define(version: 2026_04_30_121000) do create_table "beamers", force: :cascade do |t| t.json "config", default: {}, null: false t.datetime "created_at", null: false diff --git a/doc/tournament_transaction_log_api.md b/doc/tournament_transaction_log_api.md new file mode 100644 index 0000000..b5aa0d7 --- /dev/null +++ b/doc/tournament_transaction_log_api.md @@ -0,0 +1,281 @@ +# Tournament Transaction Log API + +## Purpose + +Tournament transaction log is owner-readable tournament history. + +It records ordered tournament events such as: + +- tournament creation +- timer changes +- match state changes +- match score changes +- decider match creation +- playoff start attempts and failures +- playoff start success +- group match auto-starts + +Frontend should show this as an append-only history feed for tournament owners. + +## Read Flow + +Transaction log is not embedded in normal tournament payload. +Read it from dedicated owner-only endpoint: + +- `GET /tournaments/:id/transaction_log` + +Auth: + +- same Devise token auth headers as other owner APIs +- endpoint requires authenticated tournament owner + +Response: + +- `200 OK` +- JSON array +- ordered by `sequence` ascending +- no pagination yet + +Example: + +```json +[ + { + "id": 17, + "sequence": 1, + "action": "tournament.created", + "message": "owner created tournament Spring Cup(123)", + "metadata": { + "tournament_id": 123, + "tournament_name": "Spring Cup" + }, + "created_at": "2026-04-30T10:12:42Z", + "user": { + "id": 4, + "username": "owner" + } + }, + { + "id": 18, + "sequence": 2, + "action": "match.state_changed", + "message": "owner changed match 77 (Team A(10) vs Team B(11)) to running", + "metadata": { + "match_id": 77, + "stage_type": "playoffs", + "old_state": "not_started", + "new_state": "in_progress", + "team_ids": [10, 11], + "teams": [ + { "id": 10, "name": "Team A" }, + { "id": 11, "name": "Team B" } + ], + "winner_team_id": null, + "advanced_match_ids": [], + "group_scores": [] + }, + "created_at": "2026-04-30T10:13:01Z", + "user": { + "id": 4, + "username": "owner" + } + } +] +``` + +## Field Contract + +Each entry has: + +- `id`: DB id for entry +- `sequence`: per-tournament monotonic order number, starts at `1` +- `action`: machine-readable action key +- `message`: human-readable owner history text +- `metadata`: structured action-specific data +- `created_at`: ISO-8601 timestamp +- `user`: actor object, or `null` for system actions + +`user` shape: + +```json +{ + "id": 4, + "username": "owner" +} +``` + +Frontend display guidance: + +- sort by `sequence`, not by timestamp +- use `message` for first UI pass +- use `metadata` for links/details/chips +- tolerate unknown future `action` values +- tolerate extra `metadata` fields + +## Current Action Keys + +Current `action` values: + +- `tournament.created` +- `tournament.timer_changed` +- `match.state_changed` +- `match_score.updated` +- `decider_match.created` +- `playoffs.start_failed` +- `playoffs.started` +- `group_matches.started` + +Future backend work may add more action keys. +Unknown actions should still render via `message`. + +## Metadata Notes + +`metadata` is stable JSON, but shape depends on `action`. + +Common match metadata includes: + +- `match_id` +- `stage_id` +- `group_id` +- `stage_type`: `groupstage` or `playoffs` +- `team_ids` +- `teams` +- `winner_team_id` +- `advanced_match_ids` +- `group_scores` + +Team object shape: + +```json +{ + "id": 10, + "name": "Team A" +} +``` + +Group score object shape: + +```json +{ + "team_id": 10, + "team_name": "Team A", + "points": 9, + "matches_played": 3, + "wins": 3, + "draws": 0, + "losses": 0, + "score": 12, + "score_against": 4, + "score_difference": 8, + "position": 1 +} +``` + +## Example Actions + +### Match state change + +```json +{ + "action": "match.state_changed", + "message": "owner changed match 77 to finished (Team A(10) wins) - playoffs - Team A(10) advances to match 80", + "metadata": { + "match_id": 77, + "stage_type": "playoffs", + "old_state": "in_progress", + "new_state": "finished", + "winner_team_id": 10, + "advanced_match_ids": [80] + } +} +``` + +### Match score update + +```json +{ + "action": "match_score.updated", + "message": "owner changed matchscore 91 from match 77 to 3 (3 points for Team A(10) in match vs Team B(11))", + "metadata": { + "match_id": 77, + "match_score_id": 91, + "team_id": 10, + "changes": { + "points": { + "from": 2, + "to": 3 + } + } + } +} +``` + +### Playoff start failure + +```json +{ + "action": "playoffs.start_failed", + "message": "owner attempted playoff start, failed because decider missing", + "metadata": { + "stage_id": 12, + "reason": "decider_missing", + "blocking_ties": [ + { + "group_id": 5, + "team_ids": [10, 11], + "teams": [ + { "id": 10, "name": "Team A" }, + { "id": 11, "name": "Team B" } + ] + } + ] + } +} +``` + +### Playoffs started + +```json +{ + "action": "playoffs.started", + "message": "owner started playoffs, advancing teams: Team A(10), Team B(11), Team C(12), Team D(13)", + "metadata": { + "stage_id": 12, + "advancing_team_ids": [10, 11, 12, 13], + "advancing_teams": [ + { "id": 10, "name": "Team A" }, + { "id": 11, "name": "Team B" }, + { "id": 12, "name": "Team C" }, + { "id": 13, "name": "Team D" } + ] + } +} +``` + +## Error Behavior + +Expected errors: + +- `401 Unauthorized`: missing or invalid auth +- `403 Forbidden`: authenticated user is not tournament owner +- `404 Not Found`: tournament id does not exist + +Read-only follower tournaments: + +- log read is allowed for owner +- log write is internal only; frontend has no write endpoint + +## Frontend Implementation Guide + +Recommended owner-history flow: + +1. Load tournament as usual. +2. If current user is owner, request `GET /tournaments/:id/transaction_log`. +3. Render entries sorted by `sequence`. +4. Use `message` as main text. +5. Show `created_at` as event time. +6. Link matches/teams from `metadata` when ids are present. +7. Refresh after owner mutations, or poll while history panel is open. + +There is no websocket stream for transaction log yet. +Existing tournament/match websocket updates still fire for live tournament state.