turniere-backend/doc/team_action_lists_api.md

7.9 KiB

Team Action Lists API

Purpose

Team action lists let tournament owners create checklist-style workflows for teams inside a tournament.

Examples:

  • collect playoff tokens
  • track team check-in
  • hand out medals

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

Frontend reads team action lists from the normal tournament payload:

  • GET /tournaments/:id

Example response shape:

{
  "id": 123,
  "name": "My Tournament",
  "team_action_lists": [
    {
      "id": 9,
      "name": "Playoff token collection",
      "key": "playoff_token_collection",
      "action_name": "Collect playoff token",
      "source": "group_stage_survivors",
      "team_action_items": [
        {
          "id": 41,
          "completed": false,
          "completed_at": null,
          "team": {
            "id": 55,
            "name": "Team A"
          }
        }
      ]
    }
  ]
}

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:

{
  "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. Everyone may read status. Follower tournaments in read_only_mode reject updates with 423.

There are two supported write patterns.

Option 1: update by item id

Use this when frontend already has the item id from tournament payload.

  • PATCH /team_action_items/:id

Request body:

{
  "completed": true
}

Option 2: update by list key + team id

Use this when frontend wants a business-key style request like:

  • "tick off team X from list Y in tournament Z"

Endpoint:

  • PATCH /tournaments/:tournament_id/team_action_lists/:key/teams/:team_id

Example:

  • PATCH /tournaments/123/team_action_lists/playoff_token_collection/teams/55

Request body:

{
  "completed": true
}

Response body for both write endpoints is the updated TeamActionItem.

Idempotency Rules

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:

  • send {"completed": true} to mark complete
  • send {"completed": false} to mark incomplete

Avoid:

  • toggle-style client behavior such as "invert current state"
  • whole-list writeback from stale client state

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 stores one unique item per (team_action_list_id, team_id)

completed_at behavior:

  • first successful transition to completed=true sets timestamp if none exists
  • sending completed=true again keeps same completed state
  • sending completed=false clears completed_at

Frontend Implementation Guidance

Recommended frontend flow:

  1. Load tournament via GET /tournaments/:id
  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:

  • use list key as stable frontend reference
  • use team.id as stable team reference
  • use team_action_item.id when available for direct item updates

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

Error Handling

Expected statuses:

  • 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

Sync / Follower Behavior

Team action lists are included in tournament sync snapshots.

That means:

  • source owner creates list on source tournament
  • backend enqueues tournament sync
  • 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

Team action lists also support live updates over ActionCable.

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 POST and PATCH requests

Endpoint:

  • GET /cable for websocket upgrade

Channel:

  • TournamentTeamActionListsChannel

Subscription identifier example:

{
  "channel": "TournamentTeamActionListsChannel",
  "tournament_id": 123
}

Behavior:

  • subscription immediately receives current full team-action-list snapshot for that tournament
  • later list creation and item changes broadcast updated full snapshot again
  • same broadcast path is used for:
    • list creation
    • direct item updates
    • follower sync imports
  • 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

Payload shape:

{
  "type": "team_action_lists.updated",
  "tournament_id": 123,
  "team_action_lists": [
    {
      "id": 9,
      "name": "Playoff token collection",
      "key": "playoff_token_collection",
      "action_name": "Collect playoff token",
      "source": "group_stage_survivors",
      "team_action_items": [
        {
          "id": 41,
          "completed": true,
          "completed_at": "2026-04-23T09:15:00Z",
          "team": {
            "id": 55,
            "name": "Team A"
          }
        }
      ]
    }
  ]
}

Frontend guidance:

  • use websocket payload as replacement snapshot for team-action-list state in that tournament
  • do not merge by toggling local state blindly
  • keep writes item-scoped through HTTP
  • use websocket only for read/live propagation

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. 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 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 still fit same frontend rendering model:

  • list metadata
  • list items
  • explicit completed state
  • idempotent set-state requests