Skip to content

Hi-Fi Pages: implement every screen of the design deck - #303

Merged
antosubash merged 66 commits into
mainfrom
worktree-hifi-pages
Sep 4, 2026
Merged

antosubash merged 66 commits into
mainfrom
worktree-hifi-pages

Conversation

@antosubash

Copy link
Copy Markdown
Owner

Implements every screen of the Claude Design "Hi-Fi Pages" deck (28 screens: public auth, app, ops, shell) across the framework, packages/ui and eleven modules.

  • Spec, with the Deliberate departures table that is the contract with the deck: docs/superpowers/specs/2026-09-03-hifi-pages-design.md
  • Plan: docs/superpowers/plans/2026-09-03-hifi-pages.md; per-screen gap analysis under docs/superpowers/specs/hifi-gap/
  • Evidence: 65 screenshots at 1440/390 in qa-shots/hifi/, browsable via qa-shots/hifi/index.html (open locally)

What changed

Shared (packages/ui, hosting, core)

  • Deck primitives: StatCard, SegmentedControl, ConfirmActionDialog, PasswordInput/PasswordStrength, relative-time helpers, initials, theme boot (no white flash before dark mode).
  • Shell: solid active nav pill in both sidebars, topbar log out + locale pill, phone bar with title/back/action, drawer, AuthCardShell with card / split-dark / split-light variants.
  • Hosting: SessionMiddleware subclass with a 30-day signature window and a per-session absolute expires_at (14 days, 30 with "Keep me signed in"); required_permission on 403 pages; diagnostics result kept on app.state; Alembic revision listing for Doctor.
  • AuditLink gains table_name + label_resolver; make gen-i18n regenerates the i18n key files without booting the app.

Modules

  • users / keycloak: login (remember me, provider divider), register, forgot/reset, verify, accept-invite, Keycloak interstitials + /keycloak/logged-out; admin users list (invited vs unverified via new invited_at), add people (chips, message, last batch), edit user (recent activity from the audit log), profile (password change, "Sign out everywhere" via session_version, preferences).
  • permissions: role editor and user-grants editor.
  • settings: store with scope tabs + server-side paging, new-override page with resolved value panel, module settings with env/default meta and test-connection state; DSN passwords are masked whatever the field is called.
  • feature_flags, file_storage: deck tables, uploads card with cancel/retry, bulk delete, shared confirms, audit links.
  • background_tasks: executions with windowed tiles and queue filter, bulk "Retry all failed" (idempotent, bounded), task detail with terminal traceback, workers with uptime and redacted broker URL.
  • audit_log: deck browse screen, streaming CSV export (formula-escaped), entity display labels, actor-by-name filter.
  • dashboard, branding: new stat/module tiles, Doctor on real diagnostics + migrations, branding editor with dirty tracking, presets, footer text and live preview.

Data: one migration, f53464f5ac43 (users.invited_at, users.session_version).

New endpoints: POST /api/users/admin/{id}/resend-invite, POST /api/users/me/password (rate-limited), POST /api/users/me/sessions/revoke-all, POST /api/file-storage/files/bulk-delete, POST /api/background-tasks/executions/retry-failed, GET /api/audit_log/export.csv, POST /admin/doctor/rerun, GET /keycloak/logged-out.

Verification (at c591e33; the last commit only adds the gallery HTML)

Gate Result
uv run pytest 2739 passed (baseline 2700)
npx vitest run 393 passed, 47 files
make lint clean (ruff, ty, biome, tsc ×13, file-size, hardcoded-strings, untranslated, metadata, readmes)
make doctor 0 findings
e2e (tests/e2e, live server) 33 passed, incl. 3 new tests for remember-me, bulk delete and CSV export
npm run build ok
Design every screen compared against the deck at 1440 and 390

Follow-ups filed from the review

