---
title: Build the League Log
type: task
status: completed
description: >-
  One read-only league log per hosted league over the existing per-kind records (settings, team,
  lineup, transactions, waivers, trades, bids, matchup and standings changes, stat corrections),
  with per-source visibility rules, an API route and a League Log page.
base_uri: user:task/league/build-league-log.md
created_at: '2026-10-31T17:18:12.696Z'
entity_id: c2a428f6-1e1b-4895-b1ee-1d61f09a6415
finished_at: '2026-11-02T03:50:02.853Z'
observations:
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
owner_identity_uri: █████████████████████████
priority: Medium
public_read: true
relations:
  - follows [[██████████████████████-██-██████-████-███████████-████-██████-██████]]
  - follows [[user:task/league/durable-manager-and-commissioner-action-records.md]]
  - relates [[user:task/league/wire-waivers-to-bid-changelog.md]]
started_at: '2026-11-02T02:22:29.125Z'
tags:
  - user:tag/league-xo-football.md
updated_at: '2026-11-02T06:08:08.443Z'
visibility_analyzed_at: '2026-11-05T23:48:27.613Z'
---

# Implementation Plan: Build the League Log

## Overview

- Give each hosted league one log of everything that changed its state: settings, team settings, lineups and tags, transactions, processed waivers, trades, poaches, settled restricted free agency bids, matchup and standings changes, and stat corrections.
- **Impact.** Members and the commissioner can answer "what changed, when, and from what" in one place instead of reconstructing it from Discord, the way the 2026-09-10 team-name ruling had to.
- **Ordering.** Follows [[██████████████████████-██-██████-████-███████████-████-██████-██████]], which creates the matchup, standings, playoff and fantasy-point changelogs this log reads. Follows [[user:task/league/durable-manager-and-commissioner-action-records.md]], whose 2026-09-10 decision rules out a general activity table. Relates to [[user:task/league/wire-waivers-to-bid-changelog.md]], whose waiver history becomes one more source when it lands.

## Background

- **The records already exist, one per kind of change.** `leagues_changelog`, `teams_changelog` and `rosters_players_changelog` hold settings, team and lineup or tag changes. `transactions`, `waivers`, `trades`, `poaches` and `bid_changelog` hold their own timestamped rows. The stat correction task adds `matchups_changelog`, `league_team_seasonlogs_changelog`, `playoffs_changelog` and `scoring_format_player_gamelogs_changelog`.
- **Some of that data is private until an event.** `bid_changelog` carries `bid_amount` for created, updated and cancelled bids, which are sealed (`libs-shared/constants/bid-changelog-constants.mjs:26-37`). The waiver report hides other teams' failed claims. Lineups lock per player at that player's game start (`api/routes/teams/lineups.mjs:540-545`), not per week.
- **Mirrors are different.** Mirror leagues' team claims are open to any signed-in user (`api/routes/leagues/middleware.mjs`, `require_league_not_mirror`), so a per-team visibility rule guards nothing there. Their scores come from the vendor.

### Assumptions

- Every source table carries a league id or joins to one in one step. Unchecked: verified per adapter.
- A union over about ten adapters, keyset-paged on time, answers in under a second for one league-season. Unchecked: measured in the query step.

## Design

- **A view, never a table.** No writer posts to the league log. It reads the records that already exist.
- **A source registry.** `libs-server/league-log/` defines one adapter per record. Each maps its rows to a common entry: league, season, week, time, entry type, subject team and player, old and new value or a summary, and source. Each also declares its visibility rule.
- **Visibility.**
  - Restricted free agency bids, auction elections and waiver claims appear only as settled outcomes, after processing. Neither the existence nor the amount of an unsettled bid or claim ever appears, per [[███████████████████████-███████-███████████-███████-██████-████-███-██-████-████████████]].
  - Waivers appear only as processed, successful claims. A team sees its own failed claims.
  - A lineup or tag change appears to other teams only after that player's game has kicked off.
  - Stat corrections appear as fantasy-point changes in the league's scoring format, for players on a roster in that league that week.
- **Hosted leagues only.** Mirror leagues get no league log in this task.
- **Surfaces.** `GET /api/leagues/:leagueId/log`, filtered by season, entry type and team, keyset-paged on time. A League Log page in the league section of the SPA, per `docs/guides/spa.md` and `STYLE.md`, which flags flipped matchup results.
- **Knowledge Base Impact.**
  - Add the league log read model to `text/league/database-architecture.md` § "The activity records", with the rule that it is a view, never a table.
  - Document the route in `docs/guides/api.md`.
  - Add a line to the ████ ██████████ ██████████ doc on where members see corrections.
- **Scratch.** None needed.

## Plan

- [x] **Source registry and query**
  - Files: `libs-server/league-log/index.mjs` and one adapter module per source.
  - Changes: adapters for the sources above with their visibility rules, a union query for one league and season, and keyset paging on time.
  - Verify: tests cover all of these cases.
    - Each adapter returns entries for a fixture league.
    - An unsettled bid or claim never appears, by existence or amount.
    - Another team's failed waiver claim is hidden.
    - A lineup change is hidden from another team until the player's game kicks off.
    - A mirror league returns no entries.
    - One league-season query is timed and the time recorded on this task.
- [x] **API route**
  - Files: `api/routes/leagues/log.mjs`, the leagues router, `api/swagger/config.mjs`.
  - Changes: the route with filters, a `before` cursor, response validation and a refusal for mirror leagues.
  - Verify: a route test returns paged entries, honors each filter, and returns 404 for an unknown league.
- [x] **League Log page**
  - Files: under `app/views/` and `app/core/`.
  - Changes: the page with season, entry type and team filters, old to new values, and a flag on flipped results.
  - Verify: `yarn dev:smoke` shows the page rendering entries for the home league, and each filter changes the list.
- [x] **Knowledge base updates**
  - Changes: the database architecture section, the API guide and the operations doc line, as listed in the Design.
  - Verify: `rg -n "league log" text/league/database-architecture.md` and `rg -n "/log" repository/active/league/docs/guides/api.md` each find the entry.
