Hacktoberfest 2026: los issues que los mantenedores marcaron para octubre, abiertos y aptos para principiantes. Explorar issues de Hacktoberfest

Console: release, cancel, drain, undrain and remove from the console

Abierto
#769 0 comentarios 0 reacciones 0 asignados Ver en GitHub

Los mantenedores suelen responder en 1 día

Nadie ha tomado este issue todavía.

Evaluación

Dificultad
5/5
Tiempo estimado
Más de una semana
Aptitud para principiantes
25/100
Tipo de issue
Nueva funcionalidad
Claridad
Bien especificado
Estado de actividad
Activo
Stack tecnológico
react, typescript

Línea de trabajo

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.

Escrito por el modelo de indexación a partir del texto del issue.

Descripción

task:ready

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: a write(method, path) on the client, sharing send, the token, credentials and the 401 path; a network error or timeout on a write throws without calling refuse.
  • ui/src/state/services.tsx: passes the app's notify and confirm to state. The mode comes from #766's useDaemonMode (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 asks confirm(spec) when the action has a confirm, sends the write, and calls notify(toast) with the outcome. It keeps the in-flight flag per item. confirm and notify are plain functions the app passes in, typed with ds types 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 the notify and confirm functions 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 200 removed: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_CANCELLABLE with 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: Bearer to /v1": write goes through send.
  • 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.mjs layer 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/actions offers 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)
  • write sends the method and path with the token and credentials: "same-origin", maps an error body to ApiError, sends a 401 to the session, and on a network error throws without calling refuse (extend api.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 becomes connected
  • 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.route aborts 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.route holds 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 the aria-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-origin passes (extend proxy-sign-in.spec.ts)
  • no <style> element or CSP violation with a menu, a dialog and a toast open (extend views.spec.ts); axe passes with each open, in light and dark (extend shell.spec.ts's axe pass)

Done when

  • pnpm test:console --project=chromium passes, 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.

Lenguaje dominante
TypeScript
Estrellas
20
Forks
2
Merge medio
8 h 13 min
PR fusionados (30 d)
297

Preparar el entorno

Este proyecto no incluye contenedor de desarrollo, Dockerfile ni guía de contribución, así que la configuración corre por tu cuenta: empieza por su README y consulta nuestra guía para la primera contribución para los pasos generales.

Primeros pasos

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Más de callstackincubator/simlock

Todos los issues de callstackincubator/simlock

Issues similares

Más issues de TypeScript

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.