#292 bearer/sm_auth token lifetime · #293 settings store masking · #294 session_version cache · #295 order-dependent tests + coverage · #296 i18n bypasses · #297 ui a11y/deck fidelity · #298 bulk-retry race · #299 file-storage aggregates · #300 audit label privacy · #301 re-verify against the live deck (Keycloak login captures duplicate the signed-out page) · #302 gen-i18n silent failure.

Notes for review

…mActionDialog, password inputs, relative time, initials, theme
…ill, phone bar title/action/back, split auth shell, theme boot
…, icon-matched log out, drawer locale only when there is a choice
…eports removed ids, deleted rows leave the total
…reviews

Publish was live with nothing staged, so pressing it PUT the values the server
already held and reported success for doing nothing. With `primaryColor` unset
the preset row showed no active chip while the site was already rendering
emerald — the row now reads the effective colour, so it agrees with the swatch
beside it.

Severity values are lowercase per the deck, the banner placeholder is short
enough not to clip mid-word, and the form foot's sentence is the deck's. The
Sign-in and Email preview frames fit their content instead of stretching to
the form column and leaving a 300px empty band.

The Files "Uploaded by" trigger read "Anyone" when unfiltered — the selected
item, which says nothing about the column it narrows. It now names itself like
the Type trigger beside it.
The comment naming `font-[var(--font-display)]` was itself a source file
Tailwind scans, so the utility it warns about was still being generated into
the built CSS.
`HeadlessChrome/` has no word boundary before "Chrome", so the \b anchor sent
every headless session to the Safari token that every Chromium UA also
carries — the Sessions row read "This browser · Safari on Linux" in Chrome.
…d is called

The name rule (password|secret|api_key|...) never matched broker_url,
result_backend, redis_url or database_url, yet those are exactly where a
deployment keeps its Redis and Postgres credentials — and they are
env_readable, so both the module editor and the known_keys suggestion list
handed the value back in clear text.

_field_view now also judges each reading (value / default / env_value) by
urlsplit(...).password, masking per value so a password-free default stays
visible. _strip_mask_sentinels keys on the sentinel alone, or submitting the
form would store the row of dots over the real connection string.
… the loop

A live session plus a guessable current password was a free online oracle:
LoginRateLimiter only fronts /auth/login, so wrong guesses at /me/password
cost nothing. It now carries the same throughput budget as the other
credential-adjacent endpoints.

enforce_auth_throughput_limit moved from auth_local.api down to
auth_local.rate_limit — api already imports self_account, so importing it the
other way would have been a cycle. api re-exports it, so users.module's
mount sites are untouched.

verify_and_update is deliberately slow; awaiting it on the event loop stalled
every other request in the worker, so it goes through asyncio.to_thread like
token_preview's fingerprint check.
entity_label and actor come from a display name, which is whatever the
account holder typed. Excel, Sheets and LibreOffice evaluate a cell starting
with =, +, - or @, and a leading tab or CR slips one past that check — so an
export opened next to the screen it came from would run it.

Every cell is prefixed with the spreadsheet's own text marker when it leads
with one of those, applied per cell rather than to the two user-controlled
columns so a column added later cannot reopen the hole.
…eployment

Starlette signs and expires the session cookie with one number, so honouring
"Keep me signed in for 30 days" forced the signature window to 30 days for
everyone — an ordinary 14-day session stayed replayable for a month once its
cookie was lifted off disk.

Login now writes an absolute expires_at into the session (now +
cookie_max_age_seconds, re-stamped with remember_me_max_age_seconds when the
box is ticked) and UsersAuthProvider treats a session past it as signed out,
before either the cached-context path or the DB reload. The 30-day signer
stays. A session with no expires_at predates this and is accepted as legacy;
that fail-open is temporary and documented at session_has_expired.
_resolve_bearer looked the row up by value and stopped there. That path
bypasses fastapi-users' DatabaseStrategy, which is where the lifetime is
normally applied — so a token minted to last thirty days authenticated
forever. Same _TOKEN_LIFETIME_SECONDS as the strategy, or the two disagree
about what expired means.
_version_still_current runs on the cached-context path, which is most
requests, so the revocation check was one indexed primary-key read per page
load. A 30s per-process TTL cache collapses a page's worth of requests into
one read.

The trade is bounded and documented where it lives: a revocation made in
another worker takes up to 30s to be seen here, and the worker that made it
drops its own entry immediately (revoke-all and password change both call
forget_session_version), so the browser that pressed the button never sees
the stale answer. The fail-open/fail-closed asymmetry with _load_user is now
stated in _version_still_current's docstring.

The cache lives in users/session_version_cache.py rather than provider.py:
inlined it put provider.py at 293 of the 300-line cap. It is re-exported from
provider, so users.provider.forget_session_version is the import path.
Everything else under tests/e2e reads a page. These write: sign in with "Keep
me signed in" ticked and check the session cookie's expiry at both ends of the
window (and after a second page load, which is where a window recorded only on
the sign-in response gets rolled back); upload two files, delete them from the
file-storage table and check both the rows and the footer count; click Export
CSV and check the download's header row.

The delete test uploads what it deletes, so it does not depend on seed data.
authCta lost its last caller when PublicLayout took over the sign-in swap;
with it go ui.public_nav.log_in / sign_up (its only readers), ui.errors.http_badge,
users.common.log_in / back_to_login and users.login.needs_verification_title.
Every one was grepped across .tsx/.ts/.py first. es.json keeps parity with en
for the packages-ui catalog. Keys regenerated with make gen-i18n.
The count sat in the button's text with no separator, so the computed name
ran the label and the tally together — unusable as a voice target and read
aloud as one word. The count span is aria-hidden and the button carries an
explicit "{label} {count}" name; the visible layout is unchanged.
…sword

snapshot.error is str(exc) from kombu or redis, and those messages quote the
url they were dialling. It reaches the Workers page, the JSON admin snapshot
and Doctor — the screen an operator screenshots when the queue is down.

redact_broker_url alone was no help: a sentence is not a url, so urlsplit
finds no password in it. redact_urls locates each url inside the message and
runs the existing redaction over it, applied in WorkerInspector.snapshot so
no downstream reader can forget.
…ants

SESSION_VERSION_KEY, SESSION_USER_ID_KEY and SESSION_REMEMBER_KEY were
private literals here, duplicating users.constants and
simple_module_hosting.session. The comment justifying that said this module
does not import the framework's middleware — which stopped being true when it
started reading the per-session expiry.
…e departures

The endpoint table said /api/audit-log/export.csv; the api prefix is the
package name, so it is /api/audit_log/export.csv. Browse passes items[].

Three shipped decisions had no departure row: degraded module tiles are amber
rather than the deck's emerald-soft, WorkerHealthBanner stays on the tasks
index, and the users list keeps its dropdown filters, stat row and tabs at
390 instead of collapsing to search only.
Radix closes the dialog on the action's click, so every caller passing busy
had to pin open={confirming || busy} to stop it vanishing mid-request — the
prop disabled a button nobody could see. The click is now prevented, leaving
the dialog's fate to open. Callers that never mention busy are closed here,
so nothing that relied on auto-close is left with a stuck dialog.
initTheme() runs after React mounts, so a dark-mode user got a white page for
the length of the bundle download. A blocking inline script in <head> reads
the same sm.theme key theme.ts writes and applies the same rule; theme.ts
stays the source of truth for every later change. Inline is allowed: the CSP
carries 'unsafe-inline' for scripts because Vite's preamble needs it.
The kwarg was popped, so a deployment could set it, see no error, and believe
it had shortened its sessions. It now raises and names the two knobs that do
work. Also reformats test_session_expiry.py.
…screen

