# Insights UI, accessibility, and browser evidence

Date: 2026-09-05. Scope: F04, F10, F11, and UI portions of F05–F09/F14. The original review and plan were preserved. Application changes were not deployed.

## Result and evidence strength

**Implemented and verified in Chromium with synthetic API data:** all 23 browser cases pass; 41 focused JavaScript checks pass. The browser loaded the actual static application and Chart.js. Every `/api/` request was intercepted. A separate static-only Node server bound an ephemeral loopback port and refused API requests; the test's `afterAll` closed it. No existing application backend, warehouse, real credentials, migrations, or business data were used.

These are rendered browser assertions and screenshots, not live provider/backend integration or screen-reader results. Backend contract and isolated execution evidence belongs to the other workstream reports.

| Finding | Before | Implemented behavior | Verification |
| --- | --- | --- | --- |
| F04: totals | A 600-row result could total only its first 500 rows. | Aggregation visits every returned row. 499/500/501/600 rows at GBP 2 produce GBP 998/1,000/1,002/1,200. | Exact displayed totals, CSV row counts and sums, reload assertions. |
| F04: types and units | Repeated numeric zero/one could become a flag; currency defaults disagreed. | Declared money/count remains numeric. Canonical currency metadata wins; explicit legacy ISO aliases remain supported; unknown currency is labeled. Percentage fractions are displayed/exported in percentage points with a `%` header. | Money/count 0 and 1, unknown currency, metadata override, table/chart/CSV/share percentage assertions. |
| F04: incomplete and nonadditive results | A capped subtotal could read as a complete total. | Incomplete rows and sums say “shown”; missing money values remain uncovered. Rates, averages and declared nonadditive columns do not acquire unproven totals. | Partial-result metrics, partial export names, nonadditive unit cases. |
| F06: pending work | Reload could lose the exact question/busy state; client timeout could diverge from server state. | Reload checks the server before allowing another submit; restores the exact pending question and parent; server status controls timeout/cancellation. Transient status failure stays busy with retry. | Reload/cancel, 503 recovery, simulated 181-second clock with authoritative server timeout. |
| F06: lost acknowledgement | A retry or reload could create a new logical submission. | Retries retain the exact URL, payload and request ID. The ambiguous request is retained in session storage scoped to user/vault/connection and replayed after reload. Accepted initial requests replace the optimistic thread even when the user has moved to another inquiry. | Lost initial response, double lost response plus reload, navigation before acknowledgement. Backend atomic deduplication is tested separately. |
| F07/F08: useful rows | Optional work delayed the answer; repeated transfer could hydrate whole histories. | `ready` fetches one persisted turn and renders rows while optional work continues. `done` refreshes that same turn; selected chart/SQL view is retained. Rail requests summary metadata. Optional failure uses a non-destructive toast. | No full-history refetch on ready; selected SQL view survives optional completion; real 20-second optional hold and failure preserve rows. |
| F09/F05: answer record | Query execution time could imply source freshness; confidence could imply checks not performed. | Compact receipt separates queried time, saved snapshot time, execution identity, unknown source refresh times, semantic obligations, completeness and confidence scope. | Receipt assertions and visual inspection. Semantic proof itself is backend evidence. |
| F09: exports | “Export CSV” could silently execute a fresh query. | “Export snapshot CSV” uses the saved rows. “Refresh and export CSV” explicitly requests new execution; filenames identify snapshot/refreshed and partial status. | Request-count assertion proves snapshot export does not rerun; distinct fixture values prove refreshed export does. |
| F10: empty results | Recovery suggested inspecting evidence that was hidden. | Zero-row results retain the SQL view, inspect-filters action and an edit-question action that opens and focuses the composer. | Mobile rendered SQL/filter text and focused populated input. |
| F11: panels | Keyboard focus could enter collapsed content; mobile panels lacked reliable dismissal/return. | Hidden rail and library are inert. Mobile rail/library manage entry, Tab trapping, Escape and focus return. | Desktop collapse, narrow-screen rail/library keyboard assertions. |
| F11: examples | Selection could replace the focused button and move the questions offscreen. | Theme selection preserves the button; narrow-screen theme navigation scrolls horizontally with visible, wrapped questions. | Focus, pressed state, viewport and overflow assertions; screenshot. |
| F11: charts | Default labels had poor dark-theme contrast and canvas lacked a useful alternative. | Explicit named canvas, unit-bearing labels/tooltips and an expandable exact data table. Theme-token colors update existing charts when the theme changes. | Both-theme computed-color contrast, accessible-name attributes, real table values and screenshots. |

