Skip to content

Console: layout fixes from the 2026-10-23 review #912

Closed

Description

@V3RON

Part of #906.

Scope

Seven layout faults in the console are fixed. After this PR the page no longer shifts when a scrollbar appears; the worker page has no breadcrumb; the warm-pool tab has a line between its summary and its table; the fleet table's row ⋯ sits inside its cell; Inbox row buttons have room above and below; a toast's close button sits beside its text, not over it; and a ⋯ list never repeats the button beside it. UI only: no route, contract or daemon change.

In short

The console's layout stops shifting, cramping and overlapping in the seven places the 2026-10-23 review found. The one change in behaviour: a ⋯ list beside a button (in a page header, a drawer or an Inbox row) no longer lists that button's action. When that leaves the list empty, the ⋯ is not drawn.

flowchart LR
  E[Item's actions] --> B[First safe action → button]
  E --> M["⋯ list: the rest (changed: without the button's action)"]
  M -->|empty| N[No ⋯ drawn]
Loading

Before and after, a drained worker's page header:

Button ⋯ list
Today Undrain Undrain, …
After Undrain the worker's other actions; no ⋯ when there are none

If this goes wrong, a user would notice an action missing from both the button and the ⋯, or a box that moved.

Technical spec

Facts

# Fact Source
F1 html, body set only margin: 0; no scrollbar-gutter anywhere in ui/src; the document scrolls (.app-main has no overflow-y) ui/src/ds/foundation.css:81-84; ui/src/ds/sidebar.css:9-15; rg scrollbar-gutter ui/src → no match at 192cb5fbb
F2 The worker page renders <nav className="crumbs" aria-label="Breadcrumb"> with a "Fleet" link to page.back; .crumbs is styled in master-detail.css and used nowhere else ui/src/screens/fleet/worker-page.tsx:73-77; ui/src/ds/master-detail.css:61-80; rg crumbs ui/src → those two files
F3 Browser tests click that breadcrumb to return to the Fleet with its ?page= kept e2e/console/long-lists.spec.ts:378-423; e2e/console/workers.spec.ts:933,1133,1291
F4 The warm tab draws Facts then DataTable in one Panel with no rule between; .panel .table-card drops the table's border ui/src/screens/fleet/worker-page.tsx:405-419; ui/src/ds/table.css:30-33
F5 .table .table-menu { width: 3.5rem }; cells are border-box with padding: var(--space-4) var(--space-3) and the last cell padding-right: var(--space-6); the ⋯ IconButton is 40px wide ui/src/ds/table.css:35-52,214-217; ui/src/ds/foundation.css:78; ui/src/ds/button.css:76-88
F6 .inbox-actions has padding-right: var(--space-4) and no vertical padding; the 40px buttons sit in a row whose padding is on the link only ui/src/screens/inbox/inbox.css:96-113
F7 .toast-close is position: absolute; top/right: var(--space-2) over .toast-content (padding 12px 16px); the close is a 40px bordered IconButton ui/src/ds/toast.css:35-39,72-76; ui/src/ds/toast.tsx:57-65
F8 ActionBar shows the first non-danger entry as a button and passes every entry, that one included, to RowMenu ui/src/ds/menu.tsx:92-108
F9 An Inbox row renders actions.fix as a button and actions.menu as a RowMenu; inboxActions keeps the fix in menu, and the Inbox drawer feeds the same menu to ActionBar ui/src/screens/inbox/index.tsx:41,142-156; ui/src/model/actions.ts:345-350; ui/src/state/inbox.ts:146-148
F13 Headless Chromium runs with --hide-scrollbars, so a scrollbar takes no layout space in the console lane playwright-core 1.63.0 lib/coreBundle.js:43338-43343; playwright.config.ts:18
F14 A closed Base UI menu renders no popup, so static markup shows the ⋯ trigger only @base-ui/react@1.9.0/menu/portal/MenuPortal.js:29-32; ui/src/ds/menu.tsx:79
F15 The worker page header's ⋯ is already pinned in the browser: menuitem "Undrain" visible, entries ["Undrain", "Remove worker"] e2e/console/actions.spec.ts:450-485
F16 WorkerPageView.back is read only by the breadcrumb; .crumb rules sit beside .crumbs ui/src/state/workers.ts:78,152; ui/src/screens/fleet/fleet.test.tsx:119; ui/src/ds/master-detail.css:61-81
F17 #915 edits inboxActions (ui/src/model/actions.ts) and ui/src/state/inbox.ts, not ui/src/screens/inbox/index.tsx #915 Modules touched
F10 ActionBar is used by the Inbox drawer, the lease drawer, the device drawer, the Queue drawer and the worker page header rg ActionBar ui/src --glob '!ds/*' → inbox/index.tsx:41, leases/drawer.tsx:27, fleet/device-drawer.tsx:34, queue/index.tsx:126, fleet/worker-page.tsx:71
F11 DESIGN.md: "a drawer or page header shows its item's first safe action as a secondary button beside its own ⋯"; "A list with no entry draws no ⋯"; toasts have "a Close button"; buttons are "at least 40px tall"; spacing picks from the 4px scale docs/internal/DESIGN.md:119-124,168-174,180-184
F12 Unit tests run in Node without a DOM, a ds component with renderToStaticMarkup; the browser lane is Playwright under e2e/console/ ui/AGENTS.md Rules; vitest.config.ts:18-20; package.json:55

Modules touched

Searches: rg "crumbs|table-menu|inbox-actions|toast-close|ActionBar|scrollbar" over ui/src, e2e/console, docs/internal/DESIGN.md, website/src/content/docs.

flowchart LR
  WP[screens/fleet/worker-page.tsx *] --> DS[ds/menu.tsx ActionBar *]
  IN[screens/inbox/index.tsx *] --> DS
  IN --> A[model/actions.ts]
  WP --> T[ds/table.css *]
  TO[ds/toast.tsx, toast.css *]
  F[ds/foundation.css *]
  MD[ds/master-detail.css *]
Loading

Also screens/inbox/inbox.css, screens/fleet/fleet.css (warm divider), docs/internal/DESIGN.md (Menu and Toast paragraphs), e2e/console/long-lists.spec.ts, e2e/console/workers.spec.ts, e2e/console/actions.spec.ts, and a new e2e/console/layout.spec.ts. The Inbox row drops the button's entry in screens/inbox/index.tsx (InboxActions), not in inboxActions or state/inbox.ts, so the Inbox drawer keeps Undrain (as its ActionBar button) and #915's files are untouched (F17).

Contract and event changes

None (searched: no file under src/ changes).

Other code on the same state

UI state only: the ⋯ list's entries. Searched rg "RowMenu|ActionBar|actions.menu" ui/src (12 hits). Every caller that shows a button beside a ⋯ goes through ActionBar (F10) or InboxActions (F9); DataTable's row ⋯ shows no button beside it and keeps every entry. #910 adds tabs to the device drawer and keeps its ActionBar as is; no shared lines.

Failure modes

None: the task crosses no module boundary and writes no state.

Rules in play

  • DESIGN.md Spacing: "Pick from the scale; never a value between" — every new value is a --space-* token.
  • DESIGN.md Spacing: "Buttons and inputs are at least 40px tall" — the toast close keeps a 40px hit area; only its border goes.
  • DESIGN.md Menu: "A list with no entry draws no ⋯" — kept; the paragraph gains "and the list does not repeat that action".
  • documentation rule 9 — the console's public look changes in no documented way (console.mdx names no ⋯ entries; checked rg "⋯|menu" website/src/content/docs/docs/worker/console.mdx), so no website page changes.

Tests

Seam: the browser lane, e2e/console/ against a started daemon (model: e2e/console/long-lists.spec.ts), plus ActionBar with renderToStaticMarkup (model: ui/src/ds/chart.test.tsx).

  • The page reserves room for a scrollbar: the computed scrollbar-gutter of html is stable on every page (Overview, Fleet, a worker page). A layout measure cannot fail in the lane, which hides scrollbars (F13).
  • An operator opens a worker page: no element named "Breadcrumb"; on the Warm pool tab a 1px --border line sits between the facts and the table. The tests that clicked the breadcrumb to return with the Fleet's page kept (long-lists.spec.ts:378-423, workers.spec.ts:933,1133) use the browser's Back, which keeps it; the keyboard-only test (workers.spec.ts:1252,1290-1293) follows the sidebar's Fleet link by keyboard.
  • An operator looks at fleet and Inbox rows: the fleet row's ⋯ lies inside its cell with 24px to the table's right edge; each Inbox row button has 8px between it and the row's top and bottom lines.
  • An operator sees a toast: the close button lies inside the toast's box; the title's box does not overlap it; on a one-line toast the text's and the button's vertical centres are within 4px. Cases: a one-line success toast, a drain toast with a line and Undo.
  • An operator opens a ⋯ beside a button: drained, connected worker page header → Undrain button and no ⋯, its only action being Undrain (actions.spec.ts:477-478); drained, disconnected worker → Undrain button, ⋯ entries ["Remove worker"] (actions.spec.ts:485); Inbox "drained" row → Undrain button, ⋯ without Undrain; Inbox drained drawer → Undrain button still there; lease drawer, whose only action is dangerous → no button, ⋯ keeps Release lease (as actions.spec.ts:460-462 pins); an ActionBar whose only safe entry is the button draws no ⋯ (static markup, which can see the trigger, F14).

Done when

  • pnpm test and pnpm test:console pass, with the five Tests lines above.
  • DESIGN.md's Menu paragraph says the ⋯ beside a button does not repeat it; its Toast paragraph says the close button sits beside the text.
  • rg -n 'crumb' ui/src finds nothing.

Out of scope

Depends on

Written by an agent.

Activity

  1. added
    task:draftScope written; technical spec or its spec check missing.
    on Oct 23, 2026
  2. added
    task:readySpec checked. An agent may implement it once its Depends on allow.
    and removed
    task:draftScope written; technical spec or its spec check missing.
    on Oct 23, 2026
  3. self-assigned this
    on Oct 23, 2026
  4. added 2 commits that reference this issue on Oct 23, 2026
    0bd8985
    3080eca
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

task:readySpec checked. An agent may implement it once its Depends on allow.

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions