Console: release, cancel, drain, undrain and remove from the console
维护者通常 1 天内回复
还没有人认领这个 Issue。
评估
- 难度
- 5/5
- 预计耗时
- 一周以上
- 新手友好度
- 25/100
- Issue 类型
- 功能
- 描述清晰度
- 描述清楚
- 活跃度
- 活跃
- 技术栈
- react, typescript
调研方向
Start with ui/src/data/api.ts and ui/src/state/actions.ts, then trace how the screens in ui/src/screens/leases, queue, fleet and inbox provide each item's actions. The issue names the relevant API routes and e2e/console/actions.spec.ts; run the console e2e coverage after implementing the shared action flow. Done means all five actions work from the listed surfaces, with the specified confirmations, in-flight disabling, toast outcomes and gateway-only worker controls.
由索引模型根据 Issue 内容生成。
描述
Part of #756.
Scope
The console can act. After this PR an operator can do five things without leaving the console: release a lease, cancel a waiting request, drain a worker (and undo it), undrain a worker, and remove a disconnected worker. Each action is reachable from the row's ⋯ menu, a right-click on the row, and the item's drawer. A drained worker's Inbox row also has a one-click Undrain.
This task stays whole at about 20 modules: the menu, dialog, toast and runner are one path that every action goes through, and a slice without one of them has no action a user can finish. Each ends in a toast. On a single host the worker actions never appear.
In short
Every table row ends in a ⋯ button that opens a menu, and a right-click on the row opens the same menu. The menu lists the item's actions. Dangerous ones (Release lease, Cancel request, Remove worker) come last, after a line, in the error colour. The drawer shows the item's first safe action as a button and every action under its own ⋯.
Only one Inbox item has a fix button: a drained worker's row shows Undrain before its ⋯. Every other Inbox item has none, and its actions are under ⋯ and in its drawer.
| Action | Offered on | Before it runs | Toast on success |
|---|---|---|---|
| Release lease | a held lease (Leases, Inbox heads-up, device drawer) | dialog "Release <holder>'s lease?" — "<device> on <worker> goes back to the pool now, and <holder>'s next command on it fails." Buttons Keep, Release lease |
"Released <holder>'s lease" — "<device> on <worker> goes back to the pool." Action View device |
| Cancel request | a waiting request (Queue, Inbox) | dialog "Cancel <requester>'s request?" — "<requester> stops waiting for <device> and gets an error." Buttons Keep, Cancel request |
"Cancelled <requester>'s request" |
| Drain | gateway; a worker not drained (Fleet, worker page) | nothing | "<worker> drained" — "Its leases keep running. New leases go to other workers." Action Undo (undrains) |
| Undrain | gateway; a drained worker (Fleet, worker page, Inbox fix button) | nothing | "<worker> takes leases again" |
| Remove worker | gateway; a disconnected worker (Fleet, worker page, its Inbox item's ⋯) |
dialog "Remove <worker>?" — "It leaves the fleet and its devices are forgotten. It can join again with a worker token. Type <worker> to confirm." Remove worker stays disabled until the typed text equals <worker> |
"<worker> removed" |
The placeholders are filled as follows:
<holder>is the holder name the Leases page shows.<device>is the device's model for a lease, and the device asked for, as the Queue page words it, for a request.<worker>is the worker's name (its label, else its id).<requester>is the requester name the Queue page shows.
When the daemon refuses, an error toast stays until it is closed. Its text is the daemon's message, and it has the action Retry. Its title, word for word:
- "Couldn't release
<holder>'s lease" - "Couldn't cancel
<requester>'s request" - "Couldn't drain
<worker>" (and after Undo, "Couldn't undrain<worker>") - "Couldn't undrain
<worker>" - "Couldn't remove
<worker>"
A 401 is not a toast: it signs out, as for a read. A network error or no answer is a toast whose text is "The daemon did not answer." It never signs out and never starts the proxy identity check, unlike a read.
While an action is in flight (up to the client's 10 s timeout), its dialog's button is disabled and reads "Releasing…", "Cancelling…" or "Removing…". That item's menu entries, its drawer button and its Inbox Undrain are disabled too, so a second click sends nothing. The in-flight flag is kept per item key in state/actions.ts, shared by every surface. A dialog keeps its item and stays open when the row leaves the list in a refetch; a menu whose item leaves closes.
Toasts sit in the bottom-right corner, at most three, newest at the bottom. Older ones wait hidden and come back as newer ones close, so an error toast is never lost. A success toast leaves after 6 s, paused while hovered or focused. Base UI's toast region announces them: errors at once (high priority, copied to its role="alert" element), the rest politely (its aria-live="polite" region). A toast animates only when it appears.
After sign-in, and again after each reconnect, the console reads GET /v1/status and keeps daemon.mode (#766's useDaemonMode). It offers Drain, Undrain and Remove only when that is gateway. If the read fails, it offers none until a read succeeds.
sequenceDiagram
participant O as Operator
participant C as Console
participant D as Daemon
O->>C: Release (row ⋯)
C-->>O: dialog "Release agent-1's lease?"
O->>C: Release lease
C->>D: DELETE /v1/leases/{id}
alt 202
D-->>C: { released: true }
D-->>C: event lease.released
C->>D: refetch
C-->>O: toast "Released agent-1's lease" [View device]
else 403 / 404 / 5xx
D-->>C: { error: { code, message } }
C-->>O: toast stays "Couldn't release agent-1's lease" [Retry]
end
If this goes wrong, an operator ends someone's lease by accident, or an action fails silently and the operator believes it worked.
Technical spec
Facts
| # | Fact | Source |
|---|---|---|
| F1 | The API client can only GET: send() sets no method or body; ApiClient has getJson, get, stream, healthz |
ui/src/data/api.ts:36-58,105 |
| F2 | A 401 calls onRefused (the session) or signOut; a 401/403 with no Simlock code is "not-simlock"; any other non-ok throws ApiError(status, code, message) from the body's error, with fallback "The daemon answered N" |
ui/src/data/api.ts:90,150,165 |
| F3 | DELETE /v1/leases/{id} → 202 {device:{id,state},released:true}; an operator may release any lease; unknown → 404 UNKNOWN_LEASE; on a gateway a lease on an unreachable worker fails through WORKER_UNREACHABLE |
src/http/app.ts:603-629, docs/HTTP-API.md:1309-1313, src/gateway/fleet-coordinator.ts:379,1403 |
| F4 | DELETE /v1/lease-requests/{id} → 204; 404 UNKNOWN_LEASE_REQUEST; 409 REQUEST_NOT_CANCELLABLE (granted: carries leaseId, "release it instead"; else device work in flight); an operator may cancel any request |
src/http/app.ts:376-384,781-783 |
| F5 | Drain POST /v1/workers/{id}/drain and undrain DELETE …/drain → 200 {workerId,drained}; unknown → 404 UNKNOWN_WORKER; idempotent, the second call emits nothing; no connection check |
src/http/app.ts:665-675, src/gateway/worker-registry.ts:465,604, docs/HTTP-API.md:1520-1527 |
| F6 | Remove DELETE /v1/workers/{id} → 200 {workerId,removed:true}; 409 WORKER_CONNECTED for a connected or incompatible worker; unknown → 200 removed:false |
src/http/app.ts:685, src/gateway/worker-registry.ts:511, docs/HTTP-API.md:1529-1535 |
| F7 | The three worker routes are role admin (operator) and answer 501 UNSUPPORTED_IN_WORKER_MODE on a single host |
src/contract/operations.ts:750, src/daemon/dispatcher.ts:327 |
| F8 | Events after each action: lease.released (HTTP release reason explicit), lease.rejected with cancelled, worker.drain-started, worker.drain-ended, worker.removed (reason operator) |
src/daemon/dispatcher.ts:584, docs/EVENTS.md:18,109-110, src/gateway/worker-registry.ts:469 |
| F9 | Any event refetches the routes the current screen reads | ADR 0013 §3 (docs/internal/adr/0013-…md:79-83) |
| F10 | A proxy-signed identity needs Sec-Fetch-Site: same-origin on any non-GET, else 403 FORBIDDEN "Cross-site request refused…"; tokens are exempt |
src/http/auth.ts:27-28 |
| F11 | GET /v1/status carries daemon.mode, worker or gateway |
src/contract/operations.ts:117, docs/HTTP-API.md:229,388 |
| F12 | The error body is { error: { code, message, ...details } } |
src/http/AGENTS.md (How a request runs, step 7) |
| F13 | The layers: screens may not import data or model; state may import only ds types; a write must reach a screen through a state/ hook |
.dependency-cruiser.mjs:219-253 |
| F14 | e2e can start a gateway with fake workers, stop one (disconnected), hold leases and start waiting requests | e2e/console/fixtures.ts:42,193-238, e2e/console/workers.spec.ts:716, e2e/console/waiting.spec.ts:21-36 |
| F15 | Base UI 1.9.0 ships Menu with a context-menu trigger, AlertDialog and Toast |
@base-ui/[email protected] menu/, context-menu/, alert-dialog/, toast/ directories in a scratch install |
Modules touched
Searches: ApiClient, send(, ApiError, useApi, DataTable, Drawer, "/v1/status", inboxItems.
ui/src/data/api.ts: awrite(method, path)on the client, sharingsend, the token,credentialsand the 401 path; a network error or timeout on a write throws without callingrefuse.ui/src/state/services.tsx: passes the app'snotifyandconfirmto state. The mode comes from #766'suseDaemonMode(ui/src/data/mode.ts).ui/src/model/actions.ts(new): pure. For each kind of item (lease, request, worker, Inbox item), the actions, when each applies, its confirm kind, danger, and the wording from In short.ui/src/state/actions.ts(new):useActions().run(action)is the one runner. It asksconfirm(spec)when the action has a confirm, sends the write, and callsnotify(toast)with the outcome. It keeps the in-flight flag per item.confirmandnotifyare plain functions the app passes in, typed withdstypes only.ui/src/ds/:menu.tsx(row ⋯ and right-click, on Base UI Menu and ContextMenu),confirm-dialog.tsx(Base UI AlertDialog, type-to-confirm, one host),toast.tsx(Base UI Toast; its manager is the only toast queue and owns the timer, the limit and closing),table.tsx(an action column),drawer.tsx(action bar),index.ts.ui/src/screens/leases,queue,fleet,inbox: pass each row's actions.ui/src/app/app.tsx: the toast region and the confirm host, and thenotifyandconfirmfunctions they back.e2e/console/actions.spec.ts(new),proxy-sign-in.spec.ts,views.spec.ts,shell.spec.ts,fixtures.ts.docs/CONSOLE.md(actions section; "it does not fix them" goes),docs/internal/DESIGN.md(menu, dialog, toast parts),ui/AGENTS.md.
flowchart LR
scr["screens/* (changed)"] --> sa["state/actions (new)"]
scr --> ds["ds: Menu, ConfirmDialog, Toast (new)"]
sa --> ma["model/actions (new)"]
sa --> api["data/api write (changed)"]
sa --> mode["data/mode (new)"]
ds --> base["@base-ui/react"]
Contract and event changes
None. The console calls five routes that exist and reads daemon.mode from GET /v1/status, both unchanged. No event changes.
Other code on the same state
The state is the daemon's leases, requests and workers. Searches: simlock release, worker drain, worker remove, lease.cancel. The CLI, the HTTP API and agents change the same state.
- Two operators act at once: drain and undrain are idempotent (F5), so the second gets 200 and the same state. A second release gets 404
UNKNOWN_LEASE, shown as an error toast. A second remove gets 200removed:false, shown as "<worker>removed". - An agent releases its own lease while the dialog is open: the confirm then gets 404 and an error toast.
- A request is granted while the cancel dialog is open: 409
REQUEST_NOT_CANCELLABLEwith the daemon's "release it instead" message. - The console shows no result before the daemon reports it (ADR 0013 §1). The refetch on the action's event shows it.
Failure modes
| Call | Outcome | What happens | Limit, counted from |
|---|---|---|---|
| any of the five | 2xx | success toast; the list updates on the next event refetch | the client's request timeout, from send |
| any | 401 | signed out, as for a read; no toast | — |
| any | 403 FORBIDDEN (proxy, cross-site) |
error toast with the daemon's message | — |
| release | 404 UNKNOWN_LEASE |
error toast | — |
| release on a gateway | WORKER_UNREACHABLE |
error toast with the daemon's message | — |
| cancel | 404 / 409 | error toast with the daemon's message | — |
| drain, undrain | 404 UNKNOWN_WORKER |
error toast | — |
| remove | 409 WORKER_CONNECTED |
error toast: the daemon's message says to stop the worker first | — |
| any | network error or timeout | error toast with the text "The daemon did not answer." and Retry; no sign-out and no identity check | the client's 10 s timeout, from send |
| any | a second click while in flight | nothing sent | — |
GET /v1/status |
fails or no daemon.mode |
no worker actions; read again on the next sign-in or reconnect, and the worker actions appear once it succeeds | — |
The console writes no state of its own. A reload in the middle loses only the toasts.
Rules in play
- ADR 0037 §3: one action list per kind, in
model/; the confirm rules; no optimistic state. - ADR 0037 §4: the mode read, and none on failure.
- ADR 0037 §5: the toast rules.
- ADR 0011 §6, "sends it only as
Authorization: Bearerto/v1":writegoes throughsend. - DESIGN.md "Orange ... never ... state": danger items and error toasts use the error colour, not orange. DESIGN.md "Buttons are verbs": every action label is a verb phrase.
.dependency-cruiser.mjslayer rules (F13).
Tests
Seam: the browser lane against startFleet and a single host (new e2e/console/actions.spec.ts, modelled on e2e/console/workers.spec.ts). The rules for which action shows where run in Node in model/actions.test.ts, modelled on ui/src/model/attention.test.ts. write runs in Node beside ui/src/data/api.test.ts.
model/actionsoffers exactly the table's actions: a held lease → Release; a waiting request → Cancel; on a gateway a connected undrained worker → Drain, a drained one → Undrain, a disconnected one → Drain or Undrain plus Remove; on a single host or an unknown mode, no worker action; dangerous actions are last and marked danger, after a separator; the only Inbox fix is Undrain on a drained worker (model test)- the row menu shows the dangerous items last, after a separator, in the error colour (browser line on a Fleet row of a disconnected worker)
writesends the method and path with the token andcredentials: "same-origin", maps an error body toApiError, sends a 401 to the session, and on a network error throws without callingrefuse(extendapi.test.ts)- release from a lease drawer's ⋯ works as from the row; a worker drawer's button is its first safe action (Drain or Undrain)
- release from a Leases row's ⋯: the dialog's wording as in In short, Keep closes it with nothing sent, Release lease sends the DELETE, the toast reads "Released
<holder>'s lease", View device opens the device drawer, and the row leaves the list - cancel from Queue: dialog, the toast, the request leaves the list
- drain from Fleet: no dialog, the toast, the state becomes
drained; Undo undrains and the state becomesconnected - undrain from the Inbox's fix button: the item leaves the Inbox
- remove a stopped worker: Remove worker is disabled until the name is typed exactly; the toast; the row leaves Fleet. A connected worker offers no Remove.
- a right-click on a row opens the same menu as its ⋯; Escape closes it; the keyboard moves through it
- a refused action shows the error toast titled "Couldn't release 's lease" with the daemon's message and Retry, which sends again (provoke one with a release of a lease that ended while the dialog was open; the dialog stays open as the row leaves)
- a write that gets no answer (
page.routeaborts it) shows "The daemon did not answer." and the operator stays signed in - while a release is in flight its button reads "Releasing…" and is disabled; a second click sends no second DELETE (
page.routeholds the answer) - a success toast leaves after 6 s and stays while hovered; an error toast stays until closed; at most three show, and a fourth hides the oldest until one closes
- an error toast's text is in the
role="alert"element and a success toast's in thearia-live="polite"region - on a single host no menu, drawer or Inbox row offers Drain, Undrain or Remove
- signed in through the sign-in proxy (the existing single-host fixture), a release from the console succeeds: the browser's
Sec-Fetch-Site: same-originpasses (extendproxy-sign-in.spec.ts) - no
<style>element or CSP violation with a menu, a dialog and a toast open (extendviews.spec.ts); axe passes with each open, in light and dark (extendshell.spec.ts's axe pass)
Done when
pnpm test:console --project=chromiumpasses, and CI's Console check passes in every browser.
Out of scope
- New routes and actions (retry wipe, delete device, extend lease, cleanup, snooze). ⌘K (#770). Who did an action (#703).
Depends on
- #766
- #767
- #768
Written by an agent.
- 主要语言
- TypeScript
- 星标
- 20
- 派生
- 2
- 平均合并
- 8 小时 13 分钟
- 30 天内合并 PR
- 297
环境准备
这个项目没有提供开发容器、Dockerfile 或贡献指南,环境需要你自己搭建:先看它的 README,通用步骤见我们的新手贡献指南。
从这里开始
- 先读完整个 Issue,再读项目的贡献指南。
- 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
- Fork 仓库,在一个分支上完成修改。
- 提交 Pull Request,并在描述里引用这个 Issue 编号。
callstackincubator/simlock 的其他 Issue
-
难度 2/5 1-3 小时 新手友好度 70/100
callstackincubator/simlock#639 · 1 条评论 ·
维护者通常 1 天内回复
-
feature:spec
难度 5/5 一周以上 新手友好度 25/100
callstackincubator/simlock#771 ·
维护者通常 1 天内回复
-
task:ready
难度 5/5 一周以上 新手友好度 25/100
callstackincubator/simlock#770 ·
维护者通常 1 天内回复
-
Console: Overview home and Inbox可能已有人在做 关联的 PR 仍在进行中或已合并。 未关闭task:ready
难度 5/5 一周以上 新手友好度 38/100
callstackincubator/simlock#768 ·
维护者通常 1 天内回复
-
Console: Queue and Activity in the shared frame, Activity scrolls back through history可能已有人在做 关联的 PR 仍在进行中或已合并。 未关闭task:ready
难度 4/5 3-5 天 新手友好度 48/100
callstackincubator/simlock#767 ·
维护者通常 1 天内回复
查看 callstackincubator/simlock 的全部 Issue
相似的 Issue
-
bug
难度 2/5 1-3 小时 新手友好度 78/100
维护者通常 1 天内回复
-
难度 2/5 1-3 小时 新手友好度 67/100
mobile-next/mobile-mcp#526 ·
维护者通常 1 天内回复
-
e2e-failure ready-to-code
难度 1/5 1 小时以内 新手友好度 73/100
redhat-developer/rhdh-plugin-export-overlays#4470 · 1 条评论 ·
维护者通常 1 天内回复
-
难度 2/5 1-3 小时 新手友好度 68/100
anthropics/anthropic-sdk-typescript#1273 ·
维护者通常 10 天内回复
-
难度 2/5 1-3 小时 新手友好度 82/100
CopilotKit/OpenBot#815 ·
维护者通常 1 天内回复