Skip to content

Console: watch a device from its drawer's Screen tab; the lease stream route goes #910

Closed

Description

@V3RON

Part of #906.

Scope

An operator watches a device's screen from the device drawer, on a single worker or a gateway, whether or not the device is leased. The drawer gets two tabs, Details and Screen, and opens on Details. The picture starts when Screen opens and stops when the operator leaves the tab or closes the drawer. The stream numbers move behind an ⓘ button. The lease drawer loses its Screen tab. With its last caller gone, GET /v1/stream/leases/{id} is removed.

In short

Today: Leases → a lease → Screen tab → Show screen. After: Fleet → a worker → a device → Screen tab, and the picture starts by itself. When the device is not running, the tab says why and starts by itself once the device is ready again.

flowchart TD
  R[Device row] --> D["Device drawer, Details tab (changed: tabs)"]
  D -->|Screen tab, ?drawer=screen| S{device state}
  S -->|ready or leased| P["Picture (new: no button)"]
  S -->|other| N[Its sentence]
  N -->|becomes ready| P
  P -->|ⓘ| I[Popover: stream numbers]
  P -->|leave tab, close drawer| X[Request aborted]
Loading

The tab's text, word for word (<worker> is the worker's name as the console shows it):

Device state, or worker Screen tab shows
ready, leased the picture
shutdown The device is shut down. Its screen shows while it runs.
provisioning The device is starting. Its screen shows once it is ready.
reclaiming The device is being reset. Its screen shows once it is ready.
quarantined The device is quarantined. It has no screen to show.
absent The device is not connected. Its screen shows once it is back.
deleted, or a state word the console does not know The device is <state>. It has no screen to show.
a 409 while the row still says ready or leased (the row is behind) the daemon's message, until the row catches up
worker with streaming off Streaming is off on <worker>. (as today)
worker where streaming is unavailable Streaming is unavailable on <worker>. <reason> (as today)

The ⓘ button (named "Stream details") opens a popover with State, Picture, Frames, Rate, Bandwidth and Dropped, the facts that sit under the picture today. Escape, a click outside or a second click closes it.

A new term: drawer query parameter, the device drawer's open tab in the URL, e.g. /fleet/w1/dev_1?tab=devices&drawer=screen. tab stays the worker page's tab. drawer belongs to one open drawer: closing the drawer drops it, and device row links never carry it, so the next device opens on Details.

If this goes wrong, a user would see the picture keep the worker capturing after they left the tab, or no picture for a running device.

Technical spec

Facts

