Skip to content

The daemon says what needs the operator: GET /v1/inbox #915

Closed

Description

@V3RON

Part of #905.

Scope

The daemon decides what needs the operator: GET /v1/inbox returns the Inbox items, in both modes. The sidebar count reads only it, on every page. The Inbox page and the Overview's Needs you read it plus /v1/workers (for worker and device names, links and lease counts), and no longer read leases, tokens or waiting requests. The rules move from the console to src/contract/inbox.ts in this task, so they live in one place.

In short

Same Inbox, same count, same buttons. The sidebar count, on every page, stops reading the four fleet lists; the Inbox page and Needs you keep only the worker list. Each item carries a small copy of what it is about, e.g.

{ "key": "w1/dev_9/quarantined", "group": "Broken", "condition": "quarantined",
  "device": { "id": "dev_9", "state": "quarantined", "workerId": "w1", "workerLabel": "mac-07", … } }
flowchart LR
  S[sidebar count / Inbox / Needs you] --> Q["['inbox'] (new)"] --> R["GET /v1/inbox (new)"] --> D["inbox.get → src/contract/inbox.ts (moved)"]
Loading

The Inbox page's "could not check" note goes: the daemon reads its own data, and a failed GET /v1/inbox shows the page's refusal instead.

If it goes wrong: the count or the list differs from what the Fleet and Leases pages show.

Technical spec

Facts

