feat(webapp): browse the rules, stage 1: kinds with counts, a kind in numbered pages (#1240) #1295

Merged
claude-bot merged 5 commits from feat/1240-browse-rules into main 2026-10-31 00:21:35 +00:00
Collaborator

Closes #1240. Built to the owner-signed-off drawing (lane 6e, PR #1292), with their answers: the system's order, pack buttons, numbered pages, and search inside an open kind.

Server (content_packs.py, app/reference/browse.py, search_service.py):

  • GET /api/game-systems/{key}/reference/kinds?campaign_id=&pack_id= returns the kinds in the display order (5e's ancestry is "Species"), with counts from one grouped COUNT. GM-only records are counted only for a GM, and gm_only / gm_only_total are given. The visible packs are listed (pack_id narrows the counts, not the pack list).
  • GET …/reference/page?…&page=&per_page= returns {total, page, per_page, capped, level_range, items}.
    • Without a query: A–Z with a real SQL OFFSET and no 500 cap, so every page of a 1,486-spell kind is reachable. This was a bug waiting to happen: the search's offset was capped at CANDIDATES = 500.
    • With a query: ranked, with total = min(count, 500) and capped set.
  • pack_id flows through _matching / count / search, so a count and its list always answer the same question. The existing /reference route is unchanged.

Page (Rules.jsx, new ui/Pager):

  • An empty box shows the kinds grid with counts (two columns on a phone); a GM sees a GM-only note.
  • An open kind shows: ‹ All kinds, the level range, pack buttons (more than one pack only), a search inside the kind, and rows as today's hits plus a "GMs only" chip.
  • A numbered pager: a <nav aria-label="Pages"> with aria-current, 44 px targets on touch, and on /design.
  • State lives in the URL (kind, page, lvlmin, lvlmax, pack, q), so Back returns to the same page. The empty, failed, capped and no-packs states are covered.

Fixed on integration review:

  • The server sends level_range as {min: null, max: null} for a kind with no levels, never null, which built a bogus option. The test used null.
  • A 5e challenge rating is fractional, so the selects run floor(min)…ceil(max), and "to N" asks for everything below N+1.

Wording for the owner: the Wording Review page, part j (j-001 to j-009). It includes making today's search chip say "GMs only" like the rest of the app. Don't merge until it's decided.

Checked:

  • backend: test_reference_browse (24 new) and related modules, 51 passed on the merged branch. The full suite ran on the server half: 10,411 passed, and the one audit failure was fixed.
  • frontend: 774 passed (wiki, ui, design reference). eslint: 0 errors.
  • lint, version sync and the changelog check pass.
  • A render check of the built page on the seeded stack (phone and desktop, light and dark, GM and player, axe) is running. Results follow as a comment.

🤖 Generated with Claude Code

Closes #1240. Built to the owner-signed-off drawing (lane 6e, PR #1292), with their answers: the system's order, pack buttons, **numbered pages**, and search inside an open kind. **Server (`content_packs.py`, `app/reference/browse.py`, `search_service.py`):** - `GET /api/game-systems/{key}/reference/kinds?campaign_id=&pack_id=` returns the kinds in the display order (5e's ancestry is "Species"), with counts from one grouped COUNT. GM-only records are counted only for a GM, and `gm_only` / `gm_only_total` are given. The visible packs are listed (`pack_id` narrows the counts, not the pack list). - `GET …/reference/page?…&page=&per_page=` returns `{total, page, per_page, capped, level_range, items}`. - **Without a query:** A–Z with a real SQL OFFSET and no 500 cap, so every page of a 1,486-spell kind is reachable. This was a bug waiting to happen: the search's offset was capped at `CANDIDATES` = 500. - **With a query:** ranked, with `total = min(count, 500)` and `capped` set. - `pack_id` flows through `_matching` / `count` / `search`, so a count and its list always answer the same question. The existing `/reference` route is unchanged. **Page (`Rules.jsx`, new `ui/Pager`):** - An empty box shows the kinds grid with counts (two columns on a phone); a GM sees a GM-only note. - An open kind shows: ‹ All kinds, the level range, pack buttons (more than one pack only), a search inside the kind, and rows as today's hits plus a "GMs only" chip. - A numbered pager: a `<nav aria-label="Pages">` with `aria-current`, 44 px targets on touch, and on `/design`. - State lives in the URL (`kind`, `page`, `lvlmin`, `lvlmax`, `pack`, `q`), so Back returns to the same page. The empty, failed, capped and no-packs states are covered. **Fixed on integration review:** - The server sends `level_range` as `{min: null, max: null}` for a kind with no levels, never `null`, which built a bogus option. The test used `null`. - A 5e challenge rating is fractional, so the selects run `floor(min)…ceil(max)`, and "to N" asks for everything below N+1. **Wording for the owner:** the Wording Review page, part j (j-001 to j-009). It includes making today's search chip say "GMs only" like the rest of the app. **Don't merge until it's decided.** **Checked:** - backend: `test_reference_browse` (24 new) and related modules, 51 passed on the merged branch. The full suite ran on the server half: 10,411 passed, and the one audit failure was fixed. - frontend: 774 passed (wiki, ui, design reference). eslint: 0 errors. - lint, version sync and the changelog check pass. - A render check of the built page on the seeded stack (phone and desktop, light and dark, GM and player, axe) is running. Results follow as a comment. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
GET /api/game-systems/{key}/reference/kinds (kinds in the system's order
with counts, GM-only counted only for a GM, the visible packs) and
GET .../reference/page (A-Z without a query and with no 500-record cap on
the offset, so every page of a large kind is reachable; ranked and capped
at 500 with a query; level range; pack filter). app/reference/browse.py
holds the display order and labels (5e's ancestry is "Species").

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rules & bestiary with the search box empty lists the kinds with counts;
a kind opens as an A-Z list with a level range, pack buttons when more
than one pack is loaded, a search inside the kind, and a numbered pager
(new ui/Pager, on /design). State lives in the URL so Back returns to the
same page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fix(webapp): browse levels as whole numbers, a kind with none has no level filter (#1240)
All checks were successful
CI / Which journeys does this change need? (pull_request) Successful in 12s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Has been skipped
CI / Does this change touch the recording path? (pull_request) Successful in 13s
CI / Docker image build (pull_request) Successful in 27s
CI / Bot/backend version sync (pull_request) Successful in 49s
CI / Backend lint (ruff) (pull_request) Successful in 57s
CI / Feedback site tests (pull_request) Successful in 1m38s
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 2m16s
CI / Bot tests and audit (pull_request) Successful in 4m20s
CI / Frontend (shard 2/2) (pull_request) Successful in 8m55s
CI / Frontend (shard 1/2) (pull_request) Successful in 10m39s
CI / End-to-end journeys (desktop) (pull_request) Successful in 17m43s
CI / Backend migration, tests, and audit (pull_request) Successful in 32m34s
82712caa45
The server's level_range is {min: null, max: null} for a kind with no
levels, never null, which made a bogus option; and a 5e challenge rating
is fractional, so the selects run from floor(min) to ceil(max) and "to N"
asks for everything below N+1. The test now uses the server's real shape.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Author
Collaborator

Render check (2026-10-30), on the seeded journeys stack (Playwright 1.64.1 + axe, on docker-test; the stack is torn down). Campaign: The Lanterns of Cortal (pf2e). A synthetic 120-creature pack was imported through the real API, to get a creature kind and three pages.

Rules & bestiary, as the GM (dana) and a player (joaquin), at 1280×800 and 390×844, light and dark:

Page Seen
empty box "Browse" with Creatures 120 and Rules 3 tiles, then house rules
?kind=creature ‹ All kinds, Creatures 120, the filter row (level, 3 pack buttons, Search creatures), 50 A–Z rows, Page 1 of 3
&page=2 the next 50, page 2 marked current
&lvlmin=30 "No creatures at level 30 or higher." + Clear the level
typed "gob" in Search creatures the URL gains &q=gob, Creatures 12, Gob- names only

Every one returned HTTP 200, with 0 console errors and no axe violations. At 390 there's no horizontal overflow (page 390/390, filter row 292/292). I looked at the phone shot myself: the stacked filters and the search box are normal height, and the pager sits at the bottom.

/design, where the Pager section shows its three samples: axe reports critical aria-required-children (2) and aria-valid-attr-value (3), serious color-contrast, and at 390 scrollable-region-focusable, across the whole 38,000 px gallery. The Pager itself uses only aria-label, aria-current="page" and aria-hidden on the gaps, so it's unlikely to be the source, but that hasn't been confirmed against main. It's a follow-up, not a blocker for this PR. At 390, a sample with many pages wraps "Next" onto a second line, which is acceptable.

Screenshots are in the session's scratchpad (render1240/).

**Render check (2026-10-30), on the seeded journeys stack** (Playwright 1.64.1 + axe, on docker-test; the stack is torn down). Campaign: The Lanterns of Cortal (pf2e). A synthetic 120-creature pack was imported through the real API, to get a creature kind and three pages. **Rules & bestiary**, as the GM (dana) and a player (joaquin), at 1280×800 and 390×844, light and dark: | Page | Seen | |---|---| | empty box | "Browse" with **Creatures 120** and **Rules 3** tiles, then house rules | | `?kind=creature` | ‹ All kinds, **Creatures 120**, the filter row (level, 3 pack buttons, Search creatures), 50 A–Z rows, **Page 1 of 3** | | `&page=2` | the next 50, page 2 marked current | | `&lvlmin=30` | **"No creatures at level 30 or higher."** + Clear the level | | typed "gob" in Search creatures | the URL gains `&q=gob`, **Creatures 12**, Gob- names only | Every one returned HTTP 200, with **0 console errors** and **no axe violations**. At 390 there's **no horizontal overflow** (page 390/390, filter row 292/292). I looked at the phone shot myself: the stacked filters and the search box are normal height, and the pager sits at the bottom. **`/design`**, where the Pager section shows its three samples: axe reports critical `aria-required-children` (2) and `aria-valid-attr-value` (3), serious `color-contrast`, and at 390 `scrollable-region-focusable`, across the whole 38,000 px gallery. The Pager itself uses only `aria-label`, `aria-current="page"` and `aria-hidden` on the gaps, so it's unlikely to be the source, but **that hasn't been confirmed against main**. It's a follow-up, not a blocker for this PR. At 390, a sample with many pages wraps "Next" onto a second line, which is acceptable. Screenshots are in the session's scratchpad (`render1240/`).
The owner's wording, j-008 on the Wording Review page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Merge remote-tracking branch 'origin/main' into feat/1240-browse-rules
All checks were successful
CI / Does this change touch the recording path? (pull_request) Successful in 13s
CI / Which journeys does this change need? (pull_request) Successful in 13s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Has been skipped
CI / Bot/backend version sync (pull_request) Successful in 23s
CI / Backend lint (ruff) (pull_request) Successful in 1m19s
CI / Feedback site tests (pull_request) Successful in 1m54s
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 2m13s
CI / Docker image build (pull_request) Successful in 2m41s
CI / Bot tests and audit (pull_request) Successful in 3m53s
CI / Frontend (shard 2/2) (pull_request) Successful in 8m18s
CI / Frontend (shard 1/2) (pull_request) Successful in 10m27s
CI / End-to-end journeys (desktop) (pull_request) Successful in 14m6s
CI / Backend migration, tests, and audit (pull_request) Successful in 1h19m22s
3c3ae16809
claude-bot scheduled this pull request to auto merge when all checks succeed 2026-10-30 23:02:24 +00:00
claude-bot deleted branch feat/1240-browse-rules 2026-10-31 00:21:36 +00:00
Sign in to join this conversation.
No description provided.