# Fact Source
F1 The lease drawer's tabs are Details, Screen (keepMounted) and Events; Screen is ScreenPanel with leaseId, offered, reason ui/src/screens/leases/drawer.tsx:36-60
F2 ScreenPanel draws a Show screen/Stop button, the canvas (aria-label="The leased device's screen"), then Facts with State and the readout ui/src/screens/leases/screen-panel.tsx:8-74
F3 useScreen(leaseId) streams /v1/stream/leases/<id>; leaving or changing the lease aborts ui/src/state/screen.ts:47-84,123-130
F4 refusalOf words a 404 as "only for a lease that is still held" and a 415 as "This device cannot be streamed." ui/src/state/screen.ts:90-109
F5 The readout's facts are Picture, Frames, Rate, Bandwidth, Dropped, plus a State ui/src/model/screen.ts:41-62
F6 streamOffer(worker) reads the worker's stream capability and words "Streaming is off on ." and "Streaming is unavailable on . " ui/src/model/leases.ts:231-240
F7 The device drawer has props workerId, deviceId, no tabs, and an ActionBar only for a leased device; its row comes from the worker page (useDeviceDrawer) ui/src/screens/fleet/device-drawer.tsx:10-35; ui/src/state/workers.ts:164-175
F8 DeviceRow carries status from deviceStateStatus(device.state), not the raw state ui/src/model/worker-page.ts:290-302 and deviceRow
F9 The worker page keeps its tab in ?tab=; device links keep ?tab=devices ui/src/model/worker-page.ts:623-627; :335
F10 DrawerTabs takes defaultValue only; no URL ui/src/ds/drawer.tsx:64-89
F11 @base-ui/react is 1.9.0 and ships popover; nothing in ui/src imports it pnpm-lock.yaml:548; ls /Users/szymon.chmal/Projects/pitlane/node_modules/@base-ui/react → popover, version 1.9.0; rg base-ui/react/popover ui/src → none
F12 Icons are a hand-written path map with no info icon ui/src/ds/icon.tsx PATHS
F13 The console learns its mode and its worker list at runtime; on a worker, GET /v1/workers lists the worker itself with its instance id ui/src/data/mode.ts:4; src/daemon/dispatcher.ts:877-901
F20 Closing the device drawer keeps the whole query string; device row links keep every key but tab ui/src/state/workers.ts:172; ui/src/model/worker-page.ts:335,623-633
F21 Every unmatched stream path answers noSuchStream: 404 UNKNOWN_LEASE "This path names no stream lease" src/stream/refusal.ts:24-28; src/stream/service.ts:34; src/stream/service.test.ts:58
F22 The capture registry is keyed by lease id; #909 adds device keying beside it src/stream/captures.ts:21-22,59-94; #909
F23 deviceStateStatus passes a state word it does not know through ui/src/model/tones.ts:11-20,36-38
F24 The drawer is a modal Base UI Dialog; Escape closes it ui/src/ds/drawer.tsx:34
F14 Browser tests open the lease Screen tab and its button e2e/console/screen.spec.ts:55-60,124-270; e2e/console/leases.spec.ts:645-646
F15 A unit test streams the lease path ui/src/data/api.test.ts:273
F16 Docs describing the lease Screen: ui/AGENTS.md pages table "(Details, Screen, Events)"; website/.../worker/console.mdx:159; website/.../using-devices/watching-the-screen.mdx:8,55,69 those lines
F17 DESIGN.md: a drawer has "for a lease, tabs"; "a device's shows the device's state as a status" docs/internal/DESIGN.md:161-166
F18 The stream route after #909, its refusals and the streamable states #906 Shared values
F19 The lease route's server code: leaseIdOf, requireLease, holder in the gateway link, unknownLease, x-simlock-lease src/stream/service.ts:17-26,85; src/stream/worker-link.ts:113-130; src/stream/gateway-link.ts:7-62; src/stream/refusal.ts:20-28

Modules touched

