Skip to content

Console: Fleet table and a page per worker #766

Closed

Description

@V3RON

Part of #756.

Scope

Fleet becomes a table of workers, and each worker gets its own page with tabs. A device on that page opens in the drawer. After this PR an operator reads every worker's state, slots and load in one table, opens a worker's page, and shares the address of a single device on it.

In short

/fleet is a table with the figures today's Workers list shows, in the same words. Each row has:

  • the worker's name (its label, else its id) and, under it, its system (<os> <osVersion>, e.g. macOS 26.1, today's text, ui/src/model/worker-page.ts:399-401);
  • its state word, the worst finding as today's row shows it (e.g. drained, disk 91%);
  • its slots: today's Slots group, a square per slot up to the limit, with one accessible label for the group, unchanged (ui/src/ds/slots.tsx);
  • its use, running of limit per platform as today (— for a worker not read);
  • CPU, memory and disk with today's meters.

Above the table, the toolbar has the chips All, Issues and Busy on the left (today's filters) and a search box on the right. The search matches, ignoring case, any part of the worker's name, id or system, and is kept in the address as ?q=.

Clicking a row opens the worker page /fleet/<workerId>:

  • The header shows the worker's name, its state word and its system.
  • A banner shows when the worker is disconnected ("Disconnected · last seen ago", with today's duration text, e.g. "4 s" or "12 min", ui/src/model/attention.ts:197) or drained ("Drained").
  • Above the tabs, today's Overview tab content keeps its look: the load meters with memory pressure, the capacity card per platform with the slot legend, and the leases card.
  • The tabs are Devices (today's devices table), Warm pool, Host and Events, with the tab in ?tab=. A starting worker has no Warm pool tab and its Devices tab says devices appear once startup finishes, as today.

A row under Devices opens the drawer at /fleet/<workerId>/<deviceId>. Closing it goes back to /fleet/<workerId>, keeping ?tab=devices, and focus returns to the row. The drawer shows the model, runtime, state word, and the holder and lease when leased. It shows the UDID only when the console has it: for a leased device from its lease, for a physical device from the worker view. It never shows a blank UDID field.

The Events tab lists the events the console holds for that worker: on a gateway those whose payload.workerId is this worker; on a single host all of them, since every event is the host's own. The console learns which it talks to from daemon.mode: this task adds the mode read of ADR 0037 §4 (ui/src/data/mode.ts, read after sign-in and after each reconnect), which #769 reuses for the worker actions. Until the read succeeds, the tab uses the gateway rule. With none, it says "No events for this worker in the last hour."

  • Issues: a worker with any finding under today's worker-health rule, unchanged (ui/src/model/fleet.ts:17-50): a lost connection, failed health, drained, starting, over its RAM budget, memory pressure warn or critical, or CPU, disk, or memory without a pressure reading, at 85% or more. For example, a drained worker, or one at 91% disk.
flowchart LR
  A[/fleet table/] -->|row| B["/fleet/&lt;id&gt; worker page (new)"]
  B -->|Devices row| C["/fleet/&lt;id&gt;/&lt;deviceId&gt; drawer (new)"]
  B --> D[tabs: Devices, Warm pool, Host, Events]
Loading

If this goes wrong, a person cannot find a worker's warm pool or host facts, which today sit in the inspector's tabs.

Technical spec

Facts

# Fact Source
F1 Today the worker inspector has tabs overview, host and warm-pool, kept in component state, not the URL ui/src/model/worker-page.ts:473, ui/src/screens/workers/index.tsx:140,154-158
F2 The Overview tab shows load, leases, slots and a devices table; Host shows id, connection, health, protocol, OS and tools, modules, catalog; Warm pool shows targets with want, ready, booting, why short ui/src/screens/workers/index.tsx:163-205,334,446-462
F3 Worker health: connection not connected or health failed is error; drained, health starting or RAM over budget is warn ui/src/model/fleet.ts:17-27
F4 Load findings: CPU, memory and disk warn at 85%, error at 95%, for connected workers only ui/src/model/load-thresholds.ts:7, ui/src/model/fleet.ts:31-50
F5 The Workers list filters All, Issues, Busy, with paging ui/src/state/workers.ts:75-85, docs/CONSOLE.md:252-313
F6 workerPath builds worker addresses ui/src/model/workers.ts:18
F7 A worker view carries devices (status device shape with state, physical, absentReason), leases, capacity, host, health, connection, drained, lastSeenAt src/contract/schemas.ts:230, src/contract/schemas.ts:1277-1327
F8 The console holds recent events and can tell an event's worker ui/src/model/events.ts (workerIdOf), ui/src/state/events.ts
F9 The e2e suite starts a gateway with labelled fake workers and stops one to make it disconnected e2e/console/fixtures.ts:193-238, e2e/console/workers.spec.ts:716
F10 Drawer, Toolbar, the search field and the address rules come from #765 #756 Shared values
F11 workerIdOf reads only payload.workerId, which a gateway's relayed events and its own worker.* events carry, and a single host's events do not ui/src/model/events.ts:83-90, docs/EVENTS.md:141-151, src/bus/index.ts:365
F17 GET /v1/status carries daemon.mode; a single host and a one-worker gateway list alike in /v1/workers src/contract/operations.ts:117, src/contract/schemas.ts:1270-1275
F12 The worker view drops a virtual device's driver address; a UDID reaches the console for a lease through /v1/leases, for a physical device through the view src/contract/schemas.ts:226-229,252-258, ui/src/model/leases.ts:234,281
F13 Slots is one role="img" group with one label; a slot's text is <model> <osVersion>, <state> ui/src/ds/slots.tsx:21-23, ui/src/model/fleet.ts:72
F14 Today's row shows running of limit; platformSlots draws a square per slot up to the limit; an unread worker is isUnread ui/src/model/fleet.ts:87-91, ui/src/model/worker-page.ts:57-62, ui/src/model/workers.ts:43-48
F15 Today's Overview tab holds capacity cards with the slot legend, the leases card, load meters with pressure, and the reading's age; browser tests pin them ui/src/screens/workers/index.tsx:174-203,241-261, e2e/console/workers.spec.ts:554,569,608,679
F16 A starting worker has no Warm pool tab and no device list ui/src/model/worker-page.ts:510-527

Modules touched

Searches: WorkersScreen, WorkerTab, workerInspector, workerPath, useWorkers, "Workers" in e2e/console.

  • ui/src/screens/workers/ (renamed fleet/): the table, the worker page and the device drawer.
  • ui/src/state/workers.ts: table rows, worker page, tab from ?tab=.
  • ui/src/model/worker-page.ts, ui/src/model/workers.ts: WorkerTab becomes devices | warm-pool | host | events; device detail for the drawer; issue rule reused from fleet.ts.
  • ui/src/app/routes.ts: /fleet matches the worker page and the device drawer.
  • ui/src/model/events.ts: the single-host rule for a worker's events.
  • ui/src/data/mode.ts (new), ui/src/main.tsx, ui/src/state/services.tsx: the mode read, wired to the session and the live connection, and useDaemonMode for state.
  • e2e/console/workers.spec.ts, workers-gateway-stops-mid-fetch.spec.ts, long-lists.spec.ts, views.spec.ts.
  • docs/CONSOLE.md (the Workers section becomes Fleet), ui/AGENTS.md (Pages).
flowchart LR
  routes["app/routes (changed)"] --> fleet["screens/fleet (changed)"]
  fleet --> st["state/workers (changed)"]
  fleet --> ds["ds: DataTable, Drawer, Toolbar, Tabs"]
  st --> m["model/worker-page, workers (changed)"]
Loading

Contract and event changes

None. The page reads GET /v1/workers as today.

Other code on the same state

The state is the address and ?tab=. Searches: setSearch(, usePaging(, useSearch(.

Failure modes

Call Outcome What happens Limit, counted from
/fleet/<id> for an unknown or removed worker not in the list "This worker is not in the fleet." with a link back to Fleet the next refetch
/fleet/<id>/<deviceId> for a device no longer listed not in the list the drawer says "This device is gone." the next refetch
?tab= with an unknown value, or warm-pool on a starting worker bad input the Devices tab shows —
worker incompatible no devices read the Devices tab says "No devices: the gateway cannot read this worker." —
GET /v1/workers fails error as today (Loaded's error) as today

Rules in play

  • ADR 0037 §2, "A worker is a container": its own page at /fleet/<id>, with devices in the drawer.
  • ADR 0012 §1: on a single host the table has one row, the host itself. The page works the same.
  • DESIGN.md "A status always has a word": the state column shows the daemon's word; device squares keep their words as accessible names.
  • ui/AGENTS.md "Colour is never the only signal".

Tests

Seam: the browser lane with startFleet (e2e/console/workers.spec.ts). The pure rules (issue filter, tab parsing) run in Node beside ui/src/model/workers.test.ts.

  • the Fleet table lists every worker with its name, system, state word, today's slots group and its label, running of limit (— when not read), and CPU, memory and disk (extend workers.spec.ts)
  • the Issues chip shows only workers with a health or load finding: a drained worker and a disconnected one show, a healthy one does not (fleet.test.ts cases plus one browser line); Busy works as today (existing workers.spec.ts:384)
  • the search box filters by any part of the worker's name, id or system, ignoring case, keeps ?q=, and clearing it brings every row back
  • clicking a row opens /fleet/<id> with the worker's name as the page heading, its state word and system beside it, and the Devices tab selected
  • the worker page shows today's load meters, capacity cards with the slot legend, and leases card above the tabs (move workers.spec.ts:554,569,608,679)
  • each tab (Devices, Warm pool, Host, Events) shows its content, sets ?tab=, and survives a reload
  • ?tab= with an unknown value shows Devices (model test)
  • a disconnected worker's page shows the banner "Disconnected · last seen … ago"; a drained worker's shows "Drained"
  • an incompatible worker's Devices tab says "No devices: the gateway cannot read this worker." (extend the incompatible case at workers.spec.ts:324)
  • ?tab=warm-pool on a starting worker shows Devices (model test)
  • data/mode.ts keeps gateway or worker after a good read, keeps none after a failed one, and reads again after a reconnect (data test with a fake fetch)
  • a starting worker has no Warm pool tab and says its devices appear once startup finishes (as today)
  • a Devices row opens the drawer at /fleet/<id>/<deviceId> with the device's model, runtime, state word, and holder and lease when leased; a leased device shows its UDID and an unleased simulator shows no UDID field
  • closing the device drawer returns to /fleet/<id>?tab=devices with focus on the row
  • on a gateway the Events tab lists only that worker's events; on a single host it lists all; with none it says "No events for this worker in the last hour."
  • an unknown worker id shows "This worker is not in the fleet." and a link to Fleet; an unknown device id shows "This device is gone."
  • on a single host Fleet shows one row, the host, and its page works (extend workers.spec.ts's worker-mode case)
  • axe passes on Fleet, a worker page and a device drawer, in light and dark; no <style> element or CSP violation on them (extend views.spec.ts)

Done when

  • pnpm test:console --project=chromium passes, and CI's Console check passes in every browser.

Out of scope

Depends on

Written by an agent.

Activity

  1. added
    task:draftScope written; technical spec or its spec check missing.
    on Oct 20, 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 20, 2026
  3. self-assigned this
    on Oct 20, 2026
  4. 49 remaining items

  5. added a commit that references this issue on Oct 21, 2026
    6e52d5d
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