Wireframe + hi-fi shell UI: topbar, ⌘K palette, honest lists - #271
Merged
Conversation
The last unbuilt frame on the screen-wireframe board. Its other captions were shipped by #261; 2e was labelled "proposals, not built" and stayed that way. Four list screens rendered the same shrug when they had no rows, and three of them could not tell the two reasons apart. "No users yet" is wrong when the workspace is full and the filter is merely too narrow, and "no audit entries" is worse than wrong — it reads as evidence that nothing happened, which is the last thing to tell someone working an incident. So the filtered case now names the filters doing the excluding and offers a way out of them, and the genuinely-empty case offers the one thing worth doing next. Adds a shared EmptyState wrapping the existing ui/empty primitives, so the four screens read as one treatment rather than four hand-rolled ones. file_storage already had the filtered/empty split and moves onto it. Per screen: * Users — a one-member workspace renders a perfectly valid table containing your own row and never suggests the obvious next step. A prompt now sits above that table rather than replacing it: the row is still the truthful answer to "who has access", and hiding it would cost the admin their only route to their own record. * Background tasks — "No task has run yet" is reassuring and wrong when the real cause is that no worker was ever started and the queue is filling unattended. The view now polls the fleet to tell those apart, plus a third case for an unreachable broker. Polling costs an inspect timeout, so it is gated to an unfiltered list that came back empty — the one moment the answer changes what the screen says. A filtered-empty list says nothing about the fleet and never pays for it. * Audit log — the empty panel echoes the applied filters, so the reader can see it is their query and not the record that is empty. * Files — promotes the existing dropzone into the empty table as the call to action, and adds Clear filters to the no-match case. Verified in the browser against the wireframe: solo prompt, filtered-empty users, empty tasks, and both audit-log states. Claude-Session: https://claude.ai/code/session_01NYVhsyUpmhhRbXtZk7EjAE
…d confirms and health signals The deck's turn 3 added frames 3a–3h, covering the eight screens that had no wireframe. 3h carries the only two items the deck labels "Rethink (not built)"; the rest are friction notes on shipped screens. Verified all eight against the code and implemented the seven gaps that were real. 3a — every public entry point 404'd. Landing's "Get started"/"Sign up" and all four PublicLayout buttons pointed at /auth/login, which is the JSON API prefix (/api/users/auth/login, POST) and has no page behind it. The paths now live in one place (packages/ui/src/lib/auth-routes.ts) and a test fails on any href back into /auth/. "Sign up" is gated on a new `signup` shared prop from the users module, because /users/register 404s when allow_signup is off — linking it unconditionally would have swapped one dead end for another. 3h — user delete now asks for the address to be typed; it was one click on a row that looks like every other row. Retry confirm names the task and shows the args/kwargs it is about to re-enqueue: a blind retry of a bad payload just fails the same way. List rows carry the payload for that one consumer. 3c — correlation_id was dead data: stored, selected, serialised into the page props, never rendered or queryable, so one request that touched four entities read as four unrelated rows. Adds a per-row pivot and a banner. 3f — a queue of twelve with a live worker and one with no worker render identically. The check is a lazy client fetch, not part of the render: celery's inspect waits out its full timeout even when healthy, so paying it on every page load would slow the normal path to answer a question it isn't asking. 3g — the workers snapshot only refreshes on demand, so a page left open showed a healthy fleet indefinitely. Ticks the reading's age and flags it stale. 3d — flag toggles wrote immediately with no confirm. The confirm restates the scope, which is the part you cannot see from the switch: the same control means "this tenant" or "everyone" depending on a selector further up the page. Not done, and deliberately: 3b (move uploads out of the table body, bulk select) and 3e (unify branding's three independent saves behind one dirty state) are page reshapes rather than fixes, and want their own review. Verified in a browser: both signup branches, the correlation pivot and its round trip, both confirm dialogs cancel-safe and write on confirm, the worker-health banner against a stubbed fleet, and the stale badge across the 60s threshold. make lint / make test (1926 py, 64 js) / make doctor all green. Claude-Session: https://claude.ai/code/session_01NYVhsyUpmhhRbXtZk7EjAE
…d a nav active state that works `Hi-Fi Pages.dc.html` renders 26 screens on one shared shell. The token layer it is drawn from is already the app's own — globals.css is literally themed "Emerald (SimpleModulePython HiFi)", same Sora / DM Sans / JetBrains Mono, same radius — and the per-screen content matches the real pages, because the deck was built by reading them. The delta is the shell: every app screen in the deck sits under a 56px topbar the app does not have. Adds it, carrying what the deck puts there: - Breadcrumb. `breadcrumb.tsx` had been vendored but never used. The section comes from the menu registry and the leaf from the page's own PageShell title, reported up through context — so all 18 app screens get a correct crumb with no per-page wiring and no route table to drift. The dynamic cases come out right for free: the user editor renders "Users / admin@example.com", which is exactly the crumb the deck specifies. - ⌘K palette over the same menu entries the sidebar renders, so it inherits their permission filtering and cannot offer a destination that would 403. Account actions are included, which is the keyboard route to log out. - Locale moved out of the sidebar header slot into the topbar beside search, where the deck has it; it read as part of the wordmark under the logo. The mobile bar keeps its own copy, since the topbar is desktop-only. Two defects found while wiring it up: - No sidebar item has ever been highlighted, on any page. `usePage().url` is absolute (`http://host/users/admin/add`) while menu urls are paths, so `currentUrl.startsWith(item.url)` was always false — the active-state design existed and the condition selecting it could not fire. Both the sidebar and the new breadcrumb now go through `isUnder()`, which compares paths segment-wise so `/users` cannot claim `/users-archive`. - `LocaleSwitcher` carried its old sidebar chrome (`px-3 py-2 border-b border-white/[0.06]`) on its own root, so it painted a stray rule wherever else it was placed — including the public nav it already sat in. Placement now belongs to the caller. Not done: the deck's nav is illustrative, not read from the registry — it lists Permissions as a top-level entry (the module registers no menu item by design; its pages are sub-pages with no index) and moves Doctor off the admin sidebar. Following it literally would add a nav entry leading nowhere, so the registry stays the source of truth. 16 new unit tests. Verified at 1440px: crumbs on index, sub-page, dynamic and section-less routes; active nav; ⌘K filter-and-navigate; public nav unchanged. make lint / make test (1926 py, 80 js) / make doctor all green. Claude-Session: https://claude.ai/code/session_01NYVhsyUpmhhRbXtZk7EjAE
…ename__ `AuditLink.entity_type` matches `AuditEntry.entity_type`, which the listeners write as `type(obj).__name__` — but the `ModuleBase.register_audit_links` docstring said it was the `__tablename__` and used `entity_type="users_user"` as its worked example. That is the docstring a module author actually reads, since the hook is the thing they override. Following it produces a link that never matches. Nothing errors: an unmatched lookup falls back to rendering `entity_type` as the label, so the audit row still shows text and the entry looks fine while silently never becoming a link. `AuditLink.entity_type`'s own docstring already said all of this correctly — the two directly contradicted each other, and the more prominent one was wrong. Every bundled module already registers `Model.__name__`, and background_tasks and settings each carry an inline "Class name, not __tablename__" comment — authors hit this and patched around it locally rather than the doc that misled them. Docstring only, no behaviour change. Pre-existing on main (last touched in #261), surfaced by a review of the branch against v0.0.30. Claude-Session: https://claude.ai/code/session_01NYVhsyUpmhhRbXtZk7EjAE
…the real chrome Swept all 30 view routes at 1440px and audited each for topbar, breadcrumb, active nav, console errors and horizontal overflow. Three pages were wrong. The two permissions screens had no section at all: their paths live under /permissions/ but they are reached from — and belong to — Users, so nothing highlighted in the sidebar and the crumb read "Edit permissions for admin" with no parent. A page can now declare the section it belongs to, resolved against the *visible* menu, so it never offers a parent the viewer would be refused at. Both now read "Users / …" with Users lit. Profile is deliberately left without one: it is reachable by anyone with users.self.profile, who may well not have the Users entry in their menu. ModulesEdit sized its master/detail pane with h-[calc(100vh-64px)], a magic number that predates the topbar and was already approximate. Both bars are h-14 and mutually exclusive — topbar on lg, mobile bar below it — so 56px is now exact at every width. Also fixes a footgun found by its own test: menu urls are inconsistent about trailing slashes (/users/admin but /file-storage/), so matching a declared section by string equality would have silently failed for half of them. `samePath` normalises both sides. Left as-is, deliberately: /settings/ renders no h1 because it is a full-height master/detail editor with its own rail, and its crumb still resolves from the menu; the 404 page keeps its standalone layout, matching how the deck frames errors. 8 new unit tests. make lint / make test (1926 py, 88 js) green. Claude-Session: https://claude.ai/code/session_01NYVhsyUpmhhRbXtZk7EjAE
Two correctness bugs — the audit-log correlation banner's "show all" navigated with the unapplied filter draft, and an out-of-range tasks page miscounted the window total as 0 and rendered the never-ran empty state (now clamps to the last page; regression test added). Plus cleanups: sidebar and topbar share one active-section algorithm, the ⌘K palette renders account items through the same loop as nav groups, the audit empty-state derives labels from FilterBar's exported ACTIONS, retry dialogs memoize their JSON preview, "/users/admin" became a shared constant, and the modules pane reads the chrome height from a layout- published --app-chrome-h instead of a hardcoded 56px. Claude-Session: https://claude.ai/code/session_01NYVhsyUpmhhRbXtZk7EjAE
Two behavior fixes — the tasks empty state keyed "filtered vs never ran" off the live search input instead of the server-confirmed filter, so it could flash the wrong copy during the debounce window, and the audit-log correlation banner now only renders when the correlation actually matched rows. The rest is deduplication: a shared TableEmptyRow replaces three hand-rolled filtered/empty table wrappers, InlineBanner replaces two bespoke banner shells, diagnoseWorkerHealth() is the single broker/worker predicate for the banner and the empty state, the landing CTA computes its signup-gated href/label once, USERS_ADMIN_PATH is now covered by the auth-route drift test, the correlation banner title pluralizes via CLDR suffixes, and both chrome bars size themselves off --app-chrome-h instead of independent h-14 classes. Claude-Session: https://claude.ai/code/session_01NYVhsyUpmhhRbXtZk7EjAE
Deleting the last file on a trailing page no longer strands the file list on a blank table — the view clamps past-the-end pages the same way background_tasks now does. The settings REST endpoint reports source/db_override correctly by passing the DB-overrides map the Inertia view already used, filename search escapes LIKE wildcards so a literal % or _ matches itself, test_connection runs a module's health checks concurrently, the upload queue uses a bounded worker pool instead of strictly serial uploads, and the users module's own auth pages consume LOGIN_PATH/REGISTER_PATH instead of re-hardcoding the routes. Claude-Session: https://claude.ai/code/session_01NYVhsyUpmhhRbXtZk7EjAE
…ew empty states Review fixes: ?page=0 no longer produces a negative SQL OFFSET (ge=1, matching the sibling endpoint), the landing page's bottom CTA is auth-aware like the hero, clear-filters can no longer race the search debounce into reapplying a stale status, ⌘K account items merge into a same-named nav group instead of overwriting it, and the settings overrides helper moved to _module_settings as a shared public function instead of a private cross-file import. i18n: WorkerHealthBanner, TasksEmpty and UsersEmpty now render through useT() like their audit_log sibling — new background_tasks worker_health/tasks_empty keys, and a bootstrapped users locale (locales/en.json + locale_dirs()) scoped to the empty-state copy. file_storage needed nothing; it was already translated. Claude-Session: https://claude.ai/code/session_01NYVhsyUpmhhRbXtZk7EjAE
Task-name search escapes LIKE metacharacters at both call sites — task names are full of underscores, so an unescaped "_" made every search a single-character wildcard (regression test added, mirroring the file_storage fix). The ⌘K palette memoizes its group bucketing instead of rebuilding it on every topbar render, and the breadcrumb collapse rule is documented as the naming convention it is. Claude-Session: https://claude.ai/code/session_01NYVhsyUpmhhRbXtZk7EjAE
…users search QA round 1 caught two of the review passes' own hardening choices biting back in the browser. ?page=0/-1 no longer 422s on the background-tasks and file-storage views — the endpoints clamp like their past-the-end paths already did (the JSON admin APIs keep strict validation for API callers). The users admin search escapes LIKE metacharacters through the same _contains_pattern shape as its two siblings, for email and full_name both, and list_users clamps page/per_page at the service so the raw-param admin view can't reach SQL with a negative offset. Regression tests cover all three: page=0/-1 on both views, and a literal-underscore search matching only its literal counterpart. Claude-Session: https://claude.ai/code/session_01NYVhsyUpmhhRbXtZk7EjAE
The users admin listing gets the same past-the-end clamp-and-refetch its two siblings received, with a regression test. like_contains_pattern and LIKE_ESCAPE_CHAR move into simple_module_db so three modules share one escaping implementation instead of three copies (one of them unnamed and un-greppable). The file browse endpoint builds its filter set once for both the initial query and the clamp refetch, the sidebar hands its already-resolved active section to the topbar instead of both walking the menu, and the tasks view runs its status counts and broker poll concurrently on the one path that polls. Claude-Session: https://claude.ai/code/session_01NYVhsyUpmhhRbXtZk7EjAE
…only when needed ?page=0 rendered page-1 rows labelled "Page 0" and an oversized per_page skewed totalPages — the users admin view now clamps with the same bounds the service applies and echoes those values in the pagination prop (regression test added). The tasks clear-filters guard arms only when resetting the search box will actually fire the debounce effect, so it can no longer swallow the first keystroke typed after clearing an already-empty search. Claude-Session: https://claude.ai/code/session_01NYVhsyUpmhhRbXtZk7EjAE
…ee surface nits
The content-type family filter no longer widens under a crafted query —
like_prefix_pattern() joins the shared escaping helpers and the filter
uses it, with a regression test. The merged Add People form gets back
the password-policy hint the deleted Create page used to show, the
legacy /users/admin/{create,invite} aliases are covered by an
anonymous-access test again, and ExecutionRow calls the module's
formatTs() instead of reimplementing it inline.
Claude-Session: https://claude.ai/code/session_01NYVhsyUpmhhRbXtZk7EjAE
… banner audit_log was the fourth listing with the past-the-end pagination bug — a stale ?page= or shrunk correlation rendered the banner above an empty table; its view now clamps and refetches like its three siblings. The worker health banner only renders on the unfiltered view, because its backlog derives from filter-scoped counts and a narrowed search could mask a genuinely stuck fleet. Cleanups: AppTopbar's dead fallback branch removed (activeMenuItem is required now), the audit filter interface is declared once in FilterBar, stable NO_ITEMS references keep the palette memo effective, CLEARED hoisted to module scope, a dead per_page guard and a redundant list() wrapper dropped. Claude-Session: https://claude.ai/code/session_01NYVhsyUpmhhRbXtZk7EjAE
The permissions grant-search was the fourth unescaped LIKE — hidden from earlier greps because it builds its pattern on a separate line — and now goes through the shared like_contains_pattern helper, with its own test file. The file-storage JSON API gains the same strict page/per_page bounds as its background_tasks sibling, so a non-positive page can never reach the database as a negative OFFSET (the Inertia view keeps its clamp; API callers get the 422 contract). Claude-Session: https://claude.ai/code/session_01NYVhsyUpmhhRbXtZk7EjAE
Another test's created-then-deleted setting can leave an older audit entry with the same reused integer id, so the spec asserts at least one matching entry instead of exactly one — the guarded regression is only that entity_id resolves non-empty. Claude-Session: https://claude.ai/code/session_01NYVhsyUpmhhRbXtZk7EjAE
Deploying simple-module-python with
|
| Latest commit: |
f22e937
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://39457a66.simple-module-python.pages.dev |
| Branch Preview URL: | https://wireframe-ui.simple-module-python.pages.dev |
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.
Summary
?page=clamping in every Inertia view (strict 422 kept on the JSON APIs), LIKE-metacharacter-literal search everywhere via a sharedsimple_module_dbescaping helper, and confirm dialogs for flag toggles and task retries plus a worker-health banner that only speaks for unfiltered views.TableEmptyRow/InlineBanner/worker-healthprimitives replacing hand-rolled triplicates. Branch is merged up to main @ v0.0.31.Verification
/dashboard/on port8300(and 29 more routes — see QA report)tests/e2e/test_shell_ui.pycovers breadcrumb, palette, clamping, literal searchQA Report
?page=0; P1:_wildcard in users search)Test plan
/dashboard/and confirms the topbar breadcrumb + ⌘K palette/users/admin/addshows "Users › Add people"; a role-edit page still highlights Users/admin/background-tasks/?page=0renders the list, not an error pagehttps://claude.ai/code/session_01NYVhsyUpmhhRbXtZk7EjAE