## Measured timings

All measurements below are **measured synthetic browser observations**, not estimates or production claims. No 10× claim is made.

| Measurement | Observations/result | Boundary |
| --- | --- | --- |
| Ready status to visible 600-row metric | N=30; median 286.83 ms; p95 292.98 ms; maximum 294.72 ms | Fixture server state changed to `ready`, 250 ms polling, intercepted single-turn transfer, actual DOM render. Sequential, one browser worker; optional work held pending. |
| Rows with a real 20-second optional hold | Visible after 302.93 ms; optional hold 20,302.67 ms; rows preserved after optional failure | Actual elapsed wait in browser test, synthetic job API. No provider invocation. |
| Chart label contrast, dark | 9.49:1 | Browser Chart.js tick color `rgb(175 200 216)` against card `rgb(21, 32, 42)`. |
| Chart label contrast, light | 9.79:1 | Browser Chart.js tick color `rgb(42 71 89)` against white card. |

Raw observations: [ui-ready-render.json](ui-ready-render.json), [ui-optional-20s.json](ui-optional-20s.json), [ui-chart-contrast.json](ui-chart-contrast.json). The original review's dark default-label contrast was approximately 2.88:1; this is a color comparison, not an end-to-end performance baseline. N30 consists of fresh fixture pages in one warmed browser process; it is not a cold/warm production or concurrency benchmark.

## Commands and results

Run from the repository root using the installed Playwright dependencies. `beforeAll` automatically prepares the real Chart.js 4.4.7 fixture; no session-specific file is required. The optional first command warms its cache for an offline browser run:

```sh
node tests/e2e/helpers/insights-chart-fixture.cjs
node --test tests/js/bi-answer-values.test.mjs tests/js/bi-result-date.test.mjs tests/js/bi-rowcap-gate.test.mjs tests/js/bi-sql-display.test.mjs
./node_modules/.bin/playwright test --config=playwright.insights-fixes.config.js --reporter=line
node --check src/institutional_kb/static/js/bi.js
node --check src/institutional_kb/static/js/bi-examples.js
git diff --check
```

The helper downloads the canonical `https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.js` and requires SHA-256 `2812cb8825fdc57469eb2f7bb055e9429244e599920511ee477e828499b632cb` before use. It verifies cached bytes too and replaces the cache atomically. The default cache is the operating-system temporary directory's `lore-browser-fixtures` subdirectory; `LORE_BROWSER_FIXTURE_CACHE` can select a persistent CI cache. A clean cache requires network access to that pinned artifact. No minified library or new package dependency was added to the repository. Offline verified-cache reuse and rejection of incorrect downloaded bytes are recorded in [ui-fixture-setup.log](ui-fixture-setup.log).

- [ui-values-before.tap](ui-values-before.tap): initial regression fixture, 2 passed / 7 failed before the value fix. Failed expectations establish the 500-row boundary, numeric types, partial totals and currency mismatch; the assertions were retained.
- [ui-focused.tap](ui-focused.tap): 41 passed / 0 failed, including the existing result-date, row-cap and SQL-display checks. The new value file contains 10 cases.
- [ui-browser-final.log](ui-browser-final.log): final 23 passed / 0 failed in 1.2 minutes, one worker, no retries.
- [ui-optional-failure-before.log](ui-optional-failure-before.log): preserved intermediate red evidence. It caught optional failure replacing the entire thread with an error; the renderer now retains checked rows and shows a toast.
- [ui-navigation-retry.log](ui-navigation-retry.log): 3 targeted recovery/navigation cases passed after the final acknowledgement reconciliation fix.
- [ui-final-copy-smoke.log](ui-final-copy-smoke.log): 4 passed after the final visible generation-confidence and currency-neutral wording changes; refreshed all five screenshots. The API fixture now carries the persisted `saved_snapshot` receipt shape.
- [ui-repro-browser.log](ui-repro-browser.log): 9 passed in 10.5 seconds after replacing the Chart.js dependency setup and adding browser request instrumentation; retains real Chart.js value/contrast checks. The suite now contains 24 cases; the existing 23-case full result above predates this additional diagnostic case.
- [ui-cold-fixture-browser.log](ui-cold-fixture-browser.log): 2 passed in 4.0 seconds with a newly created empty cache, proving automatic download/setup works without any earlier session file. The chart and new diagnostic cases passed after the final timing fields were added.
- Syntax and `git diff --check` passed. The lead rebuilt generated CSS with `npm run build:css`; final screenshots use that bundle.

Tooling deviation: `npx eslint` did not find a local executable and fetched ESLint 10.10.0 into the npx cache. This was an unintended tool install outside the repository, already reported to the lead; package and lock files were not changed. Its [ui-eslint.log](ui-eslint.log) reports zero errors and 134 escaped-HTML advisories. It is not evidence from a pinned project ESLint installation. Subsequent browser commands used the existing explicit local Playwright binary.

## Submission, polling, transfer and rendering observation

[ui-request-observation.json](ui-request-observation.json) records a separate N=1 synthetic diagnostic. It counts the submission POST, queued/running/ready/done status reads, selected-turn fetches and full-history fetches after submission. Request/response/completion events share one Node monotonic clock; submit-event and result-render marks share a separate browser clock. Those clocks are never subtracted from each other. The JSON includes ready-to-response-completion and response-completion-to-observed-visible intervals, so polling, response handling and the observed rendering boundary remain distinguishable.

The first run observed one submission, four status polls, two selected-turn reads and zero full-history reads after submission. Each turn carried 5,867 bytes of uncompressed fixture JSON. These are fixture body sizes, not measured network wire bytes. The diagnostic deliberately holds queued/running states and includes browser automation overhead; its end-to-end time is not a production latency or percentile claim. The existing N30 ready-to-visible measurement is retained unchanged.

Exact focused reproduction commands for this setup/measurement follow-up:

```sh
./node_modules/.bin/playwright test --config=playwright.insights-fixes.config.js --grep 'rendered total|partial snapshot|charts have|fractional percentages|ready rows|records synthetic' --reporter=line
insights_fixture_cache=$(mktemp -d /tmp/lore-insights-chart-cache.XXXXXX)
LORE_BROWSER_FIXTURE_CACHE="$insights_fixture_cache" ./node_modules/.bin/playwright test --config=playwright.insights-fixes.config.js --grep 'charts have|records synthetic' --reporter=line
```

The final diagnostic observed 291.62 ms from ready availability to visible metrics: 238.68 ms through result response completion, then 52.94 ms to observed visibility. Submission-event-to-render-mark was 832.50 ms on the browser clock. These single synthetic observations expose the measurement boundaries; they do not replace the N30 acceptance distribution or measure a live answer path.

## Screenshots

- [600 returned rows, GBP 1,200, dark desktop](600-rows-dark.png)
- [Zero-row SQL inspection, narrow dark viewport](empty-mobile-dark.png)
- [Visible questions after theme selection, narrow light viewport](examples-mobile-light.png)
- [Named chart and data alternative, narrow dark viewport](chart-mobile-dark.png)
- [Same chart, narrow light viewport](chart-mobile-light.png)

All screenshots contain synthetic values. Reduced-motion mode was enabled to capture final chart values rather than intermediate animation frames. Chart screenshots scroll the chart into view above the fixed composer. These are local evidence artifacts for the lead's final remote report; no application deployment was performed.

## Limits and remaining checks

- No screen-reader session was run. Verified accessible names, real table alternatives, inert behavior and keyboard focus in Chromium do not establish NVDA/JAWS/VoiceOver announcements or reading order.
- Browser viewport coverage is 1440 × 1000 and 390 × 844; actual iOS/Safari, Android keyboards, zoom and forced-colors modes were not exercised.
- API routing was synthetic. Owner/vault authorization, actual shared-link redaction, backend deduplication, real deadlines, warehouse freshness, SQL semantics and database-role restrictions require the independent backend evidence.
- The browser verifies same-unit table/metric/chart/CSV/reload/share values. It does not certify financial rounding policy, mixed-currency conversion, or rates whose denominator metadata is absent; the UI suppresses unproven aggregate headlines for those nonadditive fields.
- Session storage can be unavailable by browser policy. The immediate retry still preserves the key in memory, but durable reload recovery cannot persist without storage.
- Provider timing, SQL time, full HTTP latency and production cold/warm concurrency are outside these measurements. The 181-second test advances the browser clock; the 20-second optional-delay test uses actual elapsed time.
- Optional single-turn refresh preserves the selected chart/SQL view. No assertion in this suite certifies every transient table-selection, enrichment-editor or assistive-technology focus state during that refresh.
