Repository navigation
Overview and Waiting: a range picker, wait p95, leases granted and three charts #633
Description
Part of #623.
Scope
After this PR, the Overview has a range picker (1 hour, 6 hours, 24 hours, 7 days). For the chosen range it shows how long requests waited (p95) and how many of the requests made in it were granted, and three charts: leases held, requests in the queue, and device slots in use. The figures come from the daemon (GET /v1/stats, #632), so a reload or a daemon restart shows the same numbers. The Waiting page shows the wait p95 of the last hour.
In short
Today the Overview counts its one chart and its "Granted, last hour" tile from the events the page holds: the last hour the feed loads, and the stream after (F1, F2, F23). Nothing reaches past an hour, and nothing whose events are gone counts. After this PR both come from GET /v1/stats?since=<range>. The range is in the URL as ?range= (feature #623 Shared values, "Console ranges"); the stats are fetched like every other route (F6). The Capacity panel (the current snapshot) stays as it is.
flowchart LR
P["Range picker<br/>?range=1h|6h|24h|7d"] --> S["GET /v1/stats?since=range (new read)"]
S --> T["Tiles: Wait p95, Granted (changed)"]
S --> C["Charts: leases held, queue, slots (changed)"]
W["/v1/workers"] --> K[Capacity panel, as today]
S1["GET /v1/stats?since=1h"] --> WP["Waiting page: Wait p95 tile (new)"]
- Bucket: one point of a chart, as wide as the answer's
bucketMs(feature Operational metrics kept over hours and shown in the console #623 Shared values, "Bucket width"): 1 minute for 1 hour, 5 minutes for 6 hours, 15 minutes for 24 hours, 1 hour for 7 days. Each point is the peak in its bucket;nullis no data and draws as a gap, not 0. A point'satis its bucket's end, and the last one's isto, so it may be short (usage.getandsimlock statsfrom the projections #631). Its label names the bucket's start,window.from + i × bucketMsfor pointi: that isat − bucketMsfor every full bucket, as today's minute labels name a minute's start (F3). - Wait p95:
totals.wait.p95of the answer. Below 1 second it shows in milliseconds (400 ms); from 1 second in the format the console uses for every duration (38 s,2 min 10 s, F15);—whennull(no request ended its wait in the range). - Granted:
totals.granted: the requests made in the range that were granted by its end (usage.getandsimlock statsfrom the projections #631, "Counts"), not thelease.grantedevents stamped in it.docs/CONSOLE.mdsays so. - Where a stats refusal goes: the fleet tiles (Utilisation, Leases, Waiting, Workers) stay. In the tile row, one item after Workers takes the place of Granted and Wait p95 and holds
Loading…or the refusal (F19). The chart row holds one panel with the same note in place of the three charts.
What the operator sees:
| Ask | Today | After |
|---|---|---|
/ |
tile Granted 1 last hour, counted from events held, a link to Events |
tile Granted 1 last hour from the daemon, no link; then tile Wait p95 38 s last hour |
/?range=7d |
as / |
tiles and charts for the last 7 days, hourly points |
/?range=2d or /?range= |
as / |
as / (1 hour) |
| Reload, or daemon stop and start | chart and Granted rebuilt from the events the feed loads; a pruned event no longer counts | same numbers, events pruned or not |
| Range older than the kept figures | — | Figures cover from <day> <HH:MM>. above the tiles, <day> as today, yesterday or 16 Oct |
/waiting, /waiting?range=7d |
no wait figure | tile Wait p95 38 s last hour above the list; no picker, always the last hour |
If this goes wrong, the operator sees figures that change on reload, a gap drawn as zero, or a refused stats read that hides the whole fleet.
Technical spec
Facts
| # | Fact | Source |
|---|---|---|
| F1 | The Granted tile counts lease.granted events the page holds from the last hour; its unit is last hour and it links to /events. |
ui/src/model/overview.ts:146-148, :165 |
| F2 | The Overview chart is leaseHistory: leases held now, counted back minute by minute through the lease events held, never below 0. |
ui/src/model/overview.ts:179, ui/src/model/history.ts:40-56 |
| F3 | A chart has 60 points, one per minute, labelled HH:MM. |
ui/src/model/history.ts:5, :17-30 |
| F4 | MinuteChart and MinuteTable take { at, label, count } with a number count; the table's disclosure is Minute by minute. |
ui/src/ds/chart.tsx:19-23, :28-35, :92-99 |
| F5 | The Events page draws its events-per-minute chart with the same two components; it stays counted from events. | ui/src/screens/events/index.tsx:117-128, ui/src/model/event-list.ts:188 |
| F6 | The live connection refetches every watched path every second and on every stream event, one request per path in flight; a path is fetched at once when first watched, and when the tab is shown again (ADR 0013 §3). | ui/src/data/live/connection.ts:5, :252-261, :360-369, :100-108, :409-426 |
| F7 | useLiveResource keys a resource by its whole path, so /v1/stats?since=1h and ?since=7d are two resources. |
ui/src/state/live.tsx:61-69 |
| F8 | An API path is any /v1/${string}; /v1/stats?since=7d passes. |
ui/src/data/api.ts:4, :41 |
| F9 | The duration parser accepts 1h, 6h, 24h and 7d. |
src/contract/duration.ts:16 |
| F10 | The Overview joins its reads with bothViews, so one refused read hides the whole page; a 501 on /v1/lease-requests is the one exception. |
ui/src/state/overview.ts:21-28, ui/src/state/resource.ts:40-45 |
| F11 | A URL-backed choice reads an unknown value as the default, leaves the default out of the URL, keeps other keys, and is written with replace. |
ui/src/model/event-list.ts:33-46, ui/src/state/events.ts:98-100 |
| F12 | ChoiceChips is a radio group with a hidden legend. |
ui/src/ds/choice.tsx:12-37 |
| F13 | The Overview has one chart panel Leases held, last hour and a Capacity panel beside it. |
ui/src/screens/overview/index.tsx:48-67 |
| F14 | The Waiting page reads /v1/lease-requests and /v1/workers only; its list pane has a title, a meta line and a footer. |
ui/src/state/waiting.ts:28-34, ui/src/screens/waiting/index.tsx:40-48 |
| F15 | Durations format as 38 s, 2 min 10 s. A missing figure shows —. |
ui/src/model/time.ts:7-25; ui/src/screens/overview/index.tsx:110, ui/src/model/waiting.ts:113 |
| F16 | A day is named today, yesterday or 16 Oct. |
ui/src/model/event-list.ts:63-69 |
| F17 | Browser tests pin today's tile text Granted | 1 | last hour and the chart title Leases held, last hour. |
e2e/console/views.spec.ts:293-299, :165-166, :135-142 |
| F18 | The browser lane can stop and start a daemon on the same home. | e2e/console/fixtures.ts:41-44, :104-111 |
| F19 | A refused read shows its message with role="alert"; while loading, Loading…. |
ui/src/ds/loaded.tsx:16-35 |
| F20 | The design guide says a chart's table lists every minute and has 60 rows; a tile never makes a request of its own. | docs/internal/DESIGN.md:177-178, :207-214, :157-159 |
| F21 | Docs describe the event-counted chart, the 60-minute table and the "last hour" Granted tile; the manual lists "No history beyond the last hour". | docs/CONSOLE.md:39-40, :140-150; ui/AGENTS.md:45, :81-82 |
| F22 | The console reads contract types type-only from src/contract/schemas.ts. |
ui/src/model/waiting.ts:3 |
| F23 | Each screen's event feed loads the last hour from GET /v1/events, then follows the stream. |
ui/src/state/events.ts:33-42 |
| F24 | MinuteCount is defined in history.ts and used by the Events page model. |
ui/src/model/history.ts:7-11, ui/src/model/event-list.ts:11, :181 |
| F25 | A daemon with eventLog.retention of a few seconds prunes the events of a first run at its next start; projection rows keep metrics.retention (default the larger of 90 days and eventLog.retention). |
#629 Tests (e2e retention line); feature #623 Shared values, "Config keys", "Prune" |
The answer's shape, GET /v1/stats and the ranges are feature #623 Shared values ("usage.get", "GET /v1/stats", "Bucket width", "Console ranges"); the field names are #631's (totals.granted, totals.wait.p95, series[], coversFrom, partial, bucketMs).
- The charts draw complete minutes only (
usage.getandsimlock statsfrom the projections #631: the running minute is left out), so a lease taken now shows in a chart once its minute ends; the Granted tile is the live figure (decided in delivery, pending the maintainer).
Modules touched
Searches: leaseHistory, MinuteChart, MinuteTable, MinuteCount, HISTORY_MINUTES, Granted, last hour, useLiveResource, bothViews.
ui/src/model/range.ts(new): the four ranges, their labels (1 hour… for chips,last hour,last 6 hours,last 24 hours,last 7 daysfor units), reading and writing?range=as F11, and the stats path of each range (/v1/stats?since=1h,6h,24h,7d), so the state hooks only pass it on.ui/src/model/figures.ts(new): one place for what both pages show from a stats answer: the Wait p95 tile and its millisecond format, the Granted tile, the partial note, and the three chart series.ui/src/model/history.ts:leaseHistorygoes; a bucket label bybucketMsand window is added;MinuteCount.countmay benull;eventsPerMinuteandextremesstay (F5).extremesskipsnullpoints.ui/src/model/event-list.ts: follows the nullablecount(F24); its figures stay numbers.ui/src/model/overview.ts: the event-counted Granted tile and chart go; Granted (no link) then Wait p95 follow the Workers tile; the view model takes the figures separately from the fleet.ui/src/state/range.ts(new): the range hook overuseSearchandsetSearch.ui/src/state/overview.ts: reads/v1/stats?since=<range>withuseLiveResourceas its own view, not joined withbothViews(F10).ui/src/state/waiting.ts: reads/v1/stats?since=1hthe same way.ui/src/ds/chart.tsx,ui/src/ds/chart.css: points may benull; the table's disclosure is named by the bucket width.ui/src/screens/overview/index.tsx,overview.css: the picker, the note, the tiles, three chart panels, Capacity as today.ui/src/screens/waiting/index.tsx,waiting.css: the Wait p95 tile above the list.ui/src/screens/events/index.tsx: follows the chart component's props, nothing else.e2e/console/views.spec.ts: the tile and chart-title tests (F17).e2e/console/overview.spec.ts(new).docs/CONSOLE.md,docs/internal/DESIGN.md,ui/AGENTS.md.
flowchart LR
SO[screens/overview*] --> STO[state/overview*]
SW[screens/waiting*] --> STW[state/waiting*]
STO --> R[state/range*]
STW --> R
STO --> L[state/live]
STW --> L
STO --> F[model/figures*]
STW --> F
F --> H[model/history*]
F --> MR[model/range*]
SO --> DS[ds/chart*]
SE[screens/events*] --> DS
sequenceDiagram
participant B as Browser (Overview)
participant D as Daemon
B->>D: GET /v1/stats?since=1h (operator token)
D-->>B: 200 usage.get answer
Note over B: tiles and charts drawn
B->>D: again on the next event or 1-second poll (coalesced)
D-->>B: 4xx/5xx { error }
Note over B: figures keep the last answer, the refusal shows above them; fleet tiles and panels unaffected
Contract and event changes
None. The console reads #632's GET /v1/stats; its answer is typed from #631's usage.get output schema (F22). The one new input is ?range= in the page URL: anything but 1h, 6h, 24h, 7d reads as 1h (F11).
Other code on the same state
Searches: location.search writers (setSearch: 3 hits, state/paging.ts, state/events.ts), resources keyed by path (watch(: 1 hit).
- Paging (
state/paging.ts) writespageandsizeinto the same query. Writingrangekeeps every other key, and paging keepsrange(F11): the last write wins only for its own key. LiveConnection(F6) fetches the stats path as it fetches every route; at most one request per path is in flight, so a burst of events costs one more read, not one each.- Console Usage page #634 reads the same range and the same stats path. When both watch the same path, there is one resource and one fetch (F7). The Waiting page and the Overview at
1hshare/v1/stats?since=1hthe same way.
Failure modes
| Call | Outcome | What happens | Limit, counted from |
|---|---|---|---|
GET /v1/stats |
ok | Tiles, note and charts drawn from it | — |
GET /v1/stats |
error reply (403, 422, 503, 5xx) | Before any answer: the tile-row item and the chart-row panel show the refusal; after one: the last figures stay with the refusal above them (F19). The four fleet tiles, Workers, Capacity, Attention, Activity and the Waiting list are unaffected. | — |
GET /v1/stats |
body not JSON | As an error reply (connection.ts:63-65) |
— |
GET /v1/stats |
no answer, or network error | The console is disconnected, as for any route (ADR 0013 §5) | 10 s from the request (api.ts:1) |
GET /v1/stats |
answer for an older range arrives after the range changed | Not shown: each range is its own resource (F7) | — |
State written: the URL's range key only. A restart in the middle leaves nothing. Two tabs each keep their own URL.
Rules in play
- ADR 0013 §3, "Any event refetches the routes the current screen reads": the stats path is fetched like every route, with no cadence of its own (feature Operational metrics kept over hours and shown in the console #623 Shared values, "Console ranges").
- ADR 0016 §8, "a client never re-buckets": the charts draw the answer's points as they come.
- ADR 0032 §5, "A point is the peak of its bucket": each chart point is the series value, not a sum or a last value.
- Architecture rule 10, one place: the Wait p95 tile and the range rule are built once (
model/figures.ts,model/range.ts) and used by both pages. ui/AGENTS.mdlayers: rules inmodel/, reads instate/, screens draw only.- DESIGN.md charts (F20): each chart keeps a title, a summary line and its table, now one row per bucket; animation off; colour by class. The tile rule holds: the page reads the stats, the tile only shows them.
- Testing rule 7: the tile and chart-title browser tests in
views.spec.tsare extended, not copied.
Tests
Seam: ui/src/model unit tests and screen tests with renderToStaticMarkup, modelled on ui/src/screens/overview/overview.test.tsx; the browser lane modelled on e2e/console/views.spec.ts.
- the range reads
1h,6h,24hand7dfrom?range=, reads a missing, empty or unknown value (2d) as1h, and writing1hdrops the key while keepingpageandsize - each range maps to its stats path:
1h→/v1/stats?since=1h,6h→?since=6h,24h→?since=24h,7d→?since=7d - the Wait p95 tile shows
totals.wait.p95as0 ms,400 ms,999 ms,1 s,38 s,2 min 10 sfor 0, 400, 999, 1 000, 38 000 and 130 000 ms, and—when it isnull, with the range's unit (last hour,last 6 hours,last 24 hours,last 7 days), and comes right after Granted - the Granted tile shows
totals.grantedof the answer, not thelease.grantedevents held, and has no link (extend "counts the fleet's headline figures…" inui/src/model/overview.test.ts: three granted in the answer, one event held, shows 3,toabsent) - the three charts take
leasesHeld,queueDepthandslotsUsedfrom each series point in order, with a fixture where the three differ, and anullpoint staysnull - a bucket's label is its start (
at − bucketMs, and for a short last point the previous point'sat) asHH:MMfor 1-minute, 5-minute, 15-minute and 1-hour buckets in a window of one day or less, and carries the day (today 14:00,yesterday 14:00) in a 7-day window - each chart's summary line says now, highest and lowest, skips
nullpoints, and is empty when every point isnull - the partial note reads
Figures cover from <day> <HH:MM>.fromcoversFromwhenpartialis true, and is absent when it is false - the Waiting page's Wait p95 tile shows the same text as the Overview's for the same 1-hour answer, with the unit
last hour - the chart table lists every bucket newest first, shows
—for anullpoint, and names its disclosure by the bucket width (Minute by minute,Every 5 minutes,Every 15 minutes,Hour by hour) (extendui/src/ds/chart.test.tsx) - the Overview draws the range picker as a radio group of four with the chosen range checked, three chart panels titled
Leases held, <unit>,Requests in the queue, <unit>andDevice slots in use, <unit>, and the Capacity panel - the Overview shows the partial note above the tiles when the figures carry one, and no note otherwise
- a refused stats read shows its message as one item after Workers in the tile row and as one panel in the chart row, while the Utilisation, Leases, Waiting and Workers tiles, the workers table, Capacity, Attention and Activity still render; a stats read still loading shows
Loading…in the same two places - the Waiting page shows the Wait p95 tile above its list with no range picker, and a refused stats read leaves the list as it was
- the Events page chart is unchanged: the existing
eventsCharttests inui/src/model/event-list.test.tspass unedited - (console) extend "the overview's tiles show the numbers the daemon gives": the tiles end
Granted | <totals.granted> | last hour,Wait p95 | <totals.wait.p95> | last hourfromGET /v1/stats?since=1h - (console) extend the chart keyboard and outline tests to the new chart titles, each waiting until the three charts have drawn before it counts the stops (F17)
- (console) after a scripted run on the fake driver with one device and a second request that waits, picking each of 1 hour, 6 hours, 24 hours and 7 days writes its
?range=, and the two tiles and all three chart tables equalGET /v1/stats?since=<range> - (console) on a daemon with
eventLog.retentionof a few seconds (F25), after a scripted run,simlock daemon stop, a wait past the retention andstart,/?range=24hshows the same Wait p95, Granted and chart table rows (for the buckets before the stop) as before the stop, withGET /v1/eventsempty - (console) a reload of
/?range=6hon that daemon keeps 6 hours checked and the same tile numbers - (console) a lease granted while the Overview is open raises Granted within 2 seconds with no reload
- (console) the Waiting page's Wait p95 equals
GET /v1/stats?since=1htotals.wait.p95, also when opened as/waiting?range=7d - (e2e) after a scripted run that spans a completed minute, each chart table's non-null rows equal the matching series of
GET /v1/stats, and the same rows show after a daemon stop and start.
Done when
- On the fake driver after a scripted run, the Overview shows Wait p95 and Granted, and three charts, for each of 1h, 6h, 24h and 7d, matching
GET /v1/stats?since=<range>. - A reload and a daemon stop and start show the same numbers for the same range, also after the events behind them were pruned.
- The Waiting page shows Wait p95 for the last hour.
- A refused
/v1/statshides only the tiles and charts it feeds. docs/CONSOLE.mddescribes the picker, the tiles (Granted: requests made in the range that were granted), the three charts and the partial note;docs/internal/DESIGN.mdsays a chart's table has one row per bucket;ui/AGENTS.mddrops "No history beyond the last hour" for the Overview (the Events chart still counts from events) and updates its Pages row.
Out of scope
- The Usage page (Console Usage page #634).
- Explicit start and end dates.
- The Events page chart: still the last hour, counted from events.
- Waiting reasons (A waiting request says why it is not placed, on a worker #636, A waiting request says why each worker cannot take it, on a gateway #637).
Depends on
Written by an agent.
Activity
test(ui): the overview keeps its last figures under a later stats ref…
fix(console): a floored 24h window labels HH:MM; Now — summary tested…
feat(history): history.db, usage projections, simlock stats, GET /v1/…