# Fact Source
F1 inboxItems(workers, {now, requests, leases, tokens}) builds worker, device, waiting and ending items; keys <worker>/<condition>, <worker>/<device>/<condition>, request/<id>/waiting, lease/<id>/ending. ui/src/model/inbox.ts:62-150
F2 Thresholds: waiting > 60 s (WAITING_TOO_LONG_MS); ending when ttlDeadline - now < 5 min (EXPIRING_UNDER_MS); RAM from the daemon's overLimit. ui/src/model/inbox.ts:60, 87; ui/src/model/leases.ts:138-141; src/contract/schemas.ts:509-517
F3 Groups: ending → Heads-up; error tone (disconnected, incompatible, quarantined) → Broken; else Needs a look. problemCount counts items not Heads-up. ui/src/model/inbox.ts:153-175
F4 Helpers the rules use: holderName, deviceOfLease, isExpiring (model/leases.ts:64, 102, 140), stateEnteredAt (model/workers.ts:57), requestedDevice (model/waiting.ts), and the helpers they call: holderOf, agentIdOf, workerOfLease (model/leases.ts:44-78), platformName (model/workers.ts:26). as cited
F5 Callers: useInboxCount (state/shell.ts:12-19), useInbox (state/inbox.ts:87-150), overviewOf (model/overview.ts:187, 224-232), the Activity drawer's link (model/event-list.ts:156). as cited
F6 Row buttons need the worker's connection, drained, update facts, and for the Update… confirm the count of leased devices (updateFactsOf counts worker.devices); a waiting request's spec, id, requesterId, workerId; an ending lease's row, built with leaseRows({leases, tokens, workers}), which needs the worker list for the Release confirm's worker name and the toast's device link. ui/src/model/actions.ts:62, 81-83, 171-172, 290-345; ui/src/model/versions.ts:16; ui/src/model/leases.ts:179-196
F7 The four input types are contract types. ui/src/model/leases.ts:10, 16; workers.ts:5; waiting.ts:9
F8 A worker builds its own worker view in #workerList; a gateway answers from workers.views(). Both dispatchers serve lease.list, list.get {kind:"requests"} and token.list. src/daemon/dispatcher.ts:870-910, 627, 812-820, 1044; src/gateway/dispatcher.ts:244-255, 413, 517, 545-578
F9 src/contract/ imports nothing outside itself; the console may import contract values only from version-order.ts, by path (maintainer-authorized, #825). src/contract/AGENTS.md:5-6; .dependency-cruiser.mjs:31; ui/AGENTS.md Uses
F10 A read op is added as usage.get was: contract op, both dispatchers, a declared route. commit 60521f0f4; src/http/route-declarations.ts:73; src/http/app.ts:717
F11 The rules' tests. ui/src/model/inbox.test.ts, ui/src/state/inbox.test.ts
F12 The Inbox drawer's event lines name the worker and the device from the worker list. ui/src/model/inbox.ts:472-483; ui/src/model/activity.ts:17-41
F13 Console browser specs fill the Inbox, Needs you and the count by stubbing /v1/lease-requests and /v1/leases; two expect a "Could not check" note. e2e/console/inbox.spec.ts:211-290, 371, 602-650; overview.spec.ts:364-468; shell.spec.ts:270, 292; views.spec.ts:289
F14 The Inbox screen shows couldNotCheck; the Overview takes it from useInboxReads. ui/src/screens/inbox/index.tsx:96-102; ui/src/state/overview.ts:12

Modules touched

src/contract/inbox.ts (new: the rules of F1–F3 and the helpers of F4, zod-free), src/contract/operations.ts + schemas.ts (inbox.get), src/contract/protocol.ts (bump), src/contract/index.ts, src/daemon/dispatcher.ts, src/gateway/dispatcher.ts, src/http/admin-declarations.ts + app.ts (GET /v1/inbox), .dependency-cruiser.mjs (the console may import src/contract/inbox.ts by path, as version-order.ts), ui/src/model/inbox.ts (rules removed; rows, tones, filters stay), ui/src/model/leases.ts, workers.ts, waiting.ts (helpers imported from src/contract/inbox.ts), ui/src/model/actions.ts (inboxActions from the subject), ui/src/model/overview.ts, ui/src/model/event-list.ts (the Activity drawer's "Open Inbox item" link imports inboxItems from src/contract/inbox.ts; no extra read), ui/src/state/inbox.ts, shell.ts, overview.ts, queries.ts (['inbox'], poll 10 s) and ui/src/model/stale-keys.ts (inbox in the Event map rows), ui/src/screens/inbox/index.tsx and the Overview screen (the "Could not check" note goes, F14), the console browser specs of F13 (they stub GET /v1/inbox with items in the new shape instead of the lists), tests beside each, ui/AGENTS.md, src/contract/AGENTS.md, src/http/AGENTS.md, website/.../gateway/fleet-console.mdx (Attention section, if it names how the count is found). Every helper of F4 moves once and the console imports it from there. Searches: inboxItems, problemCount, holderName, holderOf, agentIdOf, workerOfLease, platformName, deviceOfLease, stateEnteredAt, isExpiring, requestedDevice, couldNotCheck, useInboxReads.

sequenceDiagram
  participant C as Console
  participant H as HTTP app
  participant D as Dispatcher
  C->>H: GET /v1/inbox (operator token)
  H->>D: inbox.get
  D->>D: worker views, leases, waiting requests, tokens → inboxItems(now)
  D-->>H: {items}
  H-->>C: 200
  Note over C,H: agent token → 403 FORBIDDEN
Loading

Contract and event changes

  • New op inbox.get, role admin, effect read, input {}, output { items: InboxItem[] } with the item shape of The console stays smooth and light on a big fleet #905 Shared values inbox.get, plus leasedDevices (non-negative integer) on the worker subject for the Update… confirm and stateEnteredAt (optional epoch ms) on the device subject. heldLeases is a non-negative integer; every other field reuses its existing schema (worker view fields, device, statusLeaseSchema, waiting request).
  • New route GET /v1/inbox, tag admin, no query.
  • This PR bumps the protocol by one. No event changes.

Other code on the same state

Reads the same worker views, lease index, request book and tokens table as worker.list, lease.list, list.get and token.list (F8), never writes. An item computed while a view refreshes shows that view as of the read, as those routes do. Searches: views() (gateway, 9 hits), registry.snapshot (daemon, 14 hits).

Failure modes

Call Outcome What happens Limit, counted from
inbox.get ok items none
inbox.get token store unavailable holder falls back to the requester id, as the console does with no tokens today —
GET /v1/inbox agent token 403 FORBIDDEN —
console read refused as every query (#914): last items kept, message shown; the page's refusal replaces the "Could not check" note —
console read lost as every query (#914): data kept, no error —

No state is written.

Rules in play

  • Architecture rule 10 (one rule, one place): the console copy of the rules and helpers is deleted; both sides import src/contract/inbox.ts.
  • ADR 0012 "a worker answers the fleet operations as a fleet of one": inbox.get on a worker uses its own worker view.
  • ADR 0044 §3: complies. Toolchain protocol bump: bumps.

Tests

Seams: the rules in src/contract/inbox.test.ts, moved from ui/src/model/inbox.test.ts; the route through src/http/app.test.ts; the console through the query layer and renderToStaticMarkup, as ui/src/screens/inbox/index.test.tsx.

  • The inbox rules turn fleet data into items: each worker (disconnected, incompatible, drained, update failed, over RAM), device (quarantined, stalled, absent), waiting (59 s no, 61 s yes) and ending (4 min yes, 6 min no) condition; group, order and key of each; each subject's fields, heldLeases included.
  • An admin reads GET /v1/inbox: gateway with two workers; worker (fleet of one); agent token → 403.
  • The sidebar count, Inbox and Needs you show the server's items: count leaves out Heads-up; the keys observed on Usage include no workers, leases, tokens or lease-requests, and on Inbox and Overview's Needs you no leases, tokens or lease-requests (Needs you means the panel's own reads; the Overview's Waiting and Ending soon tiles keep reading lease-requests and leases) (decided in delivery, accepted by the maintainer 2026-10-23); a drained item's fix button is Undrain and an update-failed one's is Update… with the leased-device count in its confirm; an ending item's menu is Release with the worker's name; drawer facts (time in state, last seen, held leases) and event lines (worker and device names) as today.

Done when

  • pnpm vitest run src/contract src/http ui/src passes; ui/src/model/inbox.ts has no inboxItems.
  • The boundaries and protocol bump checks pass.
  • The console browser lane passes, with the specs of F13 stubbing GET /v1/inbox and the "Could not check" cases removed.

Out of scope

Paging the items; new conditions; the Inbox row's layout (#912).

Depends on

Not on #912 (maintainer, 2026-10-23): the two only overlap in inboxActions, state/inbox.ts and the Inbox screen, and waiting on it made a loop with #906 (#912 merges with #906's stack, whose #911 needs this task). The feature that merges second resolves the overlap.

Written by an agent.

Activity

  1. added
    task:draftScope written; technical spec or its spec check missing.
    on Oct 23, 2026
  2. changed the title [-]The console reads per page through a query cache, and events mark data stale[/-] [+]The daemon says what needs the operator: GET /v1/inbox[/+] on Oct 23, 2026
  3. 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
  4. assigned and unassigned on Oct 23, 2026
  5. added 2 commits that reference this issue on Oct 23, 2026
    02d4aab
    ab4698e
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