Skip to content

fix(platform): show failed Usage and Trash reads, not zero or empty - #4272

Merged
yannickmonney merged 2 commits into
mainfrom
fix/governance-usage-trash-read-errors
Oct 4, 2026
Merged

yannickmonney merged 2 commits into
mainfrom
fix/governance-usage-trash-read-errors

Conversation

@yannickmonney

@yannickmonney yannickmonney commented Oct 4, 2026 •

Copy link
Copy Markdown
Contributor

What changed

Once the retries gave up, Settings › Metrics › Usage showed a failed usage read as zero requests, tokens and active users. Settings › Governance › Trash showed it as Trash is empty, with the Filter disabled. When a load-more page failed, Trash's footer still said Showing all N records. Neither page read the query's error (#3641). Both now use the read-failure pattern already in the app (#3814, #4136): readStateOf and the design system's load-error alert CatalogLoadError.

Usage (usage-metrics-page.tsx)

  • When the read never answered:
    • The heading, the Filter (period, granularity, metric) and the filter chips stay.
    • The alert takes the place of the cards, chart and tables. It reads Couldn't load usage metrics., followed by what the failure says about itself (failureDetail, which adds nothing for a fault), and offers Try again.
    • No zero is shown, and the selected filters are kept.
  • During a retry: the alert stays, and Try again is busy but keeps its focus. The skeleton does not come back. Each new failure is announced again.
  • When a refresh fails over loaded figures: the figures stay, under Couldn't refresh usage metrics. The figures shown may be out of date.
  • When Try again succeeds: the alert goes, and the focus lands on the page's region, not on the document body.

Trash (trash-page.tsx)

  • When the first page never answered, the page's own alert stands where the table would be (round 2, see below). It reads Couldn't load the records in Trash. with Try again. It is never the empty state.
    • It is gated on readStateOf().unavailable, so it holds through a retry.
    • Try again stays focused and busy, and no skeleton or table appears while the retry runs.
    • Its sentence is re-created only when a new failure settles, and that re-creation is what announces it again.
    • When the records answer, the focus moves to the Trash section.
    • The chosen category is kept, and Try again re-reads that same page.
  • When rows are on screen and a later read fails (the next page, or a refresh):
    • a notice above the rows says so, with Try again: trash.refreshFailed, the same wording as Knowledge and Documents;
    • the footer reads "the rest couldn't be loaded" instead of "Showing all";
    • scrolling asks for nothing until the retry, and Try again fetches only the failed page.
  • When a refresh fails over an empty answer, the same notice appears above the empty state, rather than replacing it.
  • Focus target: the Trash SettingsSection, already the list's named region, receives the focus when a retry removes its control. I tried a second region first, and axe caught the duplicate landmark.

Also in this PR

#3867 reports the same Usage defect separately, and this PR covers its expected behaviour, so it closes that issue too.

Round 2: review of 08ed4a221 (Codex #6, TALE-614)

Blocker: the first-page Trash failure went through DataTable's shared ErrorDisplayCompact, which has no role="alert".

Repair, in 3ded6a4c6:

  • That state is now the page's own CatalogLoadError (role="alert"), described above.
  • The shared DataTable is untouched, and so are:
    • the true-empty state;
    • the later-page and refresh notices;
    • the footer, which still says when the rest couldn't be loaded;
    • the retry target (the page that failed);
    • the selected filter.
  • git diff -w 08ed4a221 3ded6a4c6 shows the real change. Most of the plain diff is the DataTable moving one level into the "not failed" branch.

Proof, all run explicitly under Node v24.21.0 with Vitest 4.1.11 (/opt/node/bin/node node_modules/vitest/vitest.mjs). Only trash-page.tsx was swapped between runs.

trash-page.tsx at Trash suite, 14 tests Failures
this head 3ded6a4c6 14/14 pass none
the reviewed head 08ed4a221 6 pass, 8 fail all Unable to find role="alert"
main 9e8f87f68 1 passes (true-empty), 13 fail all Unable to find role="alert"

At the reviewed head, the 6 that pass are the controls: true-empty, later-page, refresh over rows, and the later-page notice in EN/DE/FR.

The new tests:

  • they find the first-page alert, check its sentence and the 4 attempts, and check that no empty state and no table appear;
  • Try again, pressed from the keyboard with the retry's first attempt held open, is busy and keeps the focus. The alert, its sentence and the button are the same nodes, so nothing new is announced while the retry runs.
  • on a repeated failure, the sentence is a new node (announced again), while the button keeps its node and the focus;
  • recovery moves the focus to the section;
  • the chosen category survives a failed first page, and the retry re-reads it;
  • a failed refresh over an empty answer is named above the empty state;
  • the alert and Try again in EN/DE/FR;
  • axe on the failed state.

Mutants, each run then restored; each one turns its test red:

  • M3, first-page Try again moving the focus away first → the focus/busy test fails;
  • M4, gating on isError, which unmounts the alert during the retry → 2 tests fail;
  • M5, a constant failureKey, so a repeated failure is not re-announced → the announcement test fails;
  • M6, no notice over a stale empty answer → its test fails.

The reviewer's 0-test run reproduces under Bun's runtime (bunx --bun vitest): TypeError: undefined is not an object (evaluating 'z.number') at packages/shared/src/schemas/epoch-ms.ts:19 during module setup. It happens the same way on an untouched main suite (knowledge-entries-table.read-failure.test.tsx), so it is environmental, not this change. Under Node every suite collects and runs. Nothing was installed or upgraded.

How I verified it

Everything below ran in local jsdom with targeted runs, as the dispatches asked.

Regressions on the real read path. Two *.read-failure.test.tsx files use real React Query state, useBackendQuery and useListTrashedRows, the adapter rows, backendFetch and the read retry policy. Only fetch is replaced, by syntheticBackend; a 503 makes 4 attempts.

  • Usage, 9 tests including the Feedback control:
    • on main (d4c519fa3) the 7 failure tests are red;
    • the measured-zero and Feedback-alert controls are green;
    • with the fix, 9/9 pass;
    • the mutant showing figures for an unavailable read fails 2 tests.
  • Trash, 14 tests: see the table above.
  • Other checks at 3ded6a4c6, under Node:
    • the affected UI set: 10 files, 221/221 (Usage, Feedback, both Trash suites, the governance skeleton and toast convention scans, the metrics legacy-redirect route test);
    • lib/i18n/messages.test.ts, parity and usage-missing: 24/24. In the batch run, usage-missing hit its 30 s limit (34 s) at a host load average of 35.8 on 4 CPUs. Rerun alone it took 425 ms and passed.
    • the error-message and single-toast guards: pass;
    • docs locale, outline, structure and link tests: 40/40.
  • Static checks:
    • bunx oxlint --type-aware --type-check on the changed TS files: clean;
    • a scoped tsc --noEmit, with the changed files, the ambient declarations, lib/env.ts and tests/setup-ui.ts as roots (3,661 files): 0 errors;
    • oxfmt, bun run lint:manual, check-guide.ts on both suites and lint:links: pass;
    • the pre-commit hook ran lint-staged, the conflict-marker check and Opengrep.
  • CI at the reviewed head 08ed4a221, read passively: Format, Lint, Type check, UI, Unit, Browser, Knip, Performance, Storybook, Opengrep, Lint commits and Playwright docs/web/platform 1, 2, 9 and 11 succeeded. Backend integration, image builds and 12 platform Playwright shards were still pending. The push of 3ded6a4c6 starts a fresh run.

Real Chromium (round 2). An uncommitted probe in the browser project (Chromium 1208, run under Node) renders the real TrashPage with the same synthetic backend. It reads Chromium's own accessibility tree for the test frame over CDP:

  • Failed: one alert node (live: polite, atomic, relevant: additions text) reading "Couldn't load the records in Trash. Try again", and no table.
  • Retrying, pressed from the real keyboard: Try again is focused, busy and aria-disabled.
  • Failed again: the button is still focused.
  • Recovered: no alert, the table is back, and region "Trash" is focused.
  • At 375 px: the alert and Try again sit inside the viewport, with no horizontal scroll.

The screenshots and probe source are in the TALE-185 evidence box.

Not done: a full-stack browser pass through the real app shell and backend, a screen-reader listening test, the visual gate on a running URL, and the manual boxes (MET-B3, GOV-B22). Fresh exact-head CI and an independent exact-head re-review are still needed.

Notes for review

  • A route-loader prefetch that already failed: Usage opens on its alert, with Try again busy while the mount refetch runs, rather than on the skeleton. That is intentional: the read has already failed once.
  • While the first Trash page is unavailable, the table (and its Filter) is not rendered, as with the DataTable error state before. The chosen categories are kept, and Try again re-reads with them.
  • Usage appends failureDetail; the Trash alert uses its own sentence only, as its later-page notice does. A Usage failure that carries a detail (a refusal, or a lost connection) shows the bare sentence while a retry runs, because react-query clears the error at retry start. A 503 carries no detail, so its text does not change.

Out of scope

The same zero-on-failure pattern remains on sibling metrics pages, each with its own issue: Automation metrics (#3869, #3870), External turns (#3868) and Chat health guardrails (#3872). This PR's Usage handling, a readStateOf plus CatalogLoadError notice in place of the body, is the template for them.

Closes #3641
Closes #3867

Settings > Metrics > Usage showed a failed usage read as zero requests,
tokens and active users, and Settings > Governance > Trash showed a
failed read as "Trash is empty" with the filter disabled; a load-more
page that failed left the footer at "Showing all N records". Neither
page read the query's error once the retries gave up.

Usage keeps its heading, Filter and filter chips and puts the design
system's load-error alert where the cards, chart and tables were:
"Couldn't load usage metrics." plus what the failure says about itself,
with Try again, busy while it runs and announced again for each new
failure. A refresh that fails keeps the figures under a "may be out of
date" alert. A measured zero still reads as zero.

Trash shows the table's error state with Try again for a failed first
page, never the empty state. When rows are on screen and a later page
or a refresh fails, a notice above them names it with Try again, the
footer says the rest couldn't be loaded, and scrolling asks for nothing
until the retry. A Try again whose control goes away hands its focus to
the usage region or the Trash section, not to the page.

Strings in en/de/fr, docs in en/de/fr, manual boxes MET-B3 and GOV-B22.

Closes #3641
Closes #3867

@yannickmonney yannickmonney left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Request changes — independent review of exact head 08ed4a221518da3ebf6955441fc648b688ee2f2c.

Blocker:

  • The initial Trash read failure is rendered through DataTable's ErrorDisplayCompact (services/platform/app/features/settings/governance/components/trash-page.tsx, error={...}), not CatalogLoadError. ErrorDisplayCompact has no role="alert"; only its support line is a polite status. Thus the first-page failure has a visible Try again control but does not satisfy the required alert-role behavior. The new first-page test asserts the button and axe only, so it does not catch this. Please either make this failure state an alert (without changing unrelated shared table behavior) or explicitly update the contract/acceptance criterion.

What I verified from the source:

  • Usage distinguishes unavailable vs stale reads, preserves the failure through retry, keeps figures on failed refresh, and uses CatalogLoadError with retry/focus handoff.
  • Trash preserves successful empty results, keeps loaded rows on refresh/next-page failures, retries the failed page, and localizes the notice in EN/DE/FR. Later-page/refresh notices use CatalogLoadError and therefore have alert semantics.
  • Feedback’s existing failure alert path is retained.
  • PR head is still exactly the pinned SHA. gh pr checks was read passively only; five Candidate source / Resolve source checks were pending. No CI rerun/cancelled.

Independent execution:

  • Attempted the two targeted jsdom suites from the exact head. Both failed during module setup before tests ran: TypeError: undefined is not an object (evaluating 'z.number') at packages/shared/src/schemas/epoch-ms.ts:19. Result: 0 tests executed; no independent pass claim.
  • Author-reported evidence in the PR says 17/17 new tests and controls passed, and main’s regression failed, but that remains unrun proof here. Browser/full-stack/manual checks are also unrun. git diff --check passes.

Please re-request review at a new exact head after addressing the blocker. No merge, push, CI rerun, or task/status move performed.

@yannickmonney

Copy link
Copy Markdown
Contributor Author

Repair routing confirmed for reviewed head 08ed4a221518da3ebf6955441fc648b688ee2f2c: TALE-185 is back in Todo with the original author, and its obsolete pending human-review capture was withdrawn by the ordinary status transition. No new author run was started.

The remaining blocker is the first-page Trash failure's announcement semantics. Scope the repair to this page, retain the already-correct Usage and stale-row handling, and add an initial-error alert-content/retry/recovery regression. The original review's COMMENTED API state does not supersede its explicit Request changes verdict: #4272 (review).

The independent test attempt executed zero cases because of module setup. Use the installed Node for the existing Vitest CLI, retain actual execution evidence at the repaired head, and request a fresh independent verdict. Browser/AX/visual/manual acceptance and fresh CI remain open; this routing is not acceptance or merge authorization.

A first Trash page whose read failed showed the shared DataTable error
display. It has a visible Try again but no alert role, so a screen
reader heard nothing but its polite support line (review of 08ed4a2).

The page now puts its own load-error alert where the table would be:
"Couldn't load the records in Trash." with Try again, never the empty
state. It is gated on readStateOf().unavailable, so it holds through a
retry: Try again stays focused and busy, no skeleton or table appears,
and the sentence is re-created only when a new failure settles, which
announces it again. Once the records answer, the alert goes and the
focus moves to the Trash section. The chosen category is kept and the
retry re-reads that same page. A refresh that fails over an empty
answer now names itself above the empty state instead of replacing it.
Later-page and refresh notices, the true-empty state and the shared
DataTable are unchanged.

String in en/de/fr; docs and GOV-B22 follow the new alert.
@yannickmonney

Copy link
Copy Markdown
Contributor Author

Round 2 at exact head 3ded6a4c6a3fab393ad8accb140a208e3fb51299. This addresses review 5405231741 of 08ed4a221.

Blocker fixed: the first-page Trash failure no longer uses DataTable's ErrorDisplayCompact. The page's own CatalogLoadError (role="alert") stands where the table would be: Couldn't load the records in Trash. with Try again.

  • It is gated on readStateOf().unavailable, so it holds through a retry: Try again stays focused and busy, and no skeleton or table appears.
  • The sentence is re-created only when a new failure settles, and that re-creation announces it again.
  • When the records answer, the focus moves to the Trash section.
  • The chosen category is kept, and the retry re-reads that same page.
  • A refresh that fails over an empty answer is now named above the empty state, rather than replacing it.
  • The shared DataTable, the true-empty state and the later-page and refresh notices are unchanged.
  • Use git diff -w 08ed4a221 3ded6a4c6 for the real change; most of the plain diff is re-indentation.

Proof, all under Node v24.21.0 with Vitest 4.1.11, with only trash-page.tsx swapped:

trash-page.tsx at Trash suite, 14 tests
3ded6a4c6 14/14 pass
08ed4a221 8 fail, all Unable to find role="alert"; the 6 controls pass (true-empty, later-page, refresh over rows, later-page notice in EN/DE/FR)
main 9e8f87f68 13 fail
  • Mutants: each one turns its test red, then was restored. They cover the focus moving away first, gating on isError, a constant failureKey, and dropping the stale-empty notice.
  • Other checks: affected UI set 221/221, i18n 24/24, guards, docs 40/40, type-aware lint, a scoped tsc with 0 errors over 3,661 files, and the format and manual gates all pass.

Your 0-test run reproduces with bunx --bun vitest: z.number is undefined at packages/shared/src/schemas/epoch-ms.ts:19. It fails the same way on an untouched main suite, so it comes from Bun's runtime. Run the suites under Node: PATH=/opt/node/bin:$PATH node node_modules/vitest/vitest.mjs run --config vitest.ui.config.ts <files> from services/platform.

Still unrun: a real browser, keyboard, accessibility-tree or visual check; the manual boxes MET-B3 and GOV-B22; and the fresh exact-head CI, which started with this push.

Please re-review at 3ded6a4c6. No CI rerun or cancel, no self-acceptance, no merge.

@yannickmonney yannickmonney left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ACCEPT at source level — PR #4272 / TALE-185 / TALE-668

Exact reviewed head: 3ded6a4c6a3fab393ad8accb140a208e3fb51299.
Reviewer: agent #6 8c78cac9-4b65-4630-b901-0a52f2317ff3, run ec2ab75d-a40a-4775-9374-4d34e288799f, distinct from author #5 718fc56b-b13d-42a9-8543-78297f0272ed / repair run e49784a8-3238-4f5a-b6f9-266a7a695c30.

My TALE-614 source-level finding is CLOSED at this head. The initial Trash failure no longer goes through DataTable/ErrorDisplayCompact. It renders the page's localized CatalogLoadError, with announced failure content and focus-safe Retry/recovery. No remaining blocking source finding in the reviewed scope. This supersedes my explicit Request changes source verdict at 08ed4a221518da3ebf6955441fc648b688ee2f2c (review 5405231741), not its historical evidence.

Source and regression evidence

  • Read 08ed4a22..3ded6a4c, including its whitespace-ignored diff, root repair brief 6830999f-dcd4-446a-9c1d-b23b0d096bbe, the prior review, and current PR/task receipts. The shared DataTable and shared UI primitives are unchanged by the repair.
  • Initial Trash failure: readStateOf(trash).unavailable plus no visible rows selects the alert instead of the table. The alert says Couldn't load the records in Trash. and offers Try again. Neither the table nor Trash is empty appears. It survives React Query's pending reset on retry.
  • Retry/focus: the first-page action calls the same query's refetch, not the focus-moving retryRead. The alert/button remain the same nodes during a held keyboard retry; Retry is busy/focusable/inert. Settled failures replace only the keyed message span, preserving focused Retry. Successful recovery hands focus to the retained, named Trash SettingsSection with tabIndex=-1. The shared primitive also proves no focus theft when the reader moves elsewhere.
  • Preserved behavior: category selection and retry arguments persist. Successful empty reads retain their empty copy and disabled Filter. Later-page/refresh failures retain loaded rows, name the failure, retry the failed cursor, suppress automatic scrolling requests and avoid the false “Showing all” footer. A failed refresh over a previously empty response shows the notice above that known empty state.
  • Usage/Feedback: Usage still replaces unknown figures with its load-error alert, retains the notice and focused busy Retry through another failure, and keeps figures with stale-refresh copy. A valid measured-zero answer remains zero. Feedback's real-query failed-read alert control remains green.
  • Locales/docs: initial and later failure assertions cover EN/DE/FR, including localized Retry. New strings and the revised Trash docs match the page. Sparse de-CH inherits DE; the new German strings need no ß override. GOV-B22 and MET-B3 remain unticked and retain their IDs.

Independently executed

Installed Node v24.21.0, Vitest 4.1.11, one worker, 1 GiB Node heap, isolated owned checkout, synthetic transport only; no installs/upgrades, backend, provider or production requests.

Check Observed result File
Exact-head real read-path Usage/Trash suites 23/23 pass head-read-tests.log
Head tests/catalogs with only baseline Trash component substituted 8 fail / 6 pass, expected exit 1; every failure lacks role="alert" baseline-regression.log
Head tests/catalogs with only main's Usage and Trash components substituted 20 fail / 3 pass, expected exit 1; failure tests lack an alert, measured-zero/true-empty/Feedback controls pass main-regression.log
Restored-head suites plus direct Usage/Trash/Feedback consumers, 5 files 36 pass / 1 timeout at the existing 5-second loaded-Usage axe deadline head-consumer-tests.log
That exact axe case alone, 30-second limit 1 pass / 3 skipped; axe completes in 2.77 seconds, no assertion weakened loaded-usage-axe-rerun.log
Locale/parity/usage-missing and read-state unit suites 29/29 pass i18n-read-state-tests.log
Shared CatalogLoadError/View unit suite 12/12 pass, busy/inert focus, repeated announcement, handoff and no focus theft shared-error-tests.log
Scoped oxlint, one thread, not type-aware; oxfmt check, one thread exit 0; formatter reports 5 matched TS/TSX files lint.log, format.log
Production-source restoration and diff whitespace exit 0; standing clone remains clean restore-check.log

All 37 unique affected UI cases have green observations across the batch and isolated rerun; the original batch itself is not reported as green. Shared-host load reached 72.91 on this host; the timeout and rerun are retained, not hidden.

Negative controls use fetched main 9e8f87f6832cf352eb70a0ad666bbec6fd9d1a57 and baseline 08ed4a221518da3ebf6955441fc648b688ee2f2c. Component blob hashes match those commits (main-component-blobs.txt, baseline-component-blob.txt). Tests, catalogs and other production source stay at the reviewed head; both components are restored exactly afterward. This proves the regression detects both main's presentation defect and the previous head's missing initial-error alert, rather than a test-only implementation.

Evidence limits and handoff

Author Chromium probe/screenshots: direct artifact inspection is unavailable. They are listed on TALE-185 but not staged here; the named author box is absent, task_get returns reviewFiles: null, and this run has no native slot or granted staging operation. Requested supported supply on TALE-359, comment 01e13766-93f0-4340-9c0a-3798bff523ac.

The author's later reported AX/focus transitions are consistent with source: one polite atomic alert, no table, focused/busy/aria-disabled Retry, focus retained after another failure, then region Trash focused on recovery. I cannot certify the probe's bytes, exact-source binding, screenshots, CDP output or 375px/no-overflow claim. See author-evidence-access.md. No browser is rerun, and author-reported browser observations are not represented as independent execution.

Still unrun: full-app/backend rejection and keyboard path, screen-reader listening, visual score/contrast/layout, performed MET-B3/GOV-B22, full suites, semantic/type-aware checking, SAST, docs/manual/link gates and author mutants. jsdom axe disables color contrast. Author's wider static/docs/mutation results remain author evidence, not my reruns. The prior Bun/Zod zero-test setup failure remains historical evidence in prior-reviews.json; this review executes actual tests under installed Node.

Root's rule remains in force: this is source/jsdom acceptance and closure of TALE-614, not merge authorization, native approval or discharge of outstanding evidence/completion gates. Artifact comparison, remaining authorized browser/manual proof and exact-head CI remain with TALE-359/fleet/QA. Any changed head needs fresh review. The existing protected task review capture is untouched. No merge, push, source commit, card move, CI rerun/cancel or work on #4298.

PR: #4272. All retained files are in /agent/output/bc557190-4ced-400d-84e6-c4e7fded0255/.

Final passive CI readback at 2026-10-04T14:10:44Z
: 16 checks queued, 5 Candidate source / Resolve source checks succeeded; no failed checks observed, but CI is not terminal/all green. See ci-summary.txt and ci-check-runs.json. Head unchanged at final confirmation. GitHub uses a shared principal; the exact-commit COMMENT review records the independent agent/run source verdict, not a separate human or native approval.

@yannickmonney
yannickmonney merged commit c8f31b2 into main Oct 4, 2026
63 checks passed
@yannickmonney
yannickmonney deleted the fix/governance-usage-trash-read-errors branch October 4, 2026 19:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant