Repository navigation
Console: watch any device's screen, and layout fixes from the 2026-10-23 review #906
Description
Request: none
Problem
In the console, a device's live screen is shown on the lease, not on the device. A device with no lease, such as an idle or warm-pool simulator, cannot be watched at all. An operator looking at a device has to find its lease first. The stream's numbers (frame rate, bandwidth, dropped frames) sit under the picture all the time, though they only matter when something looks wrong.
The same review of the console found layout faults. The page shifts sideways when a scrollbar appears. The worker page has a "Fleet" link the sidebar already gives. The warm-pool tab runs its summary into its table with no line between them. The worker's Events tab shows only the last hour, does not fill the page and cannot load older events. Several boxes are cramped or overlap: the row ⋯ button in the fleet table, the Inbox row buttons, the toast's close button, and a ⋯ list that repeats the button beside it.
Who it is for
An operator using the console, on a single worker or on a gateway, who wants to see what a device is showing, whether or not an agent holds it.
Outcome
- The operator opens any device in the console and watches its screen, leased or not.
- The picture keeps running when an agent leases the device.
- The stream's numbers are one click away, not always on screen.
- The worker page's Events tab shows that worker's whole history, scrolling back as far as it is kept.
- The layout faults listed above are gone.
How it works for the user
flowchart TD
A[Fleet → worker → device row] --> B[Device drawer opens on Details]
B --> C{Screen tab}
C -->|device ready or leased| D[Picture starts by itself]
C -->|device not running| E["Sentence: why there is no picture"]
E -->|device becomes ready| D
C -->|streaming off on the worker| F["Streaming is off on <worker>."]
D --> G[ⓘ opens the stream numbers]
D -->|leave the tab or close the drawer| H[Picture stops]
D -->|device reset after a lease, or shut down| I[Picture ends, the tab says why]
Before: Leases → a lease → Screen tab → Show screen. A device with no lease has no screen anywhere.
After: Fleet → a worker → a device → Screen tab. The lease drawer has no Screen tab.
Examples
Who may watch, and what they get, through GET /v1/stream/workers/{workerId}/devices/{deviceId}:
| Caller and device | Today (lease route) | After |
|---|---|---|
Operator, device ready (no lease) |
cannot be asked: no lease id | 200, the picture |
Operator, device leased |
200 through the lease id | 200 |
Operator, device shutdown |
cannot be asked | 409 DEVICE_NOT_RUNNING |
| Operator, device id unknown on that worker | — | 404 UNKNOWN_DEVICE |
| Operator, worker id unknown | — | 404 UNKNOWN_WORKER |
| Agent token, its own leased device | 200 through its lease id | 404 UNKNOWN_DEVICE |
Anyone, GET /v1/stream/leases/{id} |
200 or 404 UNKNOWN_LEASE |
404: no such stream path |
GET /v1/events?worker=<id>:
Daemon, worker |
Today | After |
|---|---|---|
Gateway, worker=w1 |
parameter ignored, every event | only events whose payload says workerId: "w1" |
Worker whose id is w1, worker=w1 |
ignored | every event |
Worker whose id is w1, worker=w2 |
ignored | no events |
Any, worker= (empty) or over 128 characters |
ignored | 400 |
What could go wrong
- The picture freezes or ends when an agent leases the device being watched. Unlikely; a test covers it on a real simulator.
- A device is erased while still streaming to the console after its lease ends. Unlikely; the reset stops the stream first.
- A picture freezes, still marked live, after its idle device shut down. Unlikely; the worker stops the stream when the device stops running.
- The Events tab on a busy gateway loads slowly. Possible on a large history; it reads 100 events at a time.
- A layout fix moves something else on the page. Possible; the browser tests measure each box.
Words used
- Running device: a device in state
ready(booted and idle, warm pool included) orleased. Only a running device streams. - Fleet of one: a single worker answers fleet routes as if it were a gateway with one worker, itself. Its worker id is its instance id, as
GET /v1/workersshows it. - Device drawer: the panel that opens over the worker page when you select a device row.
- Stream numbers: Picture, Frames, Rate, Bandwidth and Dropped, today shown under the picture.
Non-goals
- Sharing one capture between several viewers (One capture per lease, shared by every viewer of its screen #602). Each viewer is still its own capture.
- Letting an agent token watch a screen. Streams are for operators.
- A
--workerflag onsimlock events, or a worker filter on the MCP tools. - Booting a shut-down device to watch it.
- Any change to how a physical iPhone captures (ADR 0031).
Completion conditions
- On a gateway and on a single worker, an operator opens a
readydevice with no lease, picks Screen, and sees its picture. - While the picture runs, an agent leases that device, and the picture keeps running.
- After that lease ends, the picture stops before the device is reset.
- An agent token gets 404
UNKNOWN_DEVICEfrom the stream route, for its own leased device too. GET /v1/stream/leases/{id}answers 404 and the lease drawer has no Screen tab.- The ⓘ button shows the stream numbers; they are not under the picture.
- The worker page's Events tab fills the page and scrolls back to the oldest kept event of that worker, or to the console's event cap.
- The layout checks in the layout task pass in the browser lane.
Open questions
None.
Decisions
- ADR 0045 — A device's screen streams to operators, not a lease's (PR docs(adr): 0045 a device's screen streams to operators, not a lease's (#906) #908): the one route shape, operators only, running devices only, the refusal codes, and when a stream ends (viewer, capture, a lease end before reset, or the device stopping running). Numbered 0045 because PR docs(adr): 0044 the console reads what its page shows, and events mark it stale (#905) #907 holds 0044.
- ADR 0012 — A worker answers the fleet operations as a fleet of one: why a single worker answers the same
/v1/stream/workers/{workerId}/…path, for its own id. - Interview, 2026-10-23: the lease route is removed outright (nothing published uses it); the device drawer gets Details and Screen tabs, Details first; the picture starts when the Screen tab opens and stops when it closes, with no button; a not-running device keeps the tab and says why, and starts by itself when it becomes ready; the lease drawer loses its Screen tab with no link in its place; the worker filter is on
GET /v1/eventsonly, fleet of one on a worker; a ⋯ list never repeats the button beside it, anywhere; toasts are two columns with a borderless close button; the slow lane moves the stream tests to the device route and adds one "survives a lease" case on an iOS simulator.
Shared values
| Value or state | Rule | Written by | Read by | Source |
|---|---|---|---|---|
| Stream route | GET /v1/stream/workers/{workerId}/devices/{deviceId} on every daemon; {workerId} and {deviceId} are path segments, URL-encoded |
#909 | #910 | ADR 0045 §1 |
| Streamable states | ready, leased |
#909 | #910 (which sentence the Screen tab shows) | ADR 0045 §3; src/core/domain.ts:202-204 |
| Refusal: unknown device | 404, code UNKNOWN_DEVICE, message Unknown device: <deviceId> |
#909 | #910 | src/core/registry.ts:147; src/contract/errors.ts:324 |
| Refusal: unknown worker | 404, code UNKNOWN_WORKER, message Unknown worker: <workerId> |
#909 | #910 | src/gateway/worker-registry.ts:617; src/contract/errors.ts:441 |
| Refusal: not running | 409, code DEVICE_NOT_RUNNING, message The device is <state>; only a ready or leased device streams |
#909 | #910 | ADR 0045 §3 |
| The lease stream route | exists until #910 removes it with its last caller; #909 moves every slow-lane caller (rg -l stream/leases e2e/slow-*.test.ts: 5 files) to the device route, so #910's removal leaves only the console |
#909 keeps it, #910 removes it | — | src/stream/definition.ts:27; e2e/slow-android-stream.test.ts:93,157,212; slow-ios-physical.test.ts:494,602; slow-ios-physical-manual.test.ts:174; slow-android-physical.test.ts:190; slow-ios-stream.test.ts:78 |
docs/internal/DESIGN.md |
#910 edits the drawer bullet (ends at line 167); #912 edits the Menu bullet (starts at 168) and Toast. Adjacent lines: whichever lands second rebases the other's line, no wording shared | #910, #912 | — | docs/internal/DESIGN.md:157-184 |
website/.../worker/console.mdx |
#910 edits the Screen and device-drawer text; #911 the worker Events tab; #912 nothing | #910, #911 | — | console.mdx:126,159 |
Behaviours
| Behaviour (invoker, action, what it sees) | Seam | Cases | Task |
|---|---|---|---|
| An operator asks a single worker for a device's stream | createWorkerStream mount (src/stream/create.test.ts) |
ready → 200 frames; leased → 200; shutdown, provisioning, reclaiming, quarantined, physical absent → 409 DEVICE_NOT_RUNNING; unknown device → 404 UNKNOWN_DEVICE; another worker id → 404 UNKNOWN_WORKER; driver cannot capture → 415 |
#909 |
| An agent token asks for a device's stream | worker mount and gateway (create.test.ts, stream-gateway.e2e.test.ts) |
worker, device it holds → 404 UNKNOWN_DEVICE; gateway, device it holds → 404 UNKNOWN_DEVICE; nothing is opened |
#909 |
| An operator asks a gateway for a device's stream | src/daemon/stream-gateway.e2e.test.ts |
relayed → 200 frames; unknown worker → 404 UNKNOWN_WORKER; disconnected worker → 503 WORKER_UNREACHABLE; worker's 409 and 404 kept with their codes |
#909 |
| A watched device changes | createWorkerStream (create.test.ts) |
ready device leased → stream keeps running; lease ends → capture stopped before the reclaim hook returns; reclaimed before the first frame → 409 DEVICE_NOT_RUNNING; device.shutdown, device.absence-detected, device.quarantined, device.deleted for the device → capture stopped, stream ends; the same events for another device → nothing; capture ends → stream ends; viewer aborts → capture closed |
#909 |
| A real unleased simulator streams, and keeps streaming when leased | slow lane e2e/slow-ios-stream.test.ts |
unleased ready → frames; leased while streaming → frames continue; every other slow test that streams (slow-android-stream, slow-ios-physical, slow-ios-physical-manual, slow-android-physical) uses the device route |
#909 |
| An operator opens a device drawer and its Screen tab | e2e/console/screen.spec.ts |
opens on Details, no stream request; Screen → request to the device route and a picture, ?drawer=screen; closing drops drawer, so the next device opens on Details; leave tab → request aborted; close drawer → aborted; worker and gateway mode use the same URL shape; the device gets leased with the tab open → the same request stays open (one capture opened, none closed); the device is shut down while watched → the stream ends and the tab shows its sentence |
#910 |
| The Screen tab of a device that is not running | e2e/console/screen.spec.ts |
shutdown, provisioning, reclaiming, quarantined, absent → its sentence, no request; device turns ready while open → picture starts; streaming off on the worker → "Streaming is off on ."; unavailable → "Streaming is unavailable on . ", both as today |
#910 |
| An operator opens the stream numbers | e2e/console/screen.spec.ts |
ⓘ → popover with State, Picture, Frames, Rate, Bandwidth, Dropped; Escape and a second click close it; no numbers under the picture | #910 |
| An operator opens a lease drawer | e2e/console/leases.spec.ts |
tabs Details and Events only | #910 |
| A caller asks for a stream path that names no stream | service.test.ts |
the old GET /v1/stream/leases/lse_1 and other unmatched paths → 404 UNKNOWN_DEVICE "This path names no stream" |
#910 |
A caller pages GET /v1/events?worker= |
app.test.ts (refusals), e2e/gateway-event-relay.test.ts (pages), history.test.ts (memory) |
gateway w1 → only payload.workerId: "w1", worker.rejected excluded, paged with before and limit; with since; worker own id → all; worker other id → none; empty or 129 characters → 400 |
#911 |
| An operator opens a worker's Events tab | e2e/console/workers.spec.ts:783 |
fills the page height; scrolls back to older events of that worker until the start or the console's event cap; other workers' events never appear; a new event of the worker arrives at the top | #911 |
| The page reserves room for a scrollbar | layout spec | html's computed scrollbar-gutter is stable on Overview, Fleet and a worker page |
#912 |
| An operator opens a worker page | layout spec | no breadcrumb; warm pool tab has a 1px line between its facts and its table; going back to the Fleet with its page kept is the browser's Back (the tests that clicked the breadcrumb use it) | #912 |
| An operator looks at fleet and Inbox rows | layout spec | fleet row ⋯ inside its cell, 24px from the table's right edge; Inbox row buttons 8px from the row's top and bottom lines | #912 |
| An operator sees a toast | layout spec | close button inside the toast; text never under it; one-line toast's text and X share a centre line within 4px | #912 |
| An operator opens a ⋯ beside a button | ui/src/ds or model/actions Vitest, and layout spec |
drained worker header: Undrain button, ⋯ without Undrain; Inbox drained row: same; lease drawer, whose only action is dangerous: no button, ⋯ keeps Release lease; a ⋯ left with no entry is not drawn | #912 |
Tasks
flowchart LR
T09["#909 device stream route"] --> T10["#910 console Screen tab on the device"]
T11["#911 worker events history"]
T12["#912 layout fixes"]
- Stream any running device to an operator by worker and device id #909 Device stream route — an operator can stream any running device through
GET /v1/stream/workers/{workerId}/devices/{deviceId}, on a worker and through a gateway. Risk: a stream stops when its device is leased. - Console: watch a device from its drawer's Screen tab; the lease stream route goes #910 Console Screen tab on the device — an operator watches a device from its drawer; the lease route and the lease Screen tab are gone. Risk: the picture does not stop when the tab closes, and the worker keeps capturing.
- Console: the worker's Events tab scrolls back through its history #911 Worker events history — the worker page's Events tab scrolls back through that worker's whole history. Risk: events of another worker appear on the tab.
- Console: layout fixes from the 2026-10-23 review #912 Layout fixes — the scrollbar, breadcrumb, warm-pool line, row ⋯, Inbox row, toast and ⋯ duplicate faults are gone. Risk: a fix moves another box.
Completion conditions → tasks: 1–3 and 4 → #909 (server) and #910 (console); 5–6 → #910; 7 → #911; 8 → #912.
Open work in the same area:
- One capture per lease, shared by every viewer of its screen #602 (one capture per lease, shared): rekeyed by device after this feature; no Depends on, it waits for Stream any running device to an operator by worker and device id #909 by its own revise.
- The console stays smooth and light on a big fleet #905 (console smooth on a big fleet) and its ADR 0044 (PR docs(adr): 0044 the console reads what its page shows, and events mark it stale (#905) #907), tasks One lease is read directly, and gateway leases name the worker's lease id #913–Unchanged JSON answers under /v1 are 304 with an ETag #919: Console: the worker's Events tab scrolls back through its history #911 depends on The console reads per page through a query cache, and events mark data stale #914 (same
useWorkerEvents), The daemon says what needs the operator: GET /v1/inbox #915 (protocol bump; follows One lease is read directly, and gateway leases name the worker's lease id #913) and One shared recent-events list per console tab #917 (splitsEventFeed, sets the event cap to 1,000), and takes the cap the console has when it lands. Console: layout fixes from the 2026-10-23 review #912 changes the Inbox row inscreens/inbox/index.tsxonly, so it stays clear of The daemon says what needs the operator: GET /v1/inbox #915'sinboxActionsandstate/inbox.ts, and leavesuseWorkerPage(state/workers.ts, rewritten by The console reads per page through a query cache, and events mark data stale #914) untouched; no Depends on. - PR fix(e2e): move the mouse off the menu so a new toast is not hovered (#900) #903 / Console e2e: a success toast leaves after 6 s fails when the mouse rests over the new toast #900 (toast hover test): same toast area as Console: layout fixes from the 2026-10-23 review #912, different lines (a test's mouse moves). No Depends on.
- Console e2e: a worker is drained from Fleet with no dialog, and Undo undrains it fails on the row's drained text #886 (fleet row drained text e2e): same fleet row as Console: layout fixes from the 2026-10-23 review #912's ⋯ check. No Depends on.
- A worker joined to a gateway does not serve its own HTTP API or console #705 / Joined workers expose fleet access only through their gateway #806 (a joined worker serves no HTTP): a joined worker's stream is reached only through the gateway, as Stream any running device to an operator by worker and device id #909 has it. No Depends on.
- The warm pool learns what to keep warm, with no configuration #771 (learned warm pool): changes which devices are
ready, not how they stream. No Depends on.
Written by an agent.
Activity
docs(adr): 0044 one route shape on every daemon (#906)
V3RON commentedon Oct 23, 2026
Spec updated: Problem, business sections, Decisions (ADR 0044, PR #908), Shared values, Behaviours and Tasks added.
Written by an agent.
docs(adr): renumber to 0045; a device stream ends when its device sto…
V3RON commentedon Oct 23, 2026
Spec updated: ADR renumbered to 0045 (PR #907 holds 0044); a device stream also ends when its device stops running; slow-lane callers move in #909; Behaviours and Tasks now name #909–#912.
Written by an agent.
docs(adr): accept 0045 (#906)
V3RON commentedon Oct 23, 2026
Spec approved: ADR 0045 accepted (PR #908, stays open at the bottom of the stack); #909–#912 ready; feature planned.
Written by an agent.
V3RON commentedon Oct 23, 2026
Delivery run
Merged: none (the stack merges as a whole once every task has joined)
Parked: #909 (PR #923): the slow lane could not run, because the disk had 14.3 GiB free and the lane needs 15 (
needs-hardware)Waiting: #912 (PR #924), reviewed, in the stack; #910 (PR #928), reviewed, top of the stack; #911, on #914 and #915; ADR #908, reviewed, bottom of the stack
Decided: none
Written by an agent.
docs(adr): order the index rows 0044 before 0045 after the merge (#906)
docs(adr): 0045 a device's screen streams to operators, not a lease's (…
github-actions commentedon Oct 23, 2026
All 4 sub-issues are closed. If the completion conditions hold on
main, close this feature and flip its ADRs to Accepted.github-actions commentedon Oct 23, 2026
All 4 sub-issues are closed. If the completion conditions hold on
main, close this feature and flip its ADRs to Accepted.feat(history): the worker's Events tab scrolls back through its histo…
github-actions commentedon Oct 23, 2026
All 4 sub-issues are closed. If the completion conditions hold on
main, close this feature and flip its ADRs to Accepted.github-actions commentedon Oct 23, 2026
All 4 sub-issues are closed. If the completion conditions hold on
main, close this feature and flip its ADRs to Accepted.V3RON commentedon Oct 23, 2026
Delivery run
Merged: #908 (ADR 0045), #923 (#909), #924 (#912), #928 (#910), #943 (#911) — merged by the maintainer after the gate's third CI attempt
Parked: none
Waiting: none
Decided: none in this run (#912's dependency for #915 was dropped by the maintainer during #905)
Fixed during the run: the stack sync with #905 left
useLiveResourcein #910 (nowuseRouteQuery) and the ADR index rows out of order; #911's Events tab filter now follows the daemon mode when it arrives late.Written by an agent.