Hi-Fi Pages: implement every screen of the design deck - #303
Merged
Merged
Conversation
…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
…lete, shared confirms
…ing editor per the hi-fi deck
…nding header actions
…ng, tolerant store paging
…eports removed ids, deleted rows leave the total
…t failed resends, stale e2e selectors
…dmin-set footer text
…rs, gate resend to pending invites
…formats markdown code blocks)
…eset the session after a failed resolver
…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.
Deploying simple-module-python with
|
| 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 |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
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
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements every screen of the Claude Design "Hi-Fi Pages" deck (28 screens: public auth, app, ops, shell) across the framework,
packages/uiand eleven modules.docs/superpowers/specs/2026-09-03-hifi-pages-design.mddocs/superpowers/plans/2026-09-03-hifi-pages.md; per-screen gap analysis underdocs/superpowers/specs/hifi-gap/qa-shots/hifi/, browsable viaqa-shots/hifi/index.html(open locally)What changed
Shared (
packages/ui, hosting, core)StatCard,SegmentedControl,ConfirmActionDialog,PasswordInput/PasswordStrength, relative-time helpers,initials, theme boot (no white flash before dark mode).AuthCardShellwithcard/split-dark/split-lightvariants.SessionMiddlewaresubclass with a 30-day signature window and a per-session absoluteexpires_at(14 days, 30 with "Keep me signed in");required_permissionon 403 pages; diagnostics result kept onapp.state; Alembic revision listing for Doctor.AuditLinkgainstable_name+label_resolver;make gen-i18nregenerates 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 newinvited_at), add people (chips, message, last batch), edit user (recent activity from the audit log), profile (password change, "Sign out everywhere" viasession_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)uv run pytestnpx vitest runmake lintmake doctortests/e2e, live server)npm run buildFollow-ups filed from the review
#292 bearer/
sm_authtoken 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) · #302gen-i18nsilent failure.Notes for review
expires_atand are accepted as legacy; the fail-open is documented insession.pyand should flip to fail-closed once deployed sessions have turned over (Bearer/sm_authtokens ignore the per-session expiry and their advertised lifetime #292).qa-shots/is tracked in this repo, so the 65 PNGs (6.5 MB) are committed as verification evidence in their own commit.