Repository navigation
fix(web): fetch the preceding period so Explorer stat trends stop reading +100% - #708
Conversation
…ading +100% The stat cards' trend chip compares the selected window against the equal-length window before it, but the activity and read-activity queries only fetched the selected window. The comparison half was always empty, so pctChange(n, 0) printed +100% on every card for every range. Widen the two series fetches by one window length (comparisonSince); the usage summary (Archived/Expired, no trend) stays on the selected window.
…er since Reaching one window further back with a single request let rows from before an unaligned absolute since into the first charted bar, inflating the headline. Fetch the grid's comparison buckets as their own request (comparisonWindow) and keep the selection's fetch unchanged.
…both halves A preset's charted half ends at now, mid-bucket, so a comparison of count whole buckets was longer and read a flat account as a decline. Size it by what the chart actually covers. Clip the comparison rows to before the first charted bucket too, so a backend ignoring the bounds (the Storybook mocks) cannot count a charted bar twice.
|
The dashboard preview for this PR — redeployed on each push that changes the web app.
Preview always points at this PR's latest commit; Deployment is the immutable build of |
✅ No issues foundFixes the Explorer trend chips reading +100% by fetching the preceding period, and abbreviates large headlines. This push only regenerated visual baselines, which clears the stale LorePage screenshot. Progress: open review threads 1 → 1 → 0 → 0 → 0 across the last 5 reviews Review details
Memories used
Nothing to report — standards (skipped), measurability (skipped), integrations (skipped), severity, 0 files skipped.
|
Renders the Activity panel against MSW handlers that honour since/until and serve a uniform rate, and asserts the trend chip reads 0%. Imports nothing this PR adds, so on main it fails on the trend itself (+1200%..+5800%).
Local verify —
|
| PR | #708 · no ticket |
| Commit verified | 05237c38 (branch fix/explorer-stats-trend-window) |
| Stack | web (next dev, webpack) → production edge functions · command, serving this checkout |
| Telemetry | dash0-mcp · dataset default (RUM service.name=web, edge service.name=api) · 1 loop(s) |
| Run | 2026-10-20T10:38:25Z → 2026-10-20T10:55Z |
Acceptance criteria
| ID | Criterion | Result | Evidence |
|---|---|---|---|
| AC-1 | Each trend chip equals the period-over-period change computed independently from the responses (24h / 7d / 30d / All) | pass | S-1, S-2, S-3; oracle match on all four ranges (e.g. 24h Written 1 vs 12 → −92%; 30d 38 vs 123 → −69%) |
| AC-2 | The headline equals the selection's charted buckets; the Archived card has no chip | pass | S-1–S-3; headlines match the oracle; Archived hasChip=false on every probe |
| AC-3 | One selection + one comparison request per series; the comparison ends at the first charted bucket and covers the charted span; all 2xx | pass | RUM: 2× POST /activity, 2× GET /read-activity, 1× GET /usage per load, all 200; e.g. 7d comparison 2026-10-07T13:07Z → 2026-10-14T00:00Z, the same length as the selection, ending where the chart starts |
Edge cases
| ID | Probe | Result | Evidence |
|---|---|---|---|
| EC-1 | Absolute day range (3 days) | pass | Comparison 10-09 → 10-13 sits right before 10-13 → 10-17; chips match the oracle |
| EC-2 | Drilled single hour | pass | One bucket shows no chip (StatCard needs ≥2 points; behaviour unchanged from main); requests still well-formed |
| EC-3 | Unaligned ISO range 10:30 → 12:00 | pass | S-4; comparison 08:30 → 10:00; no rows from before the window counted |
| EC-4 | Scope selected | pass | All 4 series requests carry scope=repo::mthines/lorekit; chips match the oracle |
| EC-5 | Comparison request fails (injected 500) | pass (see F-1) | S-5; the documented error message, no uncaught error, no blank page |
| EC-6 | Rapid toggling 24h → 7d → 30d → 24h | pass | Superseded requests cancelled (RUM: no status); final chips match the final range |
| EC-7 | 375px viewport at 7d | pass | S-6; no horizontal overflow |
Not applicable (2)
- Permissions / content / size: the diff touches no input, authorization, or rendering of user text.
- Empty window: covered by the unit spec and Storybook (0 vs 0 → 0%); the live account always has activity.
Findings
| ID | Severity | Kind | What happened | Status |
|---|---|---|---|---|
| F-1 | low | ux | If only a comparison request fails, the whole stats header shows its error state, so the selection figures go with the chips | open (design trade-off) |
| F-2 | medium | crash | pre-existing: next dev --turbopack fails on /lore with "Export ApiToken doesn't exist in target module" (type re-export from a 'use server' file, lib/tokens.ts, since #697). Webpack dev and the production build are unaffected |
open, out of scope |
| F-3 | low | performance | pre-existing: RUM reports CLS 0.34 on /lore loads. Not caused by this diff (no layout change); not compared against main |
open, out of scope |
Fixes
No code changes.
Screenshots
Under the hood
- AC-1/AC-2: browser →
POST /functions/v1/memories/activity×2 +GET /read-activity×2 +GET /usage→ 200 each, ~0.5–0.9 s, run in parallel. Edge spans continue the browser's trace IDs; no ERROR span onfaas.name=memoriesacross the run. - AC-3: the comparison request's window always ends at the chart's first bucket and has the same length as the charted span (7d:
10-07T13:07 → 10-14T00:00; 30d:08-22 → 09-21). - The only 5xx in the run: 6×
POST /activity500 at 1–4 ms, the Playwright-injected EC-5 failure. The onlybrowser.error: F-2, from before the stack switched off Turbopack.
Open and escalated
- F-1 — what a comparison failure should do. Today
Promise.allturns any one failed request into the header's error state.- Option (a): keep as is. It is graceful, and it is how a failure of the selection request already behaves.
- Option (b): on a comparison failure, keep the figures and hide only the chips. Falling back to "no comparison data" without hiding the chips would bring back the +100% bug.
- Recommendation: (a) for this PR; (b) as a follow-up if partial failures turn out to be common.
- F-2: a separate fix, e.g. import the types with
import typein their consumers instead of re-exporting them from the'use server'module.
Lessons
local-verify-lessons::turbopack-dev-use-server-type-export(repo scope)local-verify-lessons::networkidle-never-settles(repo scope)
Generated by /local-verify · full artifacts in .agent/fix/explorer-stats-trend-window/local-verify/
…d chip keeps its room A 1,566,285 headline pushed the trend chip beside it off the card. Headlines now render exact and grouped below 10,000 and compact to three significant digits above it (12.3K, 883K, 1.57M), so they never exceed five characters. The exact figure stays on hover and in AnimatedNumber's announced twin. Co-Authored-By: Claude <[email protected]>
Co-Authored-By: Claude <[email protected]>
Regenerated by the Update visual baselines workflow for PR #708.
|
✅ Update visual baselines: committed 36 updated baseline(s) as ℹ️ This commit was pushed with |
The baseline workflow regenerated every stale screenshot on main. Restore the ones outside this PR's --changed story set so the diff stays scoped; the remaining updates are stories CI runs here (AnimatedNumber importers) whose baselines had drifted on main. Co-Authored-By: Claude <[email protected]>
|
Darkplane Automerge did not approve this pull request. It scored 65.81 of 100, and 75 was required.
The other 2 criteria
An AI evaluator produced these scores from the diff, the repository history, and the pull request text. The pull request text is author-controlled, so treat the scores as signals rather than proof. Reviewed for commit 47f0dc6. |
Addresses @dash0-dev's comment: #708 (comment) Refs: #708 Co-authored-by: dash0-dev[bot] <257284812+dash0-dev[bot]@users.noreply.github.com>
…r it Addresses @dash0-dev's comment: #708 (comment) Refs: #708 Co-authored-by: dash0-dev[bot] <257284812+dash0-dev[bot]@users.noreply.github.com>
… earlier The comparison sat directly before the chart, so today's cell weighed this morning against yesterday afternoon and read identical 09-18 days as -43%. It is now the charted span moved back a whole grid length, with the chart start carried explicitly for the row split. Presets move too: on a daily rhythm 7d goes from -5% to 0% and 30d from -1% to 0%; 24h and uniform rates are unchanged. Addresses the pr-reviewer comment: #708 (comment) Refs: #708 Co-authored-by: dash0-dev[bot] <257284812+dash0-dev[bot]@users.noreply.github.com>
…halves Addresses the pr-reviewer comment: #708 (comment) Refs: #708 Co-authored-by: dash0-dev[bot] <257284812+dash0-dev[bot]@users.noreply.github.com>
…end-window # Conflicts: # packages/web/src/app/(dashboard)/lore/__screenshots__/LorePage.stories.tsx/Default-1-chromium-linux.png # packages/web/src/components/lore/__screenshots__/DuplicateClustersSidebar.stories.tsx/Default-1-chromium-linux.png # packages/web/src/components/lore/__screenshots__/DuplicateClustersSidebar.stories.tsx/Saturated-Window-1-chromium-linux.png
|
/update-baselines |
Regenerated by the Update visual baselines workflow for PR #708.
|
✅ Update visual baselines: committed 15 updated baseline(s) as ℹ️ This commit was pushed with |
The baselines commit was pushed with GITHUB_TOKEN, which does not trigger workflow runs. Co-authored-by: dash0-dev[bot] <257284812+dash0-dev[bot]@users.noreply.github.com>