detail=str(exc) echoed whatever the transport said — which quotes the host,
the port and sometimes the credential it tried — straight into the invite
result rendered in the admin UI. Both mint sites (bulk invite and resend) now
return one generic sentence and log the real reason with logger.exception, so
it is a correlation id away rather than in a screenshot.
generate_verification_token and mint_invite_token built the same JWT from the
same three claims and differed only in whether invited_by was added — and the
design doc already names mint_invite_token as the single helper. The survivor
takes invited_by=None, which is what a plain verification link wants.
… cap

The secret-detection rules (a name pattern, the DSN check and the mask) move
to settings/_secrets.py and are re-exported, so every existing import site is
untouched; the formula-injection tests move to their own file, because
test_export_csv.py asks whether the export holds the right rows and this asks
whether opening it is safe.
Every deck screen captured against the branch's final code (:8301), desktop
1440x900 and phone 390x720, by
.superpowers/sdd/2026-09-03-hifi-pages/shots.py. Committed as the verification
evidence for the hi-fi pass — qa-shots/ is already tracked in this repo.
Self-contained index.html next to the 65 captures: one card per deck
screen with the 1440/390 shots, the spec's departures table, the
verification gates at c591e33 and links to the follow-up issues
#292-#302 filed from the review.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Sep 4, 2026

Copy link
Copy Markdown

Deploying simple-module-python with  Cloudflare Pages  Cloudflare Pages

Latest commit: 12e6434
Status: ✅  Deploy successful!
Preview URL: https://ad40590a.simple-module-python.pages.dev
Branch Preview URL: https://worktree-hifi-pages.simple-module-python.pages.dev

View logs

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 4, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
🔒 Security Review ⚠️ Failed 2026-09-04T07:17:28.285656Z 69e7591 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

The perf guards' logged_in_page fixture still looked for the pre-deck
"Log in" nav link and submit button, so every guard errored at setup
on CI. Same drift the e2e suite already fixed; verified against a
production-mode build locally (5 passed).
@antosubash
antosubash merged commit f7740e4 into main Sep 4, 2026
13 checks passed
antosubash added a commit that referenced this pull request Sep 8, 2026
The hi-fi pages rewrite (#303) landed while this sat open, and it moved or
rewrote every surface the sweep touched. Re-applied the fix to where the text
now lives rather than to the conflict text:

  Doctor.tsx migrations block  -> pages/components/doctor/MigrationsCard.tsx
  UploadProgressRows.tsx       -> pages/components/UploadsCard.tsx
  settings Browse.tsx table    -> pages/components/StoreTable.tsx
  users UserRow.tsx            -> rewritten in place as a stretched row link

Admin.tsx merged clean and keeps its `title`.

Two things changed on the way:

- The settings description column no longer exists, so only the value carries
  a `title`. It repeats what is rendered, which keeps a masked secret masked.
- The uploads row now truncates the failure text too, so that gets the same
  treatment; both strings come from the server and are unbounded.

The users row needed more than a moved attribute. Main's `UserRow` is a link
stretched over the whole row by a `before:inset-0` overlay, and the overlay
answers the hover. Measured in Chromium with elementFromPoint: a `title` on
the link is announced over every cell in the row — hovering Last login pops
the member's name — and the `title` on the address line below never fires at
all, because the overlay sits on top of it. Both lines are therefore lifted
back above the overlay with `relative`, which was already the file's own
device for keeping the kebab clickable. The name is wrapped rather than
lifted whole so it stays inside the anchor and still opens the user; the
anchor still clips, ellipsis confirmed at 170px. The one cost is that the
address line is no longer part of the row-wide click target.

Skipped, consistent with the original reasoning: StoreCards and UserCards are
the phone variants of these two tables, and `title` does nothing on touch.

make lint clean, 2973 Python passed, 463 JS passed.

Claude-Session: https://claude.ai/code/session_011URawzYPb8nL2gfGh2yFVn
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant