phase: 101_tokens_page_overhaul
Build and Push Containers / build-and-push-app (push) Successful in 1m49s
Build and Push Containers / build-and-push-db (push) Successful in 13s

All gates verified green — no defects found, nothing to fix. Final report:

## Phase 101 — final verification pass (all tasks already complete; verified, not re-implemented)

**Verified against the working tree** (task files 01–04 present in `complete/`):
- Active/revoked table split (Status column gone, section hidden while 0 revoked, per-table search with query state surviving re-renders) in `frontend/index.html` + `tokens.js`
- `generateToken()` refuses blank/whitespace names (`Give the token a name first.`, no request); `|| "token"` fallback deleted
- Atomic rotation: `regenerate_token` service + `POST /api/tokens/{id}/regenerate` (201 new-plaintext-once / 404 / 409 / router-wide 403) + `TokenListItem.revoked_at` (D5)
- Regenerate two-step confirm UI + CSS (`.token-regenerate`, neutral hover, no new hue); A4 pins intact

**Test / lint / coverage results:**
- `uv run pytest` → **2065 passed**
- `uv run pytest --cov=app --cov-report=term-missing` → **TOTAL 99%** (>90% ✓)
- `uv run ruff check . && uv run pyright` → clean (0 errors)
- `uv run pytest tests/e2e/test_tokens_page.py -v --no-cov` → **4 passed** (isolation, DB up)
- Regression, each in isolation: `test_api_tokens.py` **9 passed**, `test_admin_auth.py` **6 passed**, `test_shared_header.py` **6 passed**, `test_theme_semantic_completion.py` **8 passed** (its revoked-pill pin was correctly re-scoped to the revoked table in this phase)

**Completion criteria:** 1 ✓ split+search (E2E 1–2) · 2 ✓ required name (E2E 3 + source pin) · 3 ✓ rotation end-to-end, old token refused at gate (E2E 4 + API 404/409 pinned) · 4 ✓ A4 holds (list carries no plaintext/hashes) · 5 ✓ suite/coverage/lint green · 6 ✓ E2E + regressions green in isolation · 7 commit left to the harness per executor rules (all changes uncommitted in the working tree)

