Repository navigation
Console Usage page #634
Description
Part of #623.
Scope
After this PR, the console has a Usage page at /usage, last in the sidebar. It shows the full figures for the chosen range (1 hour, 6 hours, 24 hours, 7 days): requests, grants and how each was served, wait, held time and turnaround, provisioning and boot time, rejections by reason, device incidents, slots, RAM and queue use over the range, and use per platform, per worker and per requester. The Overview's Granted tile now opens it. They are the same figures simlock stats --json and GET /v1/stats give for that range.
In short
The page reads GET /v1/stats?since=<range> (#632) and draws it as stat tiles and tables. It uses #633's range picker, its ?range= rule and its partial note, and fetches the route like every other (feature #623 Shared values, "Console ranges"). It works at 360 px wide and in both themes.
flowchart LR
N["Sidebar: Usage (new)"] --> P["/usage?range=1h|6h|24h|7d"]
P --> S["GET /v1/stats?since=range"]
S --> V["Tiles + tables (new)"]
S -->|partial| Q["Figures cover from today 09:12."]
O["Overview: Granted tile (changed)"] -->|link| P
What the operator sees, for /usage?range=24h after a scripted run:
Usage [1 hour] [6 hours] (24 hours) [7 days]
Figures cover from today 09:12.
Requests 214 Granted 209 Rejected 5 Wait p95 38 s Held p50 4 min 2 s (each: last 24 hours)
Grants by source warm 160 · booted 41 · provisioned 8
Durations Count p50 p95 Max
Wait 209 0 s 38 s 2 min 10 s
Held ...
Turnaround / Provisioning / Boot ...
Rejections by reason no-wait 3 · timeout 2
Use over the range Slots: peak 8, mean 5.2, of 8 · RAM: peak 12.0 GiB, mean 900 MiB, of 16.0 GiB · Queue: peak 3, mean 0.4
Device incidents Quarantined 1 · Recovered from quarantine 1 · Recovered after a crash 0 · Lost 0
Per platform Platform, Requests, Granted, Rejected, Wait p95 (iOS, then Android)
Per worker Worker, Requests, Granted, Rejected, Wait p95; one row per worker; on a single host one row, the host
Per requester Requester (label, then id), Requests, Granted, Rejected, Held in total
| Ask | Today | After |
|---|---|---|
/usage |
page not found | figures for the last hour |
/usage?range=7d |
page not found | figures for the last 7 days |
/usage?range=30d |
page not found | as /usage |
| A requester with no token label | — | its id alone |
| No requests in the range | — | tiles 0, durations and means —, the requester table says No requests in this range. |
Overview on ?range=7d, click Granted |
no link (#633) | opens /usage?range=7d (/usage from 1h) |
A requester gw:<gateway>:agent-7 in a worker's answer |
— | shown as agent-7 |
/usage?range=24h&requesters-page=3, then choose 7 days |
— | /usage?range=7d, first page of each table |
If this goes wrong, the operator sees figures that differ from simlock stats for the same range, or a page that scrolls sideways on a phone.
Technical spec
Facts
| # | Fact | Source |
|---|---|---|
| F1 | Pages are the entries of ROUTES, in sidebar order; a test pins that order. |
ui/src/app/routes.ts:18-25, ui/src/app/routes.test.ts:18-27 |
| F2 | A route's icon is a key of PATHS; there is no usage icon. |
ui/src/ds/icon.tsx:1-16 |
| F3 | The browser lane lists the pages in NAV; the every-view tests walk it: no sideways scroll at 360 px, keyboard alone, no CSP violation, sidebar order, and the axe WCAG AA check in light and dark. |
e2e/console/fixtures.ts:321-330; e2e/console/views.spec.ts:47, :77-93, :95-133, :208-210, :244-256; e2e/console/long-lists.spec.ts:776-800, :834-855 |
| F4 | A table of a page's items is DataTable, which needs a Paging; a page with two tables keeps each one's page under its own name. |
ui/src/ds/table.tsx:28-37, ui/src/state/paging.ts:16-23, docs/CONSOLE.md:42-45 |
| F5 | A holder is shown by its token's label, or its id where it has none. | ui/src/model/leases.ts:57-59 |
| F6 | Keys the daemon spells (codes, payload keys) are shown as spelled, in mono. | docs/internal/DESIGN.md:162-163 |
| F7 | Under 768 px a table's rows stack into blocks with data-label; a table of six columns or more stacks under 1100 px. |
docs/internal/DESIGN.md:228-234, ui/src/ds/table.tsx:78 |
| F8 | Stat tiles are a StatRow of StatTiles, each a label, a value and a unit. |
ui/src/ds/stat.tsx:7-54 |
| F9 | A refused read shows its message with role="alert"; while loading, Loading…. |
ui/src/ds/loaded.tsx:16-35 |
| F10 | The sidebar's pages are listed in the user manual. | docs/CONSOLE.md:13-14 |
| F11 | #631's output keeps PR #356's figures. Per scope: requests, granted, bySource { warm, booted, provisioned, unknown? }, rejected { total, byReason } (refused-at-admission included), wait, held, turnaround, provisioning, boot (each { count, p50, p95, max }, ms, null over none), incidents { quarantined, crashRecovered, quarantineRecovered, lost }, utilisation { slots { peak, mean, max }, ram? { peakBytes, meanBytes, limitBytes } }, queue { peakDepth, meanDepth }, means time-weighted and null with no rows, queue and RAM from the scope's all rows; also declined, probes? (worker only) and failures.byEvent (device.recovery-failed only), which this page does not show. requesters[] { id, label?, requests, granted, rejected, heldTotalMs }, by requests then id. workers[] the figures plus { id, label? }: on a worker one entry, itself; on a gateway every registry worker and every worker id the rows name. |
#631 "Contract and event changes" (Counts, Incidents, Minute figures, Scopes, Requesters), F17, F21; gh pr diff 356 src/contract/schemas.ts usageFiguresSchema, usageOutputSchema |
| F12 | A gateway in the browser lane takes a worker through its join token and gateway.url. |
e2e/console/waiting.spec.ts:115-131 |
| F13 | The Overview's Granted tile has no link after #633; the tile row links through to. |
#633; ui/src/model/overview.ts:15-20 |
| F14 | formatBytes prints MiB below 1 GiB (900 MiB) and GiB with one decimal from there (20.9 GiB). |
ui/src/model/load.ts:13-18 |
| F15 | agentIdOf shows the agent behind a gw:<instance>: requester, as the Leases page does. |
ui/src/model/leases.ts:47-55 |
| F16 | Page keys are <name>-page and <name>-size; a pager is hidden when every row fits on one page. |
ui/src/model/paging.ts:10-14; docs/internal/DESIGN.md:179-186 |
The output shape, the route and the ranges are feature #623 Shared values ("usage.get", "GET /v1/stats", "Console ranges"). The range picker, the Wait p95 tile and the partial note are #633's. Means are time-weighted by the daemon (Shared values, "usage.get"); the page prints them as given.
Modules touched
Searches: ROUTES, NAV, IconName, DataTable, usePaging, and #633's range and figures modules.
ui/src/model/usage.ts(new): one stats answer to the page's view model: tiles, grants by source, durations, rejections, incidents, use over the range, the platform, worker and requester rows.ui/src/state/usage.ts(new): reads Overview and Waiting: a range picker, wait p95, leases granted and three charts #633's range and its stats path withuseLiveResource. The worker and requester tables page in the URL (workers-page/workers-size,requesters-page/requesters-size, F16); the other tables (grants by source, durations, rejections, incidents, platforms) take one constantPagingthat holds every row on one page, so no pager shows. A range change dropsworkers-pageandrequesters-pageand keeps the sizes.ui/src/model/overview.ts: the Granted tile links to/usagewith the Overview's?range=(none for1h).ui/src/screens/usage/index.tsx,usage.css(new): the page.ui/src/app/routes.ts: aUsageentry at/usage, after Events.ui/src/ds/icon.tsx: ausageicon.e2e/console/fixtures.ts:UsageinNAV.e2e/console/usage.spec.ts(new).docs/CONSOLE.md,ui/AGENTS.md(Pages table).
flowchart LR
RT[app/routes*] --> SU[screens/usage*]
SU --> STU[state/usage*]
SU --> DS[ds: StatRow, Panel, DataTable, ChoiceChips]
STU --> R[state/range, #633]
STU --> L[state/live]
STU --> PG[state/paging]
STU --> MU[model/usage*]
MU --> F[model/figures, #633]
Contract and event changes
None. The page reads #632's route, typed from #631's output schema. The only input it takes is ?range= (#633's rule) and the four page keys (F4's rule).
Other code on the same state
Searches: setSearch writers (3: state/paging.ts, state/events.ts, #633's range), watch( (1).
- The range and the two page keys share the query; each writer keeps the others' keys (F4, Overview and Waiting: a range picker, wait p95, leases granted and three charts #633).
- The Overview (Overview and Waiting: a range picker, wait p95, leases granted and three charts #633) watches the same
/v1/stats?since=<range>path; with both on screen there is one resource and at most one request in flight (connection.ts:173-177).
Failure modes
| Call | Outcome | What happens | Limit, counted from |
|---|---|---|---|
GET /v1/stats |
ok | The page draws it | — |
GET /v1/stats |
error reply (403, 5xx) | Before any answer, the refusal in the page's place; after one, the last figures stay with the refusal above them (F9) | — |
GET /v1/stats |
no answer | The console is disconnected, as for any route | 10 s from the request |
GET /v1/stats |
requesters empty, or workers empty on a gateway with none |
The table says No requests in this range. or No workers in this range. |
— |
GET /v1/stats |
utilisation.ram absent |
The RAM line is left out | — |
State written: the URL's range and page keys only. A restart in the middle leaves nothing.
Rules in play
- ADR 0013 §1, "keeps no state the daemon does not report": every figure is the answer's, and the page sums, averages or re-buckets nothing.
- ADR 0013 §3: the route is fetched on events and every second, coalesced, like every route.
- ADR 0016 §5, "every surface shows that beside the figures": the partial note sits under the picker.
- Architecture rule 10: the Wait p95 tile and the partial note are Overview and Waiting: a range picker, wait p95, leases granted and three charts #633's functions, not copies.
ui/AGENTS.mdlayers: the builder inmodel/, the read instate/, the screen draws only.- DESIGN.md: numbers mono and right-aligned; tables through
DataTable; codes as spelled (F6); 360 px, both themes, AA contrast. - Testing rule 7: phone width, keyboard, CSP, axe and sidebar order come from adding
UsagetoNAV(F3), not from new tests.
Tests
Seam: ui/src/model/usage.ts unit tests and a screen test with renderToStaticMarkup, modelled on ui/src/screens/overview/overview.test.tsx; the browser lane modelled on e2e/console/views.spec.ts.
- the tiles show Requests, Granted, Rejected, Wait p95 and Held p50 from
totals, each with the range's unit, with a fixture where every figure differs - grants by source list warm, booted and provisioned, and
unknownonly when the answer has it - the durations table has rows Wait, Held, Turnaround, Provisioning and Boot, each with its own count, p50, p95 and max as durations, and
—for anullfigure - rejections list one row per reason as the daemon spells it, largest count first and ties by reason, and say
No requests were rejected.when there are none - incidents list Quarantined, Recovered from quarantine, Recovered after a crash and Lost from their four fields
- use over the range shows
totals.utilisation.slotspeak, mean (one decimal) and max,totals.queuepeakDepth and meanDepth, andtotals.utilisation.rampeakBytes, meanBytes and limitBytes throughformatBytes(900 MiBbelow 1 GiB,12.0 GiBabove) only when it is present,—for anullfigure, with a fixture where every figure differs - the page shows no
declined,probesorfailures.byEventcount, with a fixture that has them set - the platform table has the columns Platform, Requests, Granted, Rejected and Wait p95, with iOS then Android
- the worker table has the columns Worker, Requests, Granted, Rejected and Wait p95; a row is named by its label, or its id where it has none, in the daemon's order; a single host's one entry is one row; an empty
workerssaysNo workers in this range. - a requester row shows its label with its id beside it, its id alone where it has no label, the agent alone for a
gw:<instance>:<agent>id (agentIdOf), and its held time in total as a duration, in the daemon's order - an answer with no requests shows tiles of
0,—for every percentile and mean, andNo requests in this range.in the requester table - the Usage screen draws the picker with the chosen range checked, the tiles, and each table as a region named by its panel title
- the Usage screen shows the partial note under the picker when the figures carry one, and none otherwise
- a refused stats read shows its message in place of the figures; while loading the page shows
Loading… - extend "lists the sections in the sidebar's order" in
ui/src/app/routes.test.tswith/usagelast, androuteFor("/usage")is Usage - the Overview's Granted tile links to
/usagefrom1hand to/usage?range=6h,24h,7dfrom the others (extend Overview and Waiting: a range picker, wait p95, leases granted and three charts #633's Granted case inui/src/model/overview.test.ts) - choosing another range drops
workers-pageandrequesters-pagefrom the URL and keeps the newrangeand both size keys (a model test of the URL writer) - (console) after a scripted run on the fake driver,
/usage?range=24hshows the same requests, grants by source, rejections by reason, wait p95, slots peak and mean, and requester rows asGET /v1/stats?since=24handsimlock stats --since 24h --json, and its per-worker table has one row, the host - (console)
/usageopens on 1 hour; choosing 7 days writes?range=7d, and a reload keeps 7 days checked and the same figures - (console) a lease granted while the page is open raises Requests and Granted within 2 seconds with no reload
- (console) clicking the Overview's Granted tile on
/?range=7dopens/usage?range=7d - (console) on a gateway with two workers, the per-worker table has two rows, each named by its worker's label
- (console)
UsageinNAVputs/usageunder the existing every-view tests: 360 px with no sideways scroll, keyboard alone, no CSP violation, sidebar order, and the axe check in light and dark (F3)
Done when
- On the fake driver after a scripted run, the Usage page for 24 hours shows the same figures as
simlock stats --since 24h --jsonandGET /v1/stats?since=24h. - The picker switches between 1 hour, 6 hours, 24 hours and 7 days, and the URL keeps the choice.
- The page has no sideways scroll at 360 px and passes the axe check in both themes.
- On a gateway the per-worker table has one row per worker; on a single host, one row.
docs/CONSOLE.mdlists Usage in the sidebar and describes its figures and the partial note;ui/AGENTS.mdhas its row in Pages.
Out of scope
- Charts on the Usage page; the three charts are the Overview's (Overview and Waiting: a range picker, wait p95, leases granted and three charts #633).
- The CSV ledger (
&ledger=1,GET /v1/statsand thesimlock stats --csvledger #632). - Explicit start and end dates.
Depends on
Written by an agent.
Activity
test(console): Usage tests pin queue mean rounding, sub-second boot a…
fix(console): Usage order tests can fail, one stats-state mapping, do…
test(console): the workers-order fixture differs from requests, name …
fix(history): history.db and its -wal/-shm files are created 0600 (#634)
test(console): the empty-overview title claims only what its assertio…
test(core): a non-stall hand-over to quarantine writes the delete-fai…
test(leasing): a waiter whose grant failed is queued as waiting-for-d…
feat(history): history.db, usage projections, simlock stats, GET /v1/…