turniere-backend/doc/team_action_lists_api.md

6.2 KiB

Team Action Lists API

Purpose

Team action lists let the backend expose checklist-style workflows for teams inside a tournament.

Current example:

  • group_stage_survivor_playoff_tokens

That list contains all teams that advanced from the group stage and tracks whether each team already collected a playoff token.

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": "group_stage_survivor_playoff_tokens",
      "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"
          }
        }
      ]
    }
  ]
}

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/group_stage_survivor_playoff_tokens/teams/55

Request body:

{
  "completed": true
}

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.

Good:

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

Avoid:

  • toggle-style client behavior such as "invert current state"

Why this is 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:

  • 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. 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

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:

  • 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:

  • 200 update accepted
  • 403 authenticated but not tournament owner
  • 404 tournament, list, team, or item not found
  • 423 follower tournament is read-only
  • 401 unauthenticated

Sync / Follower Behavior

Team action lists are included in tournament sync snapshots.

That means:

  • source tournament owner updates item state
  • backend enqueues tournament sync
  • follower tournament receives updated list state
  • follower users can view but not mutate item 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 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/item changes broadcast updated full snapshot again
  • same broadcast path is used for:
    • direct source updates
    • follower sync imports
  • both write 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": "group_stage_survivor_playoff_tokens",
      "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. when user checks or unchecks one team, send one idempotent HTTP PATCH for that item only
  5. 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:

  • stable list key
  • user-facing name
  • user-facing action_name

New list types should automatically fit the same frontend rendering model:

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