**Deviations:** none. Next pending phase: `98_sync_summary_visibility`.
This commit is contained in:
2026-09-12 15:16:02 -04:00
parent 58e9d94cff
commit 4dbac1660a
32 changed files with 2321 additions and 150 deletions
+82 -22
View File
@@ -913,21 +913,22 @@
lines, nothing else. -->
<span class="tokens-status" id="tokens-status" role="status" aria-live="polite"></span>
<!-- The create row: label (1–120 — the server re-validates; a
BLANK label sends "token", the placeholder documents the
fallback) + Generate. SHIPS hidden (anonymous-safe — no
dead control beside the gate; tokens.js reveals it for
admin only). The button is type="button" (no form submit)
and runs the §7.4 never-stale lifecycle ("Generating…"
while the POST is in flight, re-enabled on success AND
failure). -->
<!-- The create row: name (1–120 — the server re-validates; a
BLANK name is REFUSED client-side — tokens.js announces
"Give the token a name first." and sends NOTHING, phase
101 D3; the server's 422 is defense in depth) +
Generate. SHIPS hidden (anonymous-safe — no dead control
beside the gate; tokens.js reveals it for admin only).
The button is type="button" (no form submit) and runs the
§7.4 never-stale lifecycle ("Generating…" while the POST
is in flight, re-enabled on success AND failure). -->
<div class="token-create" id="token-create" hidden>
<input
id="token-label"
type="text"
maxlength="120"
placeholder="e.g. alice"
aria-label="Token label"
placeholder="e.g. alice — required"
aria-label="Token name"
autocomplete="off"
>
<button type="button" class="token-generate" id="token-generate">Generate</button>
@@ -959,16 +960,35 @@
</div>
</div>
<!-- The full-width table (AGENTS.md rule 5 — no skinny list):
Label | Created (locale date+time, full ISO on hover) |
Last used (locale or "never") | Status (Active em-dash vs
the rose Revoked pill — the stale-pill language) | Actions
(Revoke — the inline two-step confirm; Revoked rows carry
NO action — nothing left to revoke). tokens.js fills
#tokens-tbody; #tokens-empty-row ships hidden and is
revealed by a 0-row fetch (and a failed load). The Actions
column header is visually-hidden — the row buttons carry
their own aria-labels (the history-table convention). -->
<!-- Phase 101 (task 02, D4): the ACTIVE table's live label
search — a client-side, case-insensitive SUBSTRING filter
over the rendered rows (no fetch). SHIPS hidden with the
create row (tokens.js reveals it in the admin branch);
the query lives in module state and is RE-APPLIED after
every loadTokens() — a re-render never loses it. -->
<input
type="search"
id="token-search-active"
aria-label="Search active tokens"
placeholder="Search active tokens…"
class="token-search"
hidden
>
<!-- The full-width ACTIVE table (AGENTS.md rule 5 — no skinny
list): Label | Created (locale date+time, full ISO on
hover) | Last used (locale or "never") | Actions (Revoke
— the inline two-step confirm; phase 101 task 03 adds
Regenerate before it). The Status column is GONE (phase
101 D1 — a table that is all-active needs no status; the
table's position IS the status). tokens.js fills
#tokens-tbody with the revoked === false rows only;
#tokens-empty-row ships hidden and is revealed by a
0-active-row fetch (and a failed load); #tokens-no-match-row
(its text JS-filled) is the D4 search no-match state. The
Actions column header is visually-hidden — the row
buttons carry their own aria-labels (the history-table
convention). -->
<div class="table-wrap tokens-table-wrap" id="tokens-table-wrap" role="region" aria-label="Access tokens" tabindex="0">
<table class="tokens-table" id="tokens-table">
<caption class="visually-hidden">Access tokens issued to people who use the app — newest first</caption>
@@ -977,13 +997,53 @@
<th scope="col">Label</th>
<th scope="col">Created</th>
<th scope="col">Last used</th>
<th scope="col">Status</th>
<th scope="col"><span class="visually-hidden">Actions</span></th>
</tr>
</thead>
<tbody id="tokens-tbody">
<tr class="tokens-empty-row" id="tokens-empty-row" hidden>
<td colspan="5">No tokens yet — generate one above and hand it out.</td>
<td colspan="4">No tokens yet — generate one above and hand it out.</td>
</tr>
<tr class="tokens-empty-row" id="tokens-no-match-row" hidden>
<td colspan="4"></td>
</tr>
</tbody>
</table>
</div>
<!-- Phase 101 (task 02, D1 + D4): the REVOKED section — BELOW
the active table: a visible sub-heading + its own live
label search + its own full-width table (Label | Created
| Last used | Revoked — the revoked_at date, locale
date+time, full ISO on hover). SHIPS hidden — tokens.js
shows it when ≥ 1 token is revoked (an empty table is
noise). The table IS the status: the em-dash /
.stale-pill column is gone from BOTH tables. Revoked rows
carry NO actions (nothing left to revoke or rotate —
task 03's Regenerate lives in the active table only). -->
<h2 id="tokens-revoked-heading" class="tokens-revoked-heading" hidden>Revoked tokens</h2>
<input
type="search"
id="token-search-revoked"
aria-label="Search revoked tokens"
placeholder="Search revoked tokens…"
class="token-search"
hidden
>
<div class="table-wrap tokens-table-wrap" id="tokens-revoked-wrap" role="region" aria-label="Revoked tokens" tabindex="0" hidden>
<table class="tokens-table" id="tokens-revoked-table">
<caption class="visually-hidden">Revoked tokens — newest first</caption>
<thead>
<tr>
<th scope="col">Label</th>
<th scope="col">Created</th>
<th scope="col">Last used</th>
<th scope="col">Revoked</th>
</tr>
</thead>
<tbody id="tokens-revoked-tbody">
<tr class="tokens-empty-row" id="tokens-revoked-no-match-row" hidden>
<td colspan="4"></td>
</tr>
</tbody>
</table>