diff --git a/AGENTS.md b/AGENTS.md index a26dc23..bd49d17 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -176,6 +176,7 @@ Practical rule: For team action lists specifically: +- create lists explicitly through owner-facing HTTP API; do not auto-generate them from stage transitions unless product requirements say so - subscribe by tournament id - channel name is `TournamentTeamActionListsChannel` - broadcast full current list snapshot for that tournament diff --git a/doc/team_action_lists_api.md b/doc/team_action_lists_api.md index 9c1fd25..9adeeb9 100644 --- a/doc/team_action_lists_api.md +++ b/doc/team_action_lists_api.md @@ -2,13 +2,19 @@ ## Purpose -Team action lists let the backend expose checklist-style workflows for teams inside a tournament. +Team action lists let tournament owners create checklist-style workflows for teams inside a tournament. -Current example: +Examples: -- `group_stage_survivor_playoff_tokens` +- collect playoff tokens +- track team check-in +- hand out medals -That list contains all teams that advanced from the group stage and tracks whether each team already collected a playoff token. +Important: + +- lists are not auto-generated by backend tournament events +- owner creates a list explicitly through API +- backend materializes the initial team items from the chosen `source` ## Read Flow @@ -26,7 +32,7 @@ Example response shape: { "id": 9, "name": "Playoff token collection", - "key": "group_stage_survivor_playoff_tokens", + "key": "playoff_token_collection", "action_name": "Collect playoff token", "source": "group_stage_survivors", "team_action_items": [ @@ -45,6 +51,50 @@ Example response shape: } ``` +## Create Flow + +Only tournament owner may create lists. +Follower tournaments in `read_only_mode` reject creation with `423`. + +Endpoint: + +- `POST /tournaments/:tournament_id/team_action_lists` + +Request body: + +```json +{ + "name": "Playoff token collection", + "key": "playoff_token_collection", + "action_name": "Collect playoff token", + "source": "group_stage_survivors" +} +``` + +Response: + +- `201 Created` +- response body is the created `TeamActionList` including `team_action_items` + +Current supported sources: + +- `all_tournament_teams` +- `group_stage_survivors` + +Current source behavior: + +- `all_tournament_teams` uses all teams currently in tournament +- `group_stage_survivors` uses teams currently advancing from a finished group stage + +Creation is snapshot-style: + +- backend resolves teams at create time +- backend creates one item per resolved team +- backend does not auto-create lists later +- backend does not auto-refresh existing list membership later + +If selected source currently resolves to no teams, backend returns `422`. + ## Write Flow Only tournament owner may change item status. @@ -79,7 +129,7 @@ Endpoint: Example: -- `PATCH /tournaments/123/team_action_lists/group_stage_survivor_playoff_tokens/teams/55` +- `PATCH /tournaments/123/team_action_lists/playoff_token_collection/teams/55` Request body: @@ -93,7 +143,12 @@ Response body for both write endpoints is the updated `TeamActionItem`. ## Idempotency Rules -These endpoints are designed to be idempotent if frontend sends explicit target state. +Create endpoint is idempotent by unique business key per tournament: + +- backend enforces unique `(tournament_id, key)` +- repeated create with same key returns `422`, not a second list + +Item update endpoints are idempotent if frontend sends explicit target state. Good: @@ -103,12 +158,12 @@ Good: Avoid: - toggle-style client behavior such as "invert current state" +- whole-list writeback from stale client state -Why this is idempotent: +Why item updates are idempotent: - sending `completed=true` multiple times leaves item in same final state - sending `completed=false` multiple times leaves item in same final state -- backend does not create duplicate items - backend stores one unique item per `(team_action_list_id, team_id)` `completed_at` behavior: @@ -122,10 +177,10 @@ Why this is idempotent: Recommended frontend flow: 1. Load tournament via `GET /tournaments/:id` -2. Find desired list by stable `key` -3. Render `team_action_items` -4. On user action, send explicit target state -5. Update local UI from response or refetch tournament +2. Render current `team_action_lists` +3. If owner wants a new checklist, send `POST /tournaments/:id/team_action_lists` +4. For item changes, send explicit target state for one item only +5. Update local UI from response or websocket snapshot Recommended frontend identifiers: @@ -135,6 +190,7 @@ Recommended frontend identifiers: Recommended optimistic UI: +- create list only once per intended key - set checkbox immediately to requested target state - revert if API returns error - do not compute next state from stale cached data if multiple clients may edit @@ -143,11 +199,13 @@ Recommended optimistic UI: Expected statuses: -- `200` update accepted +- `201` list created +- `200` item update accepted +- `401` unauthenticated - `403` authenticated but not tournament owner - `404` tournament, list, team, or item not found +- `422` invalid create payload, duplicate key, unsupported source, or source with no teams - `423` follower tournament is read-only -- `401` unauthenticated ## Sync / Follower Behavior @@ -155,10 +213,12 @@ Team action lists are included in tournament sync snapshots. That means: -- source tournament owner updates item state +- source owner creates list on source tournament - backend enqueues tournament sync -- follower tournament receives updated list state -- follower users can view but not mutate item state while read-only +- follower tournament receives created list and all items +- source owner updates item state +- follower tournament receives updated completion state +- follower users can view but not mutate list state while read-only ## Websocket / Live Update Behavior @@ -168,7 +228,7 @@ This is a websocket flow, not a webhook flow. - backend pushes updates to subscribed clients over websocket - frontend does not register callback URLs -- writes still happen over normal HTTP `PATCH` requests +- writes still happen over normal HTTP `POST` and `PATCH` requests Endpoint: @@ -190,11 +250,12 @@ Subscription identifier example: Behavior: - subscription immediately receives current full team-action-list snapshot for that tournament -- later list/item changes broadcast updated full snapshot again +- later list creation and item changes broadcast updated full snapshot again - same broadcast path is used for: - - direct source updates + - list creation + - direct item updates - follower sync imports -- both write endpoints trigger the same backend update path before broadcast: +- both item update endpoints trigger the same backend update path before broadcast: - `PATCH /team_action_items/:id` - `PATCH /tournaments/:tournament_id/team_action_lists/:key/teams/:team_id` @@ -208,7 +269,7 @@ Payload shape: { "id": 9, "name": "Playoff token collection", - "key": "group_stage_survivor_playoff_tokens", + "key": "playoff_token_collection", "action_name": "Collect playoff token", "source": "group_stage_survivors", "team_action_items": [ @@ -239,20 +300,22 @@ Practical frontend pattern: 1. load initial tournament state over HTTP 2. open websocket subscription for same tournament id 3. render incoming websocket payload as latest live snapshot -4. when user checks or unchecks one team, send one idempotent HTTP `PATCH` for that item only -5. let websocket keep other viewers in sync +4. if owner submits create form, send one HTTP `POST` to create the list +5. when user checks or unchecks one team, send one idempotent HTTP `PATCH` for that item only +6. let websocket keep other viewers in sync This avoids whole-list writebacks and reduces race-condition risk between multiple clients. ## Future Extension Guidance -When new lists are added, frontend should not hardcode backend internals beyond: +When new sources or list types are added, frontend should not hardcode backend internals beyond: - stable list `key` - user-facing `name` - user-facing `action_name` +- selected `source` -New list types should automatically fit the same frontend rendering model: +New list types should still fit same frontend rendering model: - list metadata - list items