Repository navigation
Console: Fleet table and a page per worker #766
Description
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
Slotsgroup, 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 pressurewarnorcritical, 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/<id> worker page (new)"]
B -->|Devices row| C["/fleet/<id>/<deviceId> drawer (new)"]
B --> D[tabs: Devices, Warm pool, Host, Events]
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/(renamedfleet/): 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:WorkerTabbecomesdevices | warm-pool | host | events; device detail for the drawer; issue rule reused fromfleet.ts.ui/src/app/routes.ts:/fleetmatches 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, anduseDaemonModefor 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)"]
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(.
- Paging uses
pageandsize. The tab usestab. They do not collide. - Console: release, cancel, drain, undrain and remove from the console #769 adds actions to this page's header and rows. It reads the same worker views and writes nothing to the address but the drawer's.
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 (extendworkers.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.tscases plus one browser line); Busy works as today (existingworkers.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-poolon a starting worker shows Devices (model test)data/mode.tskeepsgatewayorworkerafter 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=deviceswith 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 (extendviews.spec.ts)
Done when
pnpm test:console --project=chromiumpasses, and CI's Console check passes in every browser.
Out of scope
- Drain, undrain, remove and every other action (Console: release, cancel, drain, undrain and remove from the console #769).
Depends on
Written by an agent.
Activity
test: the Fleet table lists every worker with its name, system, state…
feat(console): the Fleet table shows state, slots, use and load per w…
test: the Issues chip shows only workers with a health or load findin…
feat(console): the Issues filter asks one rule, hasIssue (#766)
test: the search box filters by any part of the worker's name, id or …
feat(console): Fleet has a search box, kept in the address as ?q= (#766)
test: the worker page shows today's load meters, capacity cards with …
test: each tab shows its content, sets ?tab=, and survives a reload (#…
test: ?tab= with an unknown value shows Devices (#766)
test: ?tab=warm-pool on a starting worker shows Devices (#766)
49 remaining items
test: an unknown worker id shows This worker is not in the fleet. and…
feat(console): a worker the fleet does not list says so, with a link …
test: on a single host Fleet shows one row, the host, and its page wo…
feat(console): the worker page's Host tab shows the worker's version (#…
test: axe passes on Fleet, a worker page and a device drawer, in ligh…
test: the console specs follow Fleet's table and the worker page, and…
docs(console): Fleet is a table, a worker has a page with tabs, and a…
test: a worker's page and device drawer name their ids by their own e…
refactor(console): the worker-list answers share one route, and the t…
test: the real-proxy lane counts the Fleet table's rows (#766)
test: the mode read counts its reads, a worker's events order by time…
test: a row click opens the page, the Events tab shows, the lease cou…
fix(console): the Fleet filter lives in the address; drop repeated an…
feat(console): Fleet is a table, and each worker has a page with tabs (…