phase: 156_table_ux_polish
Build and Push Containers / build-and-push-app (push) Successful in 2m15s
Build and Push Containers / build-and-push-db (push) Successful in 11s

Everything is verified. All gates green on the current working tree. Final report:

**Phase 156 — final verification pass (all 4 task files present in `complete/` — no outstanding work)**

- **Verified (not re-implemented):** bounded-panel `.table-wrap` (`max-height: min(70vh, 64rem)`, `overflow: auto`, opaque sticky `th` via `var(--brand-soft)`), `.cell-clip` ellipsis rule + the five label families in the three renderers, the added aria-labels, unit pins (incl. `.md-table-wrap` byte-unchanged scope guard, `#git-sources-table-wrap` harmony — no divergent rule), dedicated E2E suite (sticky ×3 tables, horizontal contract, computed-style ellipsis + reachable full text, mobile hamburger + scroll + pin)
- **Tests/lint/coverage:** `uv run pytest --cov=app --cov-report=term-missing` → **3570 passed, 99% on `app/`** (gate >90%); `uv run pytest tests/e2e/test_table_ux_polish.py -v --no-cov` (DB up) → **6/6 passed in isolation**; `uv run ruff check . && uv run pyright` → **clean** (0 errors)
- **Completion criteria:** sticky header all 3 tables (exact-0 offset while `scrollTop > 0`) ✅ · horizontal scroll unchanged ✅ · ellipsis computed style + actual clip + title/aria reachable ✅ · mobile ≤640px contract ✅ · full suite/E2E/lint/coverage ✅ · atomic commit: **harness commits this phase after the pass** (per pipeline protocol — no git history commands run by me)
- **Documentation:** AGENTS.md phase-156 wording (bounded panels, sticky headers, `.cell-clip` + aria-label idiom) matches the implementation exactly — verified consistent; README.md reviewed in full (pages/URLs vs registered routes, all `.env.example` vars documented, Quick Start, eval/llm scripts, compose prod profile, E2E examples) — verified consistent, no changes needed (README describes no table layout/scroll, so no phase-156 mention required)
- **Deviations:** none — all work was already in the working tree; this pass re-verified every gate and the docs
- **Next pending phase:** `157_chat_chip_cap`
This commit is contained in:
2026-10-04 20:46:57 -04:00
parent 742eef955b
commit 1513981c1c
33 changed files with 1790 additions and 58 deletions
+54 -5
View File
@@ -33,6 +33,14 @@ documents) + ``header.js``'s reveal-for-admin block;
between Updated and Share in the shell's History view, and the
``.stale-pill`` rose-family CSS in ``styles.css``.
Phase 156 (task 02): the ellipsis hint on the History action labels —
the ``Create link`` / ``Copy`` / ``Unshare`` / ``Delete`` visible
texts are ``.cell-clip`` spans (a squeezed cell shows "…" instead of
a hard cut) and the audit's accessible-name check lands: every one
of the four buttons' ``aria-label`` names the chat title (the
phase-66 idiom — nothing was missing, the pins keep it), so the
full text stays reachable.
The Containerfile stage-1 coverage (the shell copied, the view modules
bundled into the router) is pinned dynamically by
``tests/integration/test_containerfile_assets.py`` — a page or module
@@ -410,6 +418,15 @@ def test_two_step_delete_confirm_pair() -> None:
fn = _fn(js, "makeDeleteControl")
assert 'del.className = "history-delete"' in fn
assert 'del.setAttribute("aria-label", `Delete saved chat: ${chat.title}`)' in fn
# Phase 156 (task 02, A3) audit: the Delete button's aria-label
# names the row's identity (the chat title — the phase-66 idiom),
# and the visible text is the .cell-clip span — a squeezed cell
# shows "…" instead of a hard cut (the full text stays reachable).
assert 'delClip.className = "cell-clip"' in fn, (
"the clip label span (phase 156, A3)"
)
assert 'delClip.textContent = "Delete"' in fn, "the visible text"
assert "del.append(delClip)" in fn, "the span is the button's label"
assert 'label.textContent = "Delete?"' in fn
assert 'yes.className = "history-confirm-yes"' in fn
assert 'no.className = "history-confirm-no"' in fn
@@ -489,7 +506,10 @@ def test_history_table_css_full_width_and_palette() -> None:
title link is the accent link (brand-ink, focus-visible); the
confirm pair is Yes-on-error-rose + No-ghost; the empty-state row
is the muted centered message. Every pair is Phase-08 AA
(brand-ink/brand-soft 6.9:1, err 9.1:1, ink-soft >=6.9:1)."""
(brand-ink/brand-soft 6.9:1, err 9.1:1, ink-soft >=6.9:1).
Phase 156 (task 01): the thead's brand-soft background moved to the
ONE .table-wrap table thead th sticky rule — .history-table th
keeps the text styling only."""
css = _css()
block = re.search(r"\.history-table \{([\s\S]*?)\n\}", css)
assert block, "styles.css must style .history-table"
@@ -497,7 +517,14 @@ def test_history_table_css_full_width_and_palette() -> None:
assert "width: 100%" in body, "the table is FULL-WIDTH (AGENTS.md rule 5)"
assert "min-width: 640px" in body
th = re.search(r"\.history-table th \{([\s\S]*?)\n\}", css)
assert th and "var(--brand-soft)" in th.group(1) and "var(--brand-ink)" in th.group(1)
assert th and "var(--brand-ink)" in th.group(1), (
"the th keeps its brand-ink text styling"
)
sticky = re.search(r"\.table-wrap table thead th \{([\s\S]*?)\n\}", css)
assert sticky and "var(--brand-soft)" in sticky.group(1), (
"the thead's brand-soft background is the ONE .table-wrap "
"table thead th rule (phase 156)"
)
hover = re.search(r"\.history-table tbody tr:hover \{([^}]*)\}", css)
assert hover, "row hover is part of the table family"
link = re.search(r"\.history-title-link \{([\s\S]*?)\n\}", css)
@@ -639,17 +666,39 @@ def test_share_control_three_states_and_two_step_unshare() -> None:
assert "renderShareShared(chat, cell)" in make
assert "renderShareUnshared(chat, cell)" in make
# Unshared state: the Create link button (labeled, textContent).
# Phase 156 (task 02, A3): the visible text is the .cell-clip
# span (a squeezed Share cell shows "…" instead of a hard cut);
# the aria-label names the chat title (the phase-66 idiom — the
# audit: every one of the four History action buttons already
# carries an accessible name that names the row's identity, so
# the full text stays reachable and nothing was missing).
unshared = _fn(js, "renderShareUnshared")
assert 'create.className = "history-share-create"' in unshared
assert 'create.textContent = "Create link"' in unshared
assert 'createClip.className = "cell-clip"' in unshared, (
"the clip label span (phase 156, A3)"
)
assert 'createClip.textContent = "Create link"' in unshared
assert "create.append(createClip)" in unshared
assert 'create.setAttribute("aria-label", `Create share link: ${chat.title}`)' in unshared
assert "innerHTML" not in unshared, "XSS contract: textContent only"
# Shared state: Copy + Unshare, then the two-step confirm.
shared = _fn(js, "renderShareShared")
assert 'copy.className = "history-share-copy"' in shared
assert 'copy.textContent = "Copy"' in shared
assert 'copyClip.className = "cell-clip"' in shared, (
"the clip label span (phase 156, A3)"
)
assert 'copyClip.textContent = "Copy"' in shared
assert "copy.append(copyClip)" in shared
assert 'unshare.className = "history-unshare"' in shared
assert 'unshare.textContent = "Unshare"' in shared
assert 'unshareClip.className = "cell-clip"' in shared, (
"the clip label span (phase 156, A3)"
)
assert 'unshareClip.textContent = "Unshare"' in shared
assert "unshare.append(unshareClip)" in shared
# The audit's aria-label additions — present (the phase-66 idiom
# predated the audit; the pin keeps them):
assert 'copy.setAttribute("aria-label", `Copy share link: ${chat.title}`)' in shared
assert 'unshare.setAttribute("aria-label", `Unshare saved chat: ${chat.title}`)' in shared
assert 'label.textContent = "Unshare?"' in shared
assert 'yes.className = "history-confirm-yes"' in shared, (
"the unshare two-step reuses the phase-50 .history-confirm-* pair"