Searches: rg "ScreenPanel|ScreenView|refusalOf|useScreen|stream/leases|leaseIdOf|requireLease|unknownLease|holder\(|DrawerTabs|streamOffer" over ui/src, src/, e2e, docs/internal, website/src/content/docs. Beyond F14–F16 they find e2e/console/leases.spec.ts:337 (the lease Screen tab), ui/src/screens/leases/leases.test.tsx:168-228 (ScreenView), ui/src/state/screen.test.ts:25-31 (the 404's lease words), src/stream/gateway-link.test.ts:82, watching-the-screen.mdx:103. The lease path also sits in src/daemon/stream-gateway.e2e.test.ts:144, src/http/openapi.test.ts:353-355, src/http/core-routes.test.ts:88, ui/src/data/stream.ts:2, ui/src/data/api.test.ts:273 and src/stream/{service,gateway-link,create}.test.ts; each moves to the device route or goes.

flowchart LR
  DD[screens/fleet/device-drawer *] --> SP[screens: ScreenPanel, moved *]
  SP --> US[state/screen: useScreen by worker+device *]
  US --> API[data/api stream]
  DD --> DT[ds/drawer DrawerTabs: value from URL *]
  SP --> PO[ds/popover new *]
  SP --> IC[ds/icon info *]
  LD[screens/leases/drawer *] -.Screen tab removed.-> SP
  ST[src/stream: lease route removed *]
Loading
sequenceDiagram
  participant O as Operator
  participant C as Console
  participant D as Daemon
  O->>C: Screen tab
  C->>D: GET /v1/stream/workers/w1/devices/d1
  D-->>C: 409 DEVICE_NOT_RUNNING
  C-->>O: "The device is shut down. Its screen shows while it runs."
  D-->>C: 200 frames
  O->>C: Details tab
  C->>D: abort
Loading

Contract and event changes

  • HTTP: GET /v1/stream/leases/{id} is removed, with its lease lookup, owner check and the lease keying of the capture registry (F19, F22). noSuchStream changes for every unmatched stream path, the old one included: 404 UNKNOWN_LEASE "This path names no stream lease" becomes 404 UNKNOWN_DEVICE "This path names no stream" (F21), and definition.ts's 404 lists UNKNOWN_DEVICE and UNKNOWN_WORKER only. The route's declaration leaves the OpenAPI document. No protocol bump (no src/contract change); no events.
  • Console URL: drawer query parameter, screen or absent (Details). Any other value reads as Details.

Other code on the same state

The console's live stream request and the worker's capture. Searched rg "useScreen|api.stream" ui/src (3 hits): the screen hook, its test, the API client. One ScreenPanel is mounted at a time per drawer; Base UI unmounts a tab panel that is not keepMounted, and the Screen tab is not, so leaving it aborts its request. Two browser tabs on one device run two captures (ADR 0045 §5). #909 serves the route; this task only reads it. #912 changes ActionBar, which the device drawer keeps using as is.

Failure modes

Call Outcome What happens Limit, counted from
Console → device stream 200 picture, live —
Console → device stream 409 DEVICE_NOT_RUNNING the state's sentence, or the daemon's message while the row still says ready or leased; retried when the row turns ready or leased —
Console → device stream 404 UNKNOWN_DEVICE / UNKNOWN_WORKER the daemon's message, as refusalOf does today without the lease words —
Console → device stream 415 "This device cannot be streamed." as today —
Console → device stream 503 / network error the daemon's message; the tab stays the head timeout STREAM_HEAD_TIMEOUT_MS, from the request, as today
Console → device stream stream ends the end reason, as today; a later ready row starts it again —

No state written.

Rules in play

  • ADR 0045 §1: one route shape on either daemon — the console builds the URL from the drawer's worker id and device id, in both modes.
  • ADR 0037: "the console acts through the HTTP routes, on Base UI" — the popover is Base UI's.
  • DESIGN.md drawer rule — rewritten: a device's drawer has Details and Screen tabs; a lease's has Details and Events.
  • ui/AGENTS.md Rules: "A screen never fetches" — the stream stays in state/screen.ts; the sentence table is a pure function in model/screen.ts.
  • documentation rule 9: console.mdx (the device drawer and its Screen tab; the lease drawer without it), watching-the-screen.mdx (Fleet path, picture not bound to a lease, the lease route gone), ui/AGENTS.md pages table, src/stream/AGENTS.md (lease route gone).

Tests

Seam: the browser lane e2e/console/screen.spec.ts against a started worker with the scripted capture program (its witness log counts captures opened and closed), plus ui/src/model/screen.test.ts for the sentence table, and src/stream/service.test.ts for the removed route.

  • An operator opens a device drawer and its Screen tab: opens on Details with no capture started; Screen → one capture opened and a picture painted, URL has drawer=screen; Details → the capture closed; closing the drawer → closed, and the URL has no drawer; opening another device after that → Details, no capture; the device is leased while the tab is open → the same request stays open (one capture opened, none closed); a gateway with one worker gives the same, through the same URL shape.
  • The Screen tab of a device that is not running: each state's sentence from the table, no capture opened (model unit test for the five states, deleted, an unknown word, and a 409 on a ready row; browser for shutdown); the device turns ready while the tab is open → the picture starts; the device is shut down while watched → the stream ends and the tab shows the shutdown sentence; the console reopens only after a refusal or an end, never on a ready→leased change; streaming off on the worker → "Streaming is off on ." with no request.
  • An operator opens the stream numbers: ⓘ "Stream details" → popover with State, Picture, Frames, Rate, Bandwidth, Dropped; Escape and a second click close it while the drawer stays open and no capture closes; no Picture fact is drawn outside the popover.
  • An operator opens a lease drawer: its tabs are Details and Events (leases.spec.ts:645 changes to this).
  • A caller asks for a stream path that names no stream: GET /v1/stream/leases/lse_1, /v1/stream/workers/w1, /v1/stream/devices/d1 → 404 UNKNOWN_DEVICE "This path names no stream", nothing opened (extends service.test.ts:58).

Done when

  • pnpm test and pnpm test:console pass, with the five Tests lines above.
  • rg -l "stream/leases" src ui e2e website docs/internal --glob '!docs/internal/adr/**' lists only the test that proves the old path answers 404.
  • The OpenAPI document has no /v1/stream/leases/{id}.
  • console.mdx, watching-the-screen.mdx, ui/AGENTS.md, src/stream/AGENTS.md and DESIGN.md's drawer rule describe the device drawer's Screen tab.

Out of scope

Depends on

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
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