Skip to content

Wireframe + hi-fi shell UI: topbar, ⌘K palette, honest lists - #271

Merged
antosubash merged 19 commits into
mainfrom
wireframe-ui
Aug 20, 2026
Merged

Wireframe + hi-fi shell UI: topbar, ⌘K palette, honest lists#271
antosubash merged 19 commits into
mainfrom
wireframe-ui

Conversation

@antosubash

Copy link
Copy Markdown
Owner

Summary

  • Ships the wireframe + hi-fi shell from the design deck: a desktop topbar with a section-aware breadcrumb, a ⌘K command palette built from the permission-filtered menu registry, locale switcher placement, and one shared active-section resolution for sidebar and breadcrumb (pages outside their section's path can declare one — permissions screens highlight Users).
  • Honest list behavior across all four listing modules: designed empty states (translated via i18n, with a bootstrapped users locale), filtered-vs-empty copy, out-of-range ?page= clamping in every Inertia view (strict 422 kept on the JSON APIs), LIKE-metacharacter-literal search everywhere via a shared simple_module_db escaping helper, and confirm dialogs for flag toggles and task retries plus a worker-health banner that only speaks for unfiltered views.
  • Hardening found by the review/QA loop rode along: auth-aware landing CTAs, signup-gated register links, correlation-banner gating, pagination props that echo what the query ran with, and shared TableEmptyRow/InlineBanner/worker-health primitives replacing hand-rolled triplicates. Branch is merged up to main @ v0.0.31.

Verification

  • Browser-verified at /dashboard/ on port 8300 (and 29 more routes — see QA report)
  • Console: no errors (one known pre-existing favicon 404)
  • Local CI: lint / typecheck / tests (2056 py + 88 js) / build / full e2e (20 passed) all green
  • New e2e spec tests/e2e/test_shell_ui.py covers breadcrumb, palette, clamping, literal search
  • Verification report: https://claude.ai/code/artifact/fb4ba086-fdd8-4d12-afe9-1dbfbb24b96d

QA Report

Report links are Claude Artifacts and start private — the author can share them
from claude.ai/code/artifacts if reviewers need access.

Test plan

  • Reviewer loads /dashboard/ and confirms the topbar breadcrumb + ⌘K palette
  • /users/admin/add shows "Users › Add people"; a role-edit page still highlights Users
  • /admin/background-tasks/?page=0 renders the list, not an error page
  • CI is green

https://claude.ai/code/session_01NYVhsyUpmhhRbXtZk7EjAE

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
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying simple-module-python with  Cloudflare Pages  Cloudflare Pages

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

View logs

@antosubash
antosubash merged commit b82aeb8 into main Aug 20, 2026
13 checks passed
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