phase: 86_history_page_width

All criteria verified. Final report:

**Phase 86 final verification pass — all green, no defects found.**

- **Verified (previously implemented):** `position: relative` on shared `.table-wrap` (the one-rule A3 fix, +12 lines in `frontend/assets/styles.css`); unit pin `tests/unit/test_table_wrap_stacking.py` (3 tests); E2E story `tests/e2e/test_history_page_width.py` (5 tests: History 626→≤375, Tokens 618→≤375, SPA switch with window-sentinel, RAG regression, desktop 1280×800)
- **Tests/lint (this pass):** `uv run pytest` → 1720 passed · `uv run pytest --cov=app --cov-report=term-missing` → TOTAL **99%** (>90% floor) · E2E in isolation: `test_history_page_width.py` 5 passed, `test_chat_history.py` 5 passed, `test_api_tokens.py` 9 passed, `test_smoke.py` 3 passed · `uv run ruff check . && uv run pyright` → clean, 0 errors
- **Completion criteria:** ① 375px direct+SPA `scrollWidth ≤ innerWidth` on History/Tokens — PASS (E2E pins) ② in-card scroll preserved + full-width + a11y spans in DOM — PASS ③ Sources + desktop regression — PASS ④ full suite/coverage/E2E/lint gates — PASS ⑤ `git diff --stat` limited to `styles.css` (+12) + new test files + phase files, no `app/`/markup/JS — PASS ⑥ commit/move — left to harness per executor rules (working tree intact, `todo/` dir removal already reflected)
- **Notable:** red→green (CSS reverted → 626px failure) and manual live check already recorded in `.agents/reports/86_history_page_width/`; pre-existing untracked `.agents/remediation_plan.md` (Sep 7 security audit) untouched
- **Next pending phase:** `87_big_read_progress`
This commit is contained in:
2026-09-08 02:38:45 -04:00
parent 412a560348
commit 0f6b9ff7e6
19 changed files with 951 additions and 0 deletions
@@ -1,53 +0,0 @@
# Phase 86 — Stop the table pages from stretching the document to the table's width
**Source:** `TODO.md` L4 — "The history page appears to be the width of the table despite the table being scrollable. On mobile this results in half the page being blank and awkwardly scrollable."
**Story:** n/a (owner bug report — `TODO.md` L4, 2026-09-07)
**Context:** `frontend/index.html` — `#view-history` (the History shell view: `.container.history-shell > .table-wrap.history-table-wrap#history-table-wrap > table.history-table`, whose Actions column header is `<th scope="col"><span class="visually-hidden">Actions</span></th>` and whose `<caption class="visually-hidden">` sits first) and `#view-tokens` (the SAME pattern: `.table-wrap#tokens-table-wrap > table#tokens-table` with the identical visually-hidden Actions header + caption); the RAG view's `.docs-table` (visible text headers — NO hidden spans), `frontend/assets/styles.css` — `.visually-hidden` (the global a11y helper: `position: absolute !important; width: 1px; height: 1px; margin: -1px; … clip: rect(0 0 0 0); overflow: hidden`), `.table-wrap` (the shared scroll card: `overflow-x: auto` — the phase-07 responsive contract), `.history-table` / `.tokens-table` (`width: 100%; min-width: 640px`), `tests/e2e/test_chat_history.py` + `tests/e2e/test_api_tokens.py` (the existing History/Tokens behavior suites — this phase adds no behavior, only fixes the page-level overflow).
## Bug basis (confirmed by live reproduction, 2026-09-07 — headless Chromium, 375×812, signed-in admin)
- **Direct load of `/history.html`:** `document.documentElement.scrollWidth` = **626** vs `innerWidth` 375 — the WHOLE PAGE pans ~250px to the right into a blank region (the owner's "half the page being blank and awkwardly scrollable"; `elementFromPoint` at the far right returns `null` — nothing is painted there). The scroll card itself is fine: `#history-table-wrap` is 346px wide with `overflow-x: auto` and its own `scrollWidth` 640 — the table scrolls INSIDE the card correctly.
- **The culprit:** `<span class="visually-hidden">Actions</span>` in the table header. `.visually-hidden` is `position: absolute !important` and NO ancestor in the chain (th → table → `.table-wrap` → `.history-shell` → `.view` → `#main` → `body`) is positioned, so the span's containing block is the **initial containing block**. The span's 1px box sits at its static position — the right edge of the 640px table (measured left ≈ 625.75px) — and, as a positioned box whose containing block is the ICB, it contributes to the **document's** scrollable overflow, bypassing the card's scroll clipping. `document.scrollWidth` (626) equals the span's right edge to the pixel.
- **Decisive bisection:** hiding that span → `scrollWidth` 375. Making `#history-table-wrap` `position: relative` → `scrollWidth` 375 (the span's containing block becomes the card; its box is contained in the card's scroll area). A plain 640px block inside the card never leaks (the leak is specific to the positioned hidden span).
- **The Tokens view has the identical bug** (same visually-hidden Actions header + caption in `#tokens-table`): measured `scrollWidth` 618; the same one-property fix brings it to 375. The owner reported History; Tokens is folded in (owner-confirmed in the roadmap, A3).
- **The RAG view is clean** (measured 375 — `.docs-table` has visible text headers, no positioned hidden spans).
- A standalone minimal page with the app's exact nesting (flex columns, the container, the card, a 640px table) does NOT leak — the trigger is the positioned `.visually-hidden` span inside the table, which is why the fix is the containing block, not any flex/width property.
## Objective
On mobile (and any viewport narrower than the tables' 640px min-width) the History and Tokens pages are viewport-width: the table scrolls inside its `.table-wrap` card (the phase-07 contract, AGENTS.md rule 5 — full-width tables stay) and the document itself no longer pans into a blank region.
## Owner decisions (chat, 2026-09-07 — confirmed with the roadmap, recorded per AGENTS.md rule 3)
- **A3 — the fix targets the shared `.table-wrap` card, not just History.** One rule (`position: relative`) on `.table-wrap` fixes BOTH affected views (History, Tokens) and any future table that ships a hidden header span in the card. No markup change: the visually-hidden spans stay (they are the accessible column name / table caption — removing them would break the a11y contract); no JS change.
- **Scope fold-in:** the owner's TODO item names the History page; the Tokens view carries the byte-identical defect (confirmed by measurement) and is fixed + pinned by the same phase — reported in this phase's docs, not as a separate phase (one coherent root cause, one rule).
## Design (shared by all tasks — the executor reads this, not the chat)
- **`frontend/assets/styles.css`** — in the `.table-wrap` rule (the shared scroll card, the rule that sets `overflow-x: auto` near the "pill language" comment block) add **`position: relative;`** with a root-cause comment: the card must be the containing block for the `position: absolute` `.visually-hidden` spans it hosts (the table caption + the Actions column header) — without it their 1px boxes are positioned against the initial containing block and leak into the DOCUMENT's scrollable overflow (the History page appeared "the width of the table", TODO.md L4; the Tokens view had the identical defect). Zero-offset positioning changes no layout; the spans stay clipped by their own `clip: rect(0 0 0 0)` + 1px box; every card that does NOT host such spans (the RAG `.docs-table`, the git-sources table) is visually unchanged.
- **Not touched:** the markup of both views, `.visually-hidden` itself (used across the app — the chat labels, announcers, skip-link neighborhood), `.md-table-wrap` (chat answer tables — different class, different context), any JS, any server code.
- **Unit pin (source-level, house pattern):** the `.table-wrap` rule carries `position: relative` (the containing-block contract — the regression that omitted it must not return).
- **E2E story (new suite, one file per story per AGENTS.md rule 4):** `tests/e2e/test_history_page_width.py` — the page-width contract at 375px for History AND Tokens (direct loads + SPA switch), the in-card scroll preserved, the RAG view regression, and the desktop (1280×800) no-overflow regression.
## Dependencies
- `85_mobile_menu_gate_overlap` (todo) — pipeline predecessor (execution order) only; NO code dependency (CSS + E2E only; touches no file that phase 85 owns except `frontend/assets/styles.css` — different rules, no overlap).
## Tasks
1. `01_table_wrap_fix.md` — the one-rule CSS fix + the source-level unit pin.
2. `02_e2e_story_suite.md` — `tests/e2e/test_history_page_width.py` (History + Tokens + RAG + desktop).
3. `03_verify_and_commit.md` — full gate (suite + coverage + the new E2E story in isolation + the two adjacent suites in isolation, smoke, ruff + pyright) + atomic commit.
## Testing & Quality
- Unit — `tests/unit/test_table_wrap_stacking.py` (new, source-level): the `.table-wrap` rule's declaration block contains `position: relative` (regex on the rule text); module docstring cites the mechanism (positioned `.visually-hidden` span + unpositioned scroll card → document-level scroll leak — TODO.md L4).
- E2E — `tests/e2e/test_history_page_width.py` (new; isolation gate per AGENTS.md rule 9): admin at 375×812 — direct `/history.html` and `/tokens.html`: `document.documentElement.scrollWidth <= window.innerWidth` (no page-level pan); `#history-table-wrap` / `#tokens-table-wrap` still scroll inside (`scrollWidth > clientWidth` — the 640px table, AGENTS.md rule 5 full-width contract); SPA switch history→tokens keeps the invariant; `/sources.html` (RAG) regression: `scrollWidth <= innerWidth` AND its table still full-width; desktop 1280×800: no overflow on any of the three views and the tables render at container width.
- Regression suites (run in isolation by task 03): `tests/e2e/test_chat_history.py` (History behavior) and `tests/e2e/test_api_tokens.py` (Tokens behavior) green unchanged.
- Coverage: **>90%** on `app/` — no `app/` code changes (the floor is held by the untouched suite).
## Completion Criteria
- [ ] At 375px, `/history.html` and `/tokens.html` (direct + SPA): `documentElement.scrollWidth <= innerWidth` (the E2E pins) — the owner's "half the page being blank" is gone.
- [ ] The tables still scroll INSIDE their cards (`wrap.scrollWidth > wrap.clientWidth` — pinned) and stay full-width per AGENTS.md rule 5; the visually-hidden Actions header/caption remain in the DOM (a11y names intact).
- [ ] `/sources.html` and the desktop viewports are byte-identical in behavior (regression pins green).
- [ ] `uv run pytest` green; `uv run pytest --cov=app --cov-report=term-missing` >90%; the new E2E story + `test_chat_history.py` + `test_api_tokens.py` + `test_smoke.py` green in isolation; `uv run ruff check . && uv run pyright` clean.
- [ ] `git diff --stat` limited to `frontend/assets/styles.css`, the new unit test, the new E2E file, phase files — no markup or JS changes.
- [ ] One atomic `--no-gpg-sign` commit (e.g. `fix(ui): stop the History and Tokens tables from stretching the document width`); phase dir moved to `.agents/phases/complete/`.
## Locked decisions
- **One CSS property is the whole fix** — `position: relative` on `.table-wrap` (A3); the containing-block mechanism is pinned by the unit test + the E2E scrollWidth contract.
- **The a11y spans stay** — the visually-hidden caption + Actions header are the accessible names; the fix repositions their CONTAINING BLOCK, not their existence.
- **No `app/` change** — pure frontend; the coverage floor is held by the untouched suite.
@@ -1,30 +0,0 @@
# Task 01 — The one-rule fix: make the scroll card the containing block
**Phase:** `86_history_page_width` · **Source:** `TODO.md:4` — "The history page appears to be the width of the table despite the table being scrollable. On mobile this results in half the page being blank and awkwardly scrollable."
**Story:** n/a (owner bug report — `TODO.md` L4)
## Objective
`document.documentElement.scrollWidth` on the History (and Tokens) page at 375px drops from 626/618 to the viewport width, because the card that hosts the tables becomes the containing block for the positioned `.visually-hidden` spans instead of the initial containing block.
## Work
1. `frontend/assets/styles.css` — in the `.table-wrap` rule (the shared scroll card: the rule declaring `background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow); overflow-x: auto;`) add **`position: relative;`** as the first declaration, with a comment above it (house comment style, citing the phase):
- the card is the containing block for the `position: absolute` `.visually-hidden` elements it hosts (the table `<caption>` and the Actions column header span in `#view-history` / `#view-tokens`);
- without a positioned ancestor their 1px boxes are laid out against the initial containing block — at the 640px table's right edge — and leak into the DOCUMENT's scrollable overflow (the page appeared "the width of the table", TODO.md L4; the Tokens view carried the identical defect);
- zero-offset positioning changes no layout; the spans stay clipped by their own `clip: rect(0 0 0 0)` + 1px box; cards without such spans (the RAG `.docs-table`, the git-sources table) render unchanged.
2. `tests/unit/test_table_wrap_stacking.py` (new — house source-level pattern, the `tests/unit/test_hamburger_nav.py` style: read `frontend/assets/styles.css` as text, no browser):
- extract the `.table-wrap` rule's declaration block (the block opened by the selector `.table-wrap` — NOT `.md-table-wrap` and NOT the `#git-sources-table-wrap` / `#tokens-table-wrap` id rules: match the exact selector, e.g. the selector text is exactly `.table-wrap`);
- assert the block contains `position: relative` (regex `position:\s*relative`);
- assert the same block still contains `overflow-x: auto` (the scroll contract the fix preserves);
- module docstring: the mechanism + the measured bug basis (History 626 / Tokens 618 / RAG 375 at 375px, 2026-09-07) so the pin's intent survives.
3. `- ASSUMPTION: the property is added to the shared .table-wrap rule (not to .history-table-wrap / #tokens-table-wrap individually) — owner-confirmed A3; if the executor finds the rule already position:relative (it is not, verified 2026-09-07), stop and flag instead of proceeding.`
## Testing & Quality
- Unit: `uv run pytest tests/unit/test_table_wrap_stacking.py -v` green (the two pins).
- No integration test needed (no `app/` change); the existing suite must stay green (phase gate in task 03).
- Coverage: **>90%** on `app/` unaffected.
## Completion Criteria
- [ ] The `.table-wrap` rule reads `position: relative;` first, `overflow-x: auto;` still present; no other rule in the file changed.
- [ ] `uv run pytest tests/unit/test_table_wrap_stacking.py -v` green.
- [ ] `git diff --stat -- frontend/` shows ONLY `frontend/assets/styles.css` (one property + comment).
- [ ] No behavior change in completed work (verified by the E2E gates in tasks 02/03).
@@ -1,31 +0,0 @@
# Task 02 — The E2E story: page-width contract for the table views
**Phase:** `86_history_page_width` · **Source:** `TODO.md:4` — "The history page appears to be the width of the table despite the table being scrollable. On mobile this results in half the page being blank and awkwardly scrollable."
**Story:** n/a (owner bug report — `TODO.md` L4)
## Objective
A dedicated Playwright suite (one file per story, run in isolation — AGENTS.md rule 4/9) pins the fixed contract: at 375px the History and Tokens pages are viewport-width (no document-level pan), their tables still scroll inside the card, the RAG view and the desktop layout are unchanged.
## Work
1. `tests/e2e/test_history_page_width.py` (new) — house E2E conventions (module docstring with the Source line, the run-in-isolation command `uv run pytest tests/e2e/test_history_page_width.py -v --no-cov` — DB up, and the Test → story mapping list; a fresh 375×812 page via the session `browser` fixture, the `login` helper from `e2e.auth_helpers` for the admin, `DESKTOP = 1280×800` for the regression):
- **`test_history_page_is_viewport_width`** — admin at 375×812, direct `page.goto(app_url + "/history.html")`, wait for the settled admin state (the existing pattern: a `wait_for_function` on the admin nav link reveal, copied from `test_mobile_hamburger_nav.py`'s `_wait_settled_admin`):
- `document.documentElement.scrollWidth <= window.innerWidth` (THE pin — 626 → ≤375);
- `#history-table-wrap`: `scrollWidth > clientWidth` (the 640px table still scrolls INSIDE the card — the phase-07 / AGENTS.md-rule-5 full-width contract preserved) and `clientWidth` equals the container's content width (≤ `innerWidth`);
- the visually-hidden Actions header + caption are still in the DOM (`#history-table-wrap th .visually-hidden` and `caption.visually-hidden` present — the a11y names survived the fix).
- **`test_tokens_page_is_viewport_width`** — the same two pins for `/tokens.html` (direct load; `#tokens-table-wrap`) — the folded-in identical defect (measured 618 pre-fix).
- **`test_spa_switch_keeps_page_width`** — from `/history.html`, open the mobile menu (`page.click("#nav-toggle")` — real click; the phase-85 contract, or the desktop inline nav at 375px… NOTE: at 375px the nav links live in the dropdown, so open the menu first), click the "Chat" link to land on `/`, then click the History nav link back — hmm, simpler and sharper: at 375px open the menu on `/history.html`, click a nav link to `/tokens.html` (SPA — no document load: assert the URL changed WITHOUT a new document — `performance.getEntriesByType('navigation')` length unchanged or the router's pushState contract), and re-assert `scrollWidth <= innerWidth` on the Tokens view. (Keeps the invariant across the router, not just direct loads.)
- **`test_rag_view_regression`** — `/sources.html` at 375px: `scrollWidth <= innerWidth` (was already clean — pinned so the shared-rule change cannot regress it) AND the RAG table is still full-width inside its card (the `.table-wrap` containing the `.docs-table`: `scrollWidth > clientWidth` on a 375px viewport — the 640px `min-width` still engages).
- **`test_desktop_unchanged`** — 1280×800 admin: on `/history.html`, `/tokens.html`, `/sources.html` — `scrollWidth <= innerWidth` (no overflow at any width) and each table's `clientWidth` > the mobile 346px (the tables render at container width, no card-internal scroll needed — `wrap.scrollWidth <= wrap.clientWidth + 1`).
- Shared helpers in the module: `_mobile_page(browser)`, `_wait_settled_admin(page)` (copied, not imported — house style keeps suites self-contained; `auth_helpers.login` IS imported, as every suite does).
2. `- ASSUMPTION: the SPA-switch test asserts the router contract via the URL + a re-probe of scrollWidth (not via a navigation-timeline count) — the router's pushState leaves one document; if the executor finds a cleaner existing probe in the phase-76/77 E2E suites (e.g. test_nav_switch_keeps_stream.py), reuse that pattern verbatim.`
3. `- ASSUMPTION: no data setup — the contract holds with an EMPTY table too (the `min-width: 640px` forces the 640px width regardless of rows); if a suite fixture provides saved chats/tokens, the test must not depend on them.`
## Testing & Quality
- E2E (this task's gate): `uv run pytest tests/e2e/test_history_page_width.py -v --no-cov` green **in isolation** (DB up — the conftest `db_ready`; mock LLM via conftest for the login helper).
- The pre-fix measurement (History 626, Tokens 618) is the counterfactual: with the task-01 CSS change the pins pass; without it `test_history_page_is_viewport_width` fails (the executor verifies the test is live by temporarily reverting the CSS in a scratch branch/`git stash` run — then restores — and records the red→green in the session log).
- Coverage: >90% on `app/` unaffected (test-only task).
## Completion Criteria
- [ ] The five tests exist, the module docstring maps each to the contract (the mapping list is the suite's README).
- [ ] `uv run pytest tests/e2e/test_history_page_width.py -v --no-cov` green in isolation; the red→green verification (CSS reverted → History test fails at `scrollWidth <= innerWidth`; restored → green) is recorded.
- [ ] No change to `app/`, `frontend/`, conftest, or other suites.
@@ -1,32 +0,0 @@
# Task 03 — Full gate + atomic commit
**Phase:** `86_history_page_width` · **Source:** `TODO.md:4` — the History-page-width bug report (TODO.md L4)
**Story:** n/a (owner bug report — `TODO.md` L4)
## Objective
Run the complete phase gate, land the phase as one atomic commit, and move the phase directory to `complete/`.
## Work
1. **Full regression gate** (AGENTS.md rule 9):
- `uv run pytest` — unit + integration green.
- `uv run pytest --cov=app --cov-report=term-missing` — `app/` coverage **>90%** (no `app/` change this phase — confirm the floor is held).
- `uv run pytest tests/e2e/test_history_page_width.py -v --no-cov` — green **in isolation** (this phase's E2E story — the page-width contract).
- `uv run pytest tests/e2e/test_chat_history.py -v --no-cov` — green in isolation (the History BEHAVIOR suite — the fix must not have touched row actions, refresh, or the gate).
- `uv run pytest tests/e2e/test_api_tokens.py -v --no-cov` — green in isolation (the Tokens BEHAVIOR suite — the folded-in view's regression proof).
- `uv run pytest tests/e2e/test_smoke.py -v --no-cov` — green in isolation.
- `uv run ruff check . && uv run pyright` — clean.
2. **Manual live check** (keep the output in the session log): dev server up, 375px viewport, signed in: open `/history.html` — the page no longer pans horizontally (drag/scroll right at the edge — nothing moves past the viewport); the table card scrolls its columns internally (swipe the card, not the page); `/tokens.html` the same; `/sources.html` unchanged; 1280px — all three tables render at container width with no card-internal scroll.
3. **Commit** (AGENTS.md rule 8 — one atomic, Conventional-Commits commit, always `--no-gpg-sign`), staging `frontend/assets/styles.css`, `tests/unit/test_table_wrap_stacking.py`, `tests/e2e/test_history_page_width.py`, and the phase files:
`fix(ui): stop the History and Tokens tables from stretching the document width`
— body: TODO.md L4 — on mobile the History page panned ~250px into a blank region (document scrollWidth 626 at 375px) even though the table scrolled correctly inside its card. Root cause: the `.visually-hidden` Actions header span (and caption) are `position: absolute` with no positioned ancestor, so their 1px boxes are laid out against the initial containing block — at the 640px table's right edge — and leak into the document's scrollable overflow. `position: relative` on the shared `.table-wrap` card makes it the containing block (zero layout change; the spans stay, still clipped); the identical defect in the Tokens view (measured 618) is fixed by the same rule; the RAG view was clean and is regression-pinned. New source-level unit pin + dedicated E2E story (History + Tokens + RAG + desktop).
4. Move the phase directory: `mv .agents/phases/todo/86_history_page_width .agents/phases/complete/` and include the move in the same commit.
## Testing & Quality
- This task IS the phase-level gate — the commands above are the completion evidence.
- Coverage: >90% held.
## Completion Criteria
- [ ] All seven gate commands green (unit + integration, coverage >90%, the new E2E story + `test_chat_history.py` + `test_api_tokens.py` + smoke in isolation, ruff + pyright).
- [ ] The live check shows no page-level horizontal pan on the two table views at 375px (output kept in the session log).
- [ ] Exactly one new commit; `git show --stat HEAD` lists the staged files above + the phase files (todo → complete move) — nothing else (in particular `app/` and the table markup untouched).
- [ ] `.agents/phases/complete/86_history_page_width/` exists; `todo/` no longer contains it.