Repository navigation
feat(webui): make Activity read as a status page - #1929
Conversation
Activity was prose and repeated "N/A" lines, 1,548 px tall on the 1440x900 mock, and idle slots claimed an unknown denominator because the bar was gated on prompt_tokens, which the server leaves null for a slot without a task. - Header: the model picker (ready models first, verbatim display names) sits beside Refresh and Export, and the last snapshot time moves into the description. The page-wide history toggle and its lazy chunk are gone. - Tiles: always four StatCards (active, completed, completion tokens, queued). Null reads "unknown" with a localized reason hint; a measured tile's hint is its observation time. Loading tiles show decorative skeletons under one localized status until the first sample. - Slots: a DataTable in a named, focusable scroll region. An idle slot with a known context reads "0 / D tokens"; unknown appears only without a denominator or for a processing slot without a count. - Disclosure: an unavailable-count badge in the summary replaces the old sentence, and its content (every measurement, raw reason, history list) is built only while open. - Operations: one line per row with catalog kind and state labels, relative age, and identifiers only inside Operation details. - Copy: units, scopes and the known server reasons come from the catalog; a vitest reads src/server/webui/runtime.rs so server wording drift fails a test. The StatCard adapter now forwards loading and sparkline. Typecheck, lint, unit and the Chromium specs pass; the page now ends at 789 px. Refs #1916
Output of python3 scripts/webui/build_bundle.py for the previous commit; --verify reproduces the same digest. The history chunk is gone because Activity no longer lazy-loads it. Refs #1916
The active-request sparkline anchored its five-minute window to the last sample that had a measurement. activePoints drops samples whose active_requests is null, so when the newest runtime samples carry no measurement (for example after the model is unloaded and the server reports "model provider is not loaded"), the older dots slid to the right edge and read as current. The chart before this redesign anchored to the latest runtime sample, valid or not.
Sparkline now takes an explicit end, the receive time of the latest runtime sample, which ActivityPage already computes as latestRuntimeAt and passes through RuntimeView as historyEnd. The sparkline renders only when there are two points and that end is known.
A new runtime.test.tsx case builds 150 samples 2 s apart whose last 30 are missing. Before the fix the last dot sat at x=117 of a 120-wide plot ("expected 117 to be less than 102"); after it the dot sits near x=94 and the case passes. typecheck, lint, check:theme-selectors and the unit suite (563 tests in 56 files) pass.
Refs #1916
Rebuild src/webui/assets with scripts/webui/build_bundle.py so the embedded WebUI carries the Activity sparkline fix that anchors the chart to the latest runtime sample; the index and code-highlight chunks change hash and the manifest and index.html follow them.
A failed operation's banner printed the raw ErrorCode enum word (for example stale_revision) as page copy in both locales. The epic #1910 contract keeps enum names and raw identifiers out of user copy except inside an explicit details disclosure, and this was the last enum still on screen. The banner now reads a localized reason per ErrorCode through an exhaustive Record<ErrorCode, StringKey>, falls back to a generic reason for a code newer than the build, and keeps the no-code message for a null error. The raw code moves into the row's Operation details disclosure. The server's error message is still never rendered. New activity.error.* and activity.details.error_code strings are mirrored in the strings fixture, and docs/webui/activity.md states the rule. Before the fix, the rewritten Activity test failed in both locales because the collapsed text ended in "stale_revision" instead of the localized reason; after it, all 15 Activity tests pass. typecheck, lint, unit (565 tests) and check:theme-selectors pass. Refs #1916
Rebuild src/webui/assets with scripts/webui/build_bundle.py so the embedded WebUI carries the localized failed-operation error reasons on Activity; the entry and code-highlight chunks were renamed by their new content hashes and the manifest and index.html follow them.
Implementation Review SummaryIntent
Findings Addressed
Remaining Items
Verification
|
Security and performance review (b2a474d)No CRITICAL or HIGH findings, so no fix commits. Security
PerformanceNo timers, intervals, fetches or document listeners were added. Catalog options, the name index and history points are memoized, the slot columns are memoized, and the Intl formatters are cached. Temporary CDP probe on the mock, per runtime update, baseline 835a552 vs this branch:
The CI Activity performance harness starts the server without Remaining (LOW, not fixed)
Gatestypecheck, lint, unit (565 Vitest, 35 node:test), |
The measurement disclosure indexed SCOPE_KEYS[metric.scope] directly, so a scope value outside the current closed union (schema validation keeps it closed today, but a future server scope would arrive before the schema and this code catch up) rendered the literal word "undefined" instead of localized copy. Add a scopeKey() helper with a runtime fallback to activity.scope.unknown and cover it with a unit test that casts scope to an unrecognized value. Also add the two occupancy-rule and context-rule test cases the review found untested: a zero (not just null) request_context_tokens now has a dedicated regression test, verified to fail without the `<= 0` guard in slots.tsx. Validation: - pnpm --dir webui run typecheck, lint, unit (567 Vitest, 35 node:test), check:theme-selectors: all pass. - python3 scripts/webui/build_bundle.py && --verify: pass, bundle regenerated for the runtime.tsx change. - scripts/ci/check_webui_contract.py and check_binary_assets.py: pass. - Playwright chromium on port 4916: tests/activity.spec.ts (8/8), tests/ui-common-components.spec.ts (14/14). Refs #1916
CI's Linux runner failed the 390 px keyboard and layout spec: the slot table scrolled sideways by 31 px because the auto table layout sized columns from their widest words, and the Linux fallback sans-serif is wider than the macOS system face. On macOS the table had 3 px to spare. The slot table now uses a fixed layout at 100 percent width with per-column shares, so its width never follows the text. A container query below 48rem gives the state badge more room, tightens the inline cell padding, uses a smaller head font and a compact badge. Header copy is shorter (Context, Decoded, Cached; Korean in step, fixture in sync). The CSS is scoped to the Activity slot table. Measured at 390 px with four idle slots, forcing Verdana through CSSOM for the wide case: overflow is 0 in en and ko with both fonts, and the unbroken min-content table needs 275 px (en default), 301 px (en Verdana) and about 176 px (ko) of the 358 px column. activity, shell and ui-common-components specs, typecheck, lint, unit and the theme gate pass. Refs #1916
Output of python3 scripts/webui/build_bundle.py for the slot table fix; --verify reproduces digest 0507ec7a. Refs #1916
…tivity-status-page # Conflicts: # docs/webui/ui-common.md # src/webui/assets/assets/code-highlight-CVUuwqcC.js # src/webui/assets/assets/code-highlight-Dk2RxJiZ.js # src/webui/assets/assets/code-highlight-RVRncNAw.js # src/webui/assets/assets/history-yShpES-e.js # src/webui/assets/assets/index-SCLyOIB0.js # src/webui/assets/assets/index-uBcwUASe.css # src/webui/assets/index.html # src/webui/assets/mlxcel-webui-manifest.json
- relativeTime clamps to the past: updated_at is server time and the render's now is client time, so a browser clock behind the server read recent operations as "in 3 seconds". Each step truncates instead of rounding, so 23.6 hours reads "23 hours ago" rather than "yesterday". - A measured tile without an observation time says so instead of "Observed at No observation time". - A primary metric absent from the snapshot says the server did not report it, instead of pointing at the measurements disclosure, which does not list it. Refs #1916 Refs #1929
…tivity-status-page # Conflicts: # docs/webui/ui-common.md # src/webui/assets/assets/code-highlight-HW2W_ZnA.js # src/webui/assets/assets/code-highlight-NhHf6Eg4.js # src/webui/assets/assets/code-highlight-RVRncNAw.js # src/webui/assets/assets/history-ClynwMho.js # src/webui/assets/assets/index-DnT-nVPF.css # src/webui/assets/assets/index-Wq1p5Z_k.css # src/webui/assets/assets/index-XNR5k3S1.css # src/webui/assets/assets/index-h-4zIuYd.js # src/webui/assets/index.html # src/webui/assets/mlxcel-webui-manifest.json
…#1975) ## Summary Closes the eleven LOW items left open by the #1930 and #1929 hardening, in one PR as the issue asks. - Models: the page before the inspector drawer is inert below 1100 px; row labels and tooltips isolate the display name; a row action whose row re-sorts onto another page turns the pager there and focuses the follow-up control (a failed action's one-shot return never turns the page); search, filters and sort survive a route round trip via `library-view.ts`, scoped to the server instance they were set against and cleared on logout; row callback refs are stable; consuming an inspector request notifies subscribers. - Drawer: a Tab propagates to descendants unless the adapter wraps it, the panel has no stop, or it starts on the package's stale first/last element. - Activity: the slot box is a region and tab stop only while it overflows (shared `useOverflowRegion`); Operations sorts only on operations/locale change; formatters are cached per locale and precision; the plural form follows the rounded count; zero context reads unknown; a re-selected model waits for its first sample instead of showing old tiles under the stale banner. - Gallery: a loading StatCard sample, placed after the lifecycle samples because `browser.spec.ts` pairs the progress samples with their next sibling; `csp.spec.ts` asserts its skeleton. ## Changes during review - The pager-follow first treated focus on `<body>` as still waiting, so in Safari and macOS Firefox (where clicking Next does not focus it) it turned the page back after the user paged away. The intent now remembers the row's page: a re-sort is followed, a user page change ends it. - The security review found the library view outliving logout and a server-instance change (the next session inherited the previous search; a remembered source or task could hide every row on a new catalog). It is now instance-scoped and reset on logout. - A browser spec now checks that the page behind the drawer is inert in Chromium and that focus returns to Inspect after Close. - A bilingual technical report is committed under `TECHNICAL_REPORTS/`. Existing tests that assumed the old behavior were updated: slot-region and tab-order expectations, row-label locators (built from `isolate()`), and Activity fixtures now seed one history sample. ## Test plan - [x] Every new test fails against the main sources and passes here (the CSP skeleton assertion by inspection: main's gallery renders no StatCard) - [x] typecheck, lint, full unit (35 node + 710 Vitest), default Playwright suite (124, includes models, activity, shell) - [x] `build_bundle.py --verify`, `check_binary_assets.py`, `check_webui_contract.py` - [x] CSP spec against a real secured server: `mlxcel-server` built from `dc1845d3` (`--release --features cuda`, GB10, embedded bundle, `--api-key-file --webui`), served `style-src 'self'` without `unsafe-inline`; both themes pass with the skeleton assertion Closes #1974
Part of #1910. Closes #1916.
Activity was prose and repeated "N/A" lines, and idle slots claimed an unknown context denominator: the occupancy bar was gated on
prompt_tokens, which the server leaves null for a slot without a task. The page is now a status page on the shared components, with every visible string from the catalog in both locales.What changed
display_name, still the only combobox on the route) sits beside Refresh and Export in thePageHeaderactions; the last snapshot time joins the description. The history toggle and its lazyhistory-*.jschunk are gone.StatCards: Active requests, Total completed requests, Total completion tokens, Queued requests. A null or missing value reads "unknown", never 0. A measured tile's hint is "Observed at {time}". A null tile gets a localized reason hint mapped from the server's closed reason set. Before the first sample, four decorative skeleton tiles sit under one localized status.Badgeinside the disclosuresummaryreplaces the sentence; clicking it opens the disclosure. The content (memory and timing notes, every measurement with its localized scope and its raw server reason, the history list) is built only while the disclosure is open. An unrecognized scope falls back to the "Unknown" label.DataTablein a named, focusable scroll region (more than eight rows scroll inside it). The table uses a fixed layout with percentage column shares and a container query for narrow columns (smaller cell padding, compact badge), so it fits a 390 px column in any font; headers are Slot, State, Context, Decoded, Cached. Occupancy rules follow the issue in order, so an idle slot with a known context reads0 / 40,960 tokens, and "unknown" appears only for a null or zero denominator or a processing slot without a count. Slot reasons are catalog labels; an unrecognized one keeps its raw text behind a disclosure.now, download progress, and cancel. A failed operation shows a localized reason for its error code (13 known codes plus a generic fallback); the raw code, the operation id, the model id and the absolute time appear only inside Operation details.error.messageis never shown.<path>of zero-length round-capped segments in the Active requests tile, shown once two samples with a value exist. Its right edge is the latest runtime sample, so a trailing run of null samples (for example after an unload) stays blank instead of sliding old dots to "now".metricValue(metric, locale),relativeTime(iso, now, locale)and the slot counts use cachedIntlformatters and catalog unit templates (singular and plural in English).reasons.test.tsreadssrc/server/webui/runtime.rs, so server wording drift fails a test instead of silently falling back. Unused keys are deleted fromstrings.tsandstrings.json.StatCardadapter forwardsloading(with the package's English "Loading" skeleton statuses made decorative) andsparkline.docs/webui/activity.md,ux-contract.mdandui-common.mddescribe the idle0 / Dexception, "unknown" instead of N/A, the sparkline and the error-reason mapping.Deviations from the issue body
metric.reason, and slot reasons, scopes and error codes are mapped to catalog labels. The epic contract says raw reason strings and enum names are not user copy; the raw text stays available inside the details disclosures.error.code, for the same contract reason; the code itself moved into Operation details.SmoothHeightkeeps its in-flight semantics (pinned byui-common.mdand the components spec) rather than "active when the list length changes".IconButtonbeside the count, because the count is the live region and is not focusable.0 / 40,960 tokens(Korean0 / 40,960 토큰).Evidence
Test first, on 835a552:
pnpm exec vitest run src/features/activity/activity.test.tsx -t "idle slot"failed withAssertionError: idle rows reading "0 / 40,960 tokens": expected +0 to be 4. It passes after the change. The review fixes were also test first: the sparkline-anchor test failed before its fix ("expected 117 to be less than 102"), the error-code tests (en, ko, unknown code) failed before theirs, and the zero-denominator and unknown-scope tests were checked by reverting the guard and watching them fail.document.documentElement.scrollHeightat 1440x900 on the mock (four idle slots, one operation, loaded-model chip): 1,548 before, 900 after. 900 is the viewport; the page content ends at 789 px. The issue's real-binary baseline was 1,584.Per-poll work
The page re-renders on every 2 s runtime poll. No polling, timers, intervals, fetches or document listeners were added, and
webui/scripts/activity-performance.mjsis unchanged (its selectors still match: one combobox, options named bydisplay_name).<section>per slot, 19StatCards inside the closed measurements disclosure, the operations list with acatalog.findper row.Chromium CDP per runtime update on the mock, three runs each:
Total main-thread time per poll is about the same (3.8 to 4.1 ms before, 3.5 to 4.2 ms after); the extra ~0.2 ms layout comes from the tile observation times and the sparkline changing each poll. The GB10
WebUI installed artifactjob starts the server without--slots, so it never renders the slot table. At a synthetic worst case (256 slots, 200 operations) the slot table costs about 0.7 ms more layout per update than the old sections; that path is not exercised in CI.Gates
All pass on the final head:
pnpm --dir webui run typecheck,lint,unit(35 node:test, 567 Vitest),check:theme-selectors,python3 scripts/webui/build_bundle.pyand--verify,scripts/ci/check_webui_contract.py,scripts/ci/check_binary_assets.py,scripts/ci/check_cross_repo_refs.py. Playwright Chromium, one spec per run: activity 8, ui-common-components 14, shell 14, browser 29, theme 23, ui-common-shell 12, models 8, settings 3, chat 2, performance 4.csp.spec.tsneeds the real server and was not run. The first CI runs failed the 390 px slot-table overflow assertion on Linux (wider fallback font, 31 px over); the fixed layout above was verified with Verdana forced through CSSOM (0 overflow, 57 px slack in English) and the CIWebUI bundlejob now passes.After merging main (7cc49fc, #1928) the branch still passes typecheck, lint, check:theme-selectors, unit (35 node:test, 608 Vitest),
build_bundle.py --verify, the contract and binary-asset checks, and Playwright activity 8, shell 14, ui-common-components 14, settings 5.Real-binary acceptance
Run against
mlxcel-serverbuilt from ca1c4df (--profile test-fast --features metal,accelerate, embedded bundle) with--settings --props --metrics --slots,qwen3-0.6b-4bitloaded and counters starting at zero, driven by headless Chromium through the bundled WebUI. The merge of main touches no Activity source./ui-api/v1/runtimeagrees (1 and 5).0 / 40,960 tokenswith empty decoded and cached cells. After it, slot 0 reads24 / 40,960 tokenswith 5 decoded and 0 cached, and slots 1 to 3 read0 / 40,960 tokens. No "N/A" and no "unknown" occupancy anywhere; effective parallelism and shared pool context read "unknown".op_ormdl_id is in the page text until Operation details is opened, which then shows both.qwen3-0.6b-4bit; tiles read1건and5 토큰, idle rows0 / 40,960 토큰, the operation row "모델 로드, 완료, 3분 전". Raw English reasons appear only inside the open disclosure.scrollHeightequals the 900 px viewport in English and Korean (the issue's real-binary before number was 1,584). At 390x844 there is no horizontal page scroll and the slot table does not scroll sideways, in both locales. The loaded model is the first option in the picker.The GB10 Activity performance gate (
WebUI installed artifact) ran on this PR for the first time ata0cccb35. The first run reportedinvestigatewith median decode degradation 2.21% (one visible, baseline CV 2.20%, paired range -5.5% to +5.8%) and 2.50% (two visible, CV 2.37%, range -4.3% to +4.8%). Both ranges cross zero and the medians sit at about 1x the baseline CV, the #1949 noise signature. Re-run of the same commit on a quiet host (no other jobs, load average about 1.4) passed: 0.45% (CV 0.86%) and -2.72% (CV 4.49%), bothwithin-target.Hardening
relativeTimenever reads a past event as future.updated_atis server time and the render'snowis client time, so a browser clock behind the server showed recent operations as "in 3 seconds". Each step now truncates instead of rounding, so 23.6 hours reads "23 hours ago" rather than "yesterday".activity.reason.counter_unavailablestring) instead of pointing at the measurements disclosure, which does not list it.6900e3c4, feat(webui): redesign the model library as a dense list with row actions #1930), the only source conflict wasdocs/webui/ui-common.md(main's DataTable and Badge rows, with this PR's Activity sentences kept); the bundle was regenerated. Typecheck, lint, unit (35 node + 696 Vitest), theme selectors,build_bundle.py --verify, contract and binary-asset checks, and Playwright activity/ui-common-components/models/shell/chat/settings (60) pass.Left as LOW: the slot scroll region stays a tab stop without overflow (kept for axe at any width);
Operationsre-sorts per render anddesign-system/format.tsbuilds anIntl.NumberFormatper call (both predate this PR); the downloadrepo_idstays visible by design.