Every stat card in the Lore Explorer's Activity panel showed a +100% trend at every range.
The trend chip compares the charted window against the equal-length period before it (
computeRangeTrends/computeCountTrendtally2 × countbuckets). ButfetchExplorerStatsonly asked the server for the selected window, so the comparison half was always empty, andpctChange(n, 0)returns +100% for any n.Fix: activity and read-activity are now each fetched a second time, over the grid's comparison period (
comparisonWindowinlib/aggregations.ts):chartStart), so an unaligned range can't leak earlier rows into the first bar and nothing is counted twice, not even against the Storybook mocks, which ignore the bounds.The selection fetch and the Archived/Expired usage summary are unchanged. The cost is two extra small requests per header load.
Headlines: stat-card figures of 10,000 or more now abbreviate (
12.3K,1.57M, viaformatHeadline) so the trend chip keeps its room beside them; the exact value stays in the hover title and the screen-reader announcement. The Storybook visual baselines are refreshed.Tests:
explorer-stats.trend.spec.tsdrivesfetchExplorerStatsagainst a fake server that filters bycreated_atthe way the RPCs do. It pins:ExplorerStatsTrend.test.stories.tsxis the end-to-end repro. It renders the Activity panel against MSW handlers that honour the window and serve a uniform rate, and imports nothing this PR adds. Onmainit fails on the chip itself (+1200%–+5800%where0%is honest); with the fix it passes.Docs:
tags.mdxanddocs/key-files.mddescribe the headline abbreviation. The trend fix restores documented behaviour ("vs. the preceding period of the same length").🧪 UI verification spec — run against the preview deployment
Target: preview
Format: intent
Spec 1: The Activity panel's trend chips compare against the preceding period
Changed: the trend chips on the Stat charts cards (Memories written, Memories retrieved, Memories opened, Scopes active) in the Lore Explorer's Activity panel
Start: /lore
Preconditions:
Steps:
Expected:
Hints:
Out of scope: