---
title: Fix the plays page returning no rows
type: task
status: completed
description: >-
  The /plays page renders headers and zero rows on production and locally, though 47,492 rows match
  its default view predicates (play_year 2025, play_type != NOPL). Predates and is independent of
  the 2026-08-23 stale-cursor fix.
base_uri: user:task/league/fix-plays-page-returning-no-rows.md
created_at: '2026-08-23T06:01:53.050Z'
entity_id: 1c136b0f-01d5-4c5a-bf78-3da503f7262f
finished_at: '2026-09-04T08:17:21.676Z'
observations:
  - ████████████████████████████████████████████████
owner_identity_uri: █████████████████████████
priority: Medium
public_read: true
tags:
  - user:tag/league-xo-football.md
updated_at: '2026-09-04T08:17:21.676Z'
visibility_analyzed_at: '2026-11-05T23:49:26.940Z'
---

## Problem

The `/plays` page rendered its headers and then no rows at all, on production and on a local `yarn dev:live` build alike. Observed 2026-08-23 while verifying an unrelated pagination fix.

The default `All Plays` view was not at fault, and neither was the data. Its `where` clause (`app/core/plays-view/default-plays-views.js`) is two ordinary predicates — `play_year = String(current_season.stats_season_year)`, resolved to `2025` in the browser, and `play_type != 'NOPL'` — and 47,492 `nfl_plays` rows matched them. Never an offseason artifact.

## Resolution

Two independent causes, each sufficient on its own. Both are fixed, deployed and verified on production.

**No view was ever selected, so no request was composed.** `load_plays_views` called the owner-scoped `GET /api/plays/views`, which can only 401 for an anonymous visitor, and the bootstrap that ends in the results request hung off `GET_PLAYS_VIEWS_FULFILLED`. An anonymous load therefore selected no view and issued nothing. Fixed in `3be36394c` by porting the data-views anonymous-restore path: with no `userId`, restore straight from browser storage.

**Once a view WAS selected, the composed request was silently dropped.** `send` in `app/core/ws/service.js` returns `false` when the socket is not open, and the page composes its first request from a mount effect that can beat the socket open. Nothing retransmitted it, because the reducer keeps the view id rather than the params. Closed first in `3be36394c` by replaying an in-flight request on `WEBSOCKET_OPEN`, then replaced in `1f55dda67` with the send queue `/data-views` already used — `queue_until_open` with `replace_key: 'plays_view_request'` — so both surfaces solve the one problem the one way. The replay saga and its two watchers are gone.

Answers to what the task listed as open questions:

- The `PLAYS_VIEW_REQUEST` did not go out at all, for both reasons above. A direct trace recorded `send` returning `sent: false` on an anonymous cold load.
- The page was not auth-gated differently from data views. The difference was that data views already had the anonymous-restore path and the send queue.
- `api/sockets/plays_view.mjs` was not at fault — nothing reached it.
- The `play_year` versus `nfl_plays.season_year` column-id mismatch is expected; the data-view layer maps it.

## Why it read as silent

Worth keeping, because it defeated the first round of verification. The status pill renders only for a `pending` that already carries a server-assigned queue position, so a request that never reached the wire displays nothing at all and reads identical to success. The oracle is rendered rows, never the pill.

And a cold-load row count is unreliable ON LOCALHOST in BOTH directions: whether the mount effect beats the socket open is load-dependent, so an injected-fault control on the dev server read the same 78 rows with the queue disabled as with it enabled, while a trace an hour earlier on that same server had caught the drop directly. The durable check is the saga-effect assertion in `test/websocket.send-queue.spec.mjs`, which gates the opt-in call sites and needs no race.

## Verification

Anonymous `/plays` on production renders 78 `.row` elements with data, in a fresh ephemeral browser profile. A `/plays` URL carrying its own table state renders that state's columns rather than the default view's, and the player drawer's Plays tab renders rows with week and description populated.

## Key locations

- `app/views/pages/plays/plays.js`, `app/core/plays-view/{sagas.js,default-plays-views.js}`
- `app/core/ws/{service.js,send-queue.js}`, `test/websocket.send-queue.spec.mjs`
- `app/core/plays-view-request/reducer.js`
- Browser-verification traps for this surface: `docs/guides/ship.md`, `docs/guides/spa.md`
