Skip to content

feat(dashboard): make the activity histogram the time-range selector - #445

Open
njbrake wants to merge 4 commits into
mainfrom
feat/usage-time-range-picker
Open

feat(dashboard): make the activity histogram the time-range selector#445
njbrake wants to merge 4 commits into
mainfrom
feat/usage-time-range-picker

Conversation

@njbrake

@njbrake njbrake commented Jul 28, 2026

Copy link
Copy Markdown
Member

Description

Reworks the Usage and Activity time-range controls around a request-volume histogram, the pattern CloudWatch Logs Insights, Kibana, and Grafana/Loki use for log exploration. The selection lives on the chart itself: full-height handles at the window edges zoom (react-aria slider thumbs, pointer-captured, 0.1-bucket steps), a slim strip along the axis band pans, regions outside the window dim in place, and −/+ buttons step the zoom. Zooming out past the extent promotes to the next larger preset (30d to 90d to 12mo), and Reset restores the full extent. Presets set the extent the chart spans; the tiles, breakdowns, and log follow the selection.

This fixes the three defects from the issue:

  • "All" on Usage silently meant a rolling 30 days (an omitted start hit the server's 30-day default), so the total drifted with the clock. Usage drops "All" for bounded 90d and 12mo presets.
  • The old custom picker (two datetime-local inputs) only filtered when both a date and a time were entered, and read local wall-clock times as UTC. The selection is now bucket-based and UTC-aligned, matching the summary endpoint's bucketing and the existing "times in UTC" captions.
  • The bucket is chosen from the active preset's extent, and the effective window is always captioned, so a default or clamp is never invisible.

Other changes riding along, all part of the same control rework:

  • Activity keeps a truthful "All" (its list endpoint is genuinely unbounded); both pages now share the timeline, presets, and window math (web/src/lib/timeRange.ts), so the labels can't drift apart again.
  • Active model/user/key/status/priced filters render as removable chips with an "Add filter" toggle for the pickers.
  • The Refresh button becomes an icon with an "updated Ns ago" freshness label, riding the timeline's preset row on Usage and Activity, and added to the Overview header.
  • Interaction details: drag feedback is CSS-positioned shades (the recharts tree never re-renders mid-drag), the window commits once on release snapped outward to whole buckets, the histogram spans the preset extent independently of the selection so there is always context to zoom back out into, bar tooltips keep working (the pan strip overlays the axis band, not the plot), and touch-pan-y keeps mobile scrolling intact.

Verified end-to-end against a live gateway with seeded data (drag-zoom, pan, step zoom with preset promotion, tooltips, mobile layout, drill-through to Activity).

PR Type

  • New Feature
  • Bug Fix
  • Refactor
  • Documentation
  • Infrastructure / CI

Relevant issues

Fixes #440

Checklist

  • I understand the code I am submitting.
  • I have added or updated tests that cover my change (tests/unit, tests/integration).
  • I ran the Definition of Done checks locally (make lint, make typecheck, make test).
  • Documentation was updated where necessary.
  • If the API contract changed, I regenerated the OpenAPI spec (uv run python scripts/generate_openapi.py).

Notes: this is a dashboard-only change (no gateway API contract change, so no OpenAPI regeneration needed). Frontend checks run instead: npm --prefix web run typecheck, npm --prefix web test (327 tests), and the committed bundle was rebuilt (npm --prefix web run build).

AI Usage

  • No AI was used.
  • AI was used for drafting/refactoring.
  • This is fully AI-generated.

AI Model/Tool used:

Claude Code (Opus 4.8 and Fable 5), directed by @njbrake

Any additional AI details you'd like to share:

Design direction (histogram-as-selector, overlay handles, preset promotion on zoom-out) was decided by @njbrake across several review rounds against the running dashboard.

  • I am an AI Agent filling out this form (check box if true)

🤖 Generated with Claude Code

Summary

  • Replaced Usage and Activity time controls with a shared request-volume timeline supporting presets, zooming, panning, reset, and UTC-aligned windows.
  • Updated Usage to use bounded 90-day and 12-month ranges, while preserving Activity’s unbounded “All” option.
  • Made selected windows drive charts, metrics, breakdowns, and request logs, with clearer effective-window labels and improved bucket selection.
  • Added removable filter chips with an “Add filter” control.
  • Added refresh freshness indicators and icon-based refresh actions across dashboard pages.
  • Added comprehensive tests for timeline interactions, time-range calculations, filters, refresh behavior, and page queries.
  • Rebuilt the dashboard production assets and updated their references.

Rework the Usage and Activity time controls around a request-volume
histogram, the pattern CloudWatch Logs Insights, Kibana, and Grafana/Loki use
for log exploration. The selection lives on the chart itself: full-height
handles at the window edges zoom (react-aria slider thumbs, so they are
pointer-captured and glide at 0.1-bucket steps), a slim strip along the axis
band pans, regions outside the window dim in place, and −/+ buttons step the
zoom. Zooming out past the extent promotes to the next larger preset
(30d -> 90d -> 12mo), so a wrong default window is always one tap from a wider
view; Reset restores the full extent. Presets set the extent the chart spans;
the tiles / breakdowns / log follow the selection. This also fixes the original
defects from the issue:

- "All" on Usage silently meant a rolling 30 days (an omitted start hit the
  server's 30-day default), so the total drifted with the clock. Usage now
  drops "All" for bounded 90d and 12mo presets, reaching the full span the
  server serves without an unbounded, clock-rolling window.
- The old custom picker (two datetime-local inputs) filtered only when both a
  date and a time were entered, and read local wall-clock times as UTC. The
  selection is bucket-based and UTC-aligned, matching the summary endpoint's
  bucketing and the existing "times in UTC" caption, so a picked range lines up
  with the bars and nothing is silently reinterpreted.
- The bucket is chosen from the active preset's extent, and the effective
  window is always captioned, so a default or clamp is never invisible.

Interaction details: drag feedback is a pair of CSS-positioned shades over the
chart (the recharts tree never re-renders mid-drag) and the window commits
once, on release, snapped outward to whole buckets. The histogram spans the
preset extent independently of the selection, so there is always context to
zoom back out into (one extra summary query, deduped with the active one when
the selection is full). The handles are the only chart-level selector, so a
stray tap cannot collapse the window; the pan strip sits over the axis band, so
bar tooltips keep working; and touch-pan-y keeps mobile scrolling intact.

Activity keeps a truthful "All" (its list endpoint is genuinely unbounded).
Both pages share the timeline, the presets, and the window math (lib/timeRange).

Active model/user/key/status/priced filters render as removable chips with an
"Add filter" toggle for the pickers, the log-tool convention for keeping applied
filters visible (especially on mobile). The Refresh button is an icon with an
"updated Ns ago" freshness label; on Usage and Activity it rides the timeline's
preset row rather than taking a dedicated header row, and stays in the header on
Overview.

Fixes #440

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@njbrake, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 17 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: e22c5e9b-e45c-4119-a719-96218d02d1aa

📥 Commits

Reviewing files that changed from the base of the PR and between b6340b4 and 4b17dc3.

📒 Files selected for processing (27)
  • src/gateway/static/dashboard/assets/ActivityPage-DIACtZv5.js
  • src/gateway/static/dashboard/assets/AliasesPage-x0NFKgtH.js
  • src/gateway/static/dashboard/assets/BudgetsPage-D-S9-U_l.js
  • src/gateway/static/dashboard/assets/ConfirmDialog-KvA7b1-a.js
  • src/gateway/static/dashboard/assets/DocsPage-BZcIfS8u.js
  • src/gateway/static/dashboard/assets/FilterChips-DZjxf-ot.js
  • src/gateway/static/dashboard/assets/KeysPage-CZjqxzex.js
  • src/gateway/static/dashboard/assets/ModelScopeControl-u-XlXgXz.js
  • src/gateway/static/dashboard/assets/ModelsPage-BkzSZx5b.js
  • src/gateway/static/dashboard/assets/OverviewPage-BedTU_aZ.js
  • src/gateway/static/dashboard/assets/ProvidersPage-BP60GuZv.js
  • src/gateway/static/dashboard/assets/SettingsPage-DR0J2MHH.js
  • src/gateway/static/dashboard/assets/TablePagination-CXhesdmH.js
  • src/gateway/static/dashboard/assets/ToolsGuardrailsPage-DiZ1TbOa.js
  • src/gateway/static/dashboard/assets/UsagePage-Ce_jAzFl.js
  • src/gateway/static/dashboard/assets/UsersPage-5kctrw19.js
  • src/gateway/static/dashboard/assets/index-BRBpdVLg.css
  • src/gateway/static/dashboard/assets/index-DV8qvdUj.js
  • src/gateway/static/dashboard/index.html
  • web/src/components/ActivityTimeline.test.tsx
  • web/src/components/ActivityTimeline.tsx
  • web/src/components/ui.test.tsx
  • web/src/components/ui.tsx
  • web/src/pages/ActivityPage.test.tsx
  • web/src/pages/ActivityPage.tsx
  • web/src/pages/OverviewPage.tsx
  • web/src/pages/UsagePage.tsx

Walkthrough

Changes

The dashboard adds shared UTC time-range utilities, interactive timeline and filter-chip controls, refreshed Usage and Activity page integrations, Overview refresh behavior, and regenerated static dashboard bundles and assets.

Dashboard analytics

Layer / File(s) Summary
Shared time-range contracts and calculations
web/src/lib/timeRange.ts, web/src/lib/timeRange.test.ts
Defines shared presets, bucket calculations, range mapping, and UTC window labels with test coverage.
Timeline, filter chips, and refresh controls
web/src/components/ActivityTimeline.tsx, web/src/components/FilterChips.tsx, web/src/components/ui.tsx, web/src/components/*.test.tsx
Adds interactive timeline selection, removable filter chips, and a visibility-aware refresh button with component tests.
Usage analytics integration
web/src/pages/UsagePage.tsx, web/src/pages/UsagePage.test.tsx
Uses shared windows, context summaries, dynamic bucketing, timeline controls, filter chips, and absolute drill-through bounds.
Activity analytics integration
web/src/pages/ActivityPage.tsx, web/src/pages/ActivityPage.test.tsx
Adds shared range handling, independent timeline extents, context aggregation, URL synchronization, and removable entity filters.
Dashboard wiring and generated assets
web/src/pages/OverviewPage.tsx, src/gateway/static/dashboard/assets/*, src/gateway/static/dashboard/index.html
Adds Overview refresh wiring and regenerates dashboard page bundles, shared dependencies, CSS, documentation, charts, and asset references.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

Suggested reviewers: khaledosman

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 1.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed It uses a valid Conventional Commit-style feat prefix and clearly summarizes the main dashboard time-range change.
Description check ✅ Passed All required template sections are present and substantially filled out, including PR type, relevant issue, checklist, and AI usage.
Linked Issues check ✅ Passed The changes implement the issue's histogram-based selector, bounded long presets, UTC-aligned bucketing, and shared Usage/Activity time-range logic.
Out of Scope Changes check ✅ Passed The diff is dominated by the requested dashboard refactor and bundle rebuild; no clear unrelated feature work stands out.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/usage-time-range-picker
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch feat/usage-time-range-picker

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai
coderabbitai Bot requested a review from khaledosman July 28, 2026 18:07
Four fixes from reviewing the PR diff:

- Frame a drill-down window that reaches outside the preset extent. A Usage
  drill-through carries explicit bounds while Activity's `range` param still
  holds the 24h default, so the timeline highlighted "24h" and drew an
  unrelated 24h histogram under a 30-day window. The window itself is now the
  frame (no preset highlighted), and zoom-out falls back to the smallest
  broader preset so it never dead-ends.
- Make arrow keys work on the handles. The slider's built-in arrow step is the
  fine pointer step (0.1 bucket), which the bucket-snapping commit immediately
  undid, so a single arrow press was a silent no-op. A capture-phase keydown on
  the wrapper reroutes arrows to whole-bucket steps before react-aria's
  unconditional handler sees them; PageUp/Down, Home, and End stay react-aria's.
- Commit pan drags from a ref. A pointerup can fire before the last
  pointermove's setState re-renders, so committing from the closure value could
  snap to a different bucket than the one on screen.
- Refresh stale comments that still referenced the removed calendar picker and
  its local-time semantics.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (11)
web/src/components/FilterChips.tsx (1)

52-61: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Give the ✕ control a visible focus ring.

It's tabbable and correctly labelled (nice), but only hover: states are styled, so a keyboard user tabbing across chips can't see where they are. Either add focus-visible: styling or lean on HeroUI's Button with isIconOnly, which brings its own focus treatment.

♻️ Minimal fix
-              className="ml-0.5 inline-flex h-4 w-4 items-center justify-center rounded-full text-[var(--otari-muted)] hover:bg-[var(--otari-line)] hover:text-[var(--otari-ink)]"
+              className="ml-0.5 inline-flex h-4 w-4 items-center justify-center rounded-full text-[var(--otari-muted)] hover:bg-[var(--otari-line)] hover:text-[var(--otari-ink)] focus-visible:ring-2 focus-visible:ring-[var(--otari-brand)] focus-visible:outline-none"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/components/FilterChips.tsx` around lines 52 - 61, Add a visible
keyboard focus treatment to the chip removal button in FilterChips, using
focus-visible styling consistent with the existing design tokens while
preserving its current hover appearance and accessibility attributes.
web/src/components/ui.tsx (1)

146-174: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Optional: let the clock idle when there's nothing to relabel.

The hook is tidy and leak-free (visibility pausing is a nice touch). One small polish: when updatedAt is falsy no label renders, yet a 15s re-render still fires on every RefreshButton on the page. Passing a nullable interval (or gating the hook on updatedAt) would make the idle case genuinely free.

Also applies to: 191-191

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/components/ui.tsx` around lines 146 - 174, Update useDisplayClock and
its RefreshButton call sites so the clock does not create an interval when
updatedAt is falsy and no label renders. Accept a nullable or disabled interval,
preserve the existing visibility and cleanup behavior when enabled, and ensure
each RefreshButton passes the disabled state for missing updatedAt.
web/src/components/ActivityTimeline.tsx (1)

19-24: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

The recharts tree does re-render mid-drag, unlike what the comment promises.

Nice architecture overall (CSS shades instead of chart redraws is the right instinct), but data is rebuilt on every render, and every pointermove calls setSel. So each drag frame hands BarChart a fresh array and recharts reconciles the whole histogram. This is the one place where memoizing is clearly earned by measured cost: hoist the chart element into a useMemo keyed on series/bucket so React reuses the exact same element during drags, and the comment becomes true.

♻️ Memoize the chart subtree so drags don't touch it
-import { useEffect, useRef, useState } from "react";
+import { useEffect, useMemo, useRef, useState } from "react";
   const starts = series.map((p) => p.bucketStart);
-  const data = series.map((p) => ({ x: p.bucketStart, requests: p.requests }));
   const n = series.length;
+  const chart = useMemo(() => {
+    const data = series.map((p) => ({ x: p.bucketStart, requests: p.requests }));
+    return (
+      <ResponsiveContainer width="100%" height={90}>
+        <BarChart data={data} margin={{ top: 4, right: 0, left: 0, bottom: 0 }}>
+          <XAxis
+            dataKey="x"
+            tickLine={false}
+            axisLine={false}
+            interval="preserveStartEnd"
+            minTickGap={40}
+            tickFormatter={(iso: string) => formatTick(iso, bucket)}
+            tick={{ fontSize: 10, fill: "var(--otari-muted)" }}
+          />
+          <Tooltip cursor={{ fill: "var(--otari-line)", opacity: 0.35 }} content={<TimelineTooltip bucket={bucket} />} />
+          <Bar dataKey="requests" fill={BRAND} radius={[2, 2, 0, 0]} isAnimationActive={false} />
+        </BarChart>
+      </ResponsiveContainer>
+    );
+  }, [series, bucket]);

Then render {chart} inside the role="img" wrapper in place of the inline ResponsiveContainer block.

Also applies to: 123-124, 278-295

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/components/ActivityTimeline.tsx` around lines 19 - 24, Memoize the
Recharts subtree in ActivityTimeline using a useMemo keyed by the stable series
and bucket inputs, keeping the chart data and BarChart element inside that
memoized value so pointer-driven setSel updates do not rebuild it. Replace the
inline ResponsiveContainer block inside the role="img" wrapper with the memoized
chart element, while preserving the existing chart configuration and drag
behavior.
web/src/lib/timeRange.ts (1)

28-28: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Prefer undefined over null for these absent values.

Both the unbounded preset marker and the "no series" return use null. The frontend standards ask for undefined in our own types, and it also lets consumers use plain optional-chaining/?? patterns without mixing both nullish flavours. Note this ripples into the Usage/Activity pages (out of this cohort), so it's a follow-up rather than a blocker.

As per coding guidelines: "Use undefined rather than null for absent values in own TypeScript types".

♻️ Sketch of the change
-  seconds: number | null;
+  seconds?: number;
-): { startIso: string; endIso: string } | null {
-  if (starts.length === 0) return null;
+): { startIso: string; endIso: string } | undefined {
+  if (starts.length === 0) return undefined;

Also applies to: 95-95

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/lib/timeRange.ts` at line 28, Update the time-range type and related
absent-value handling to use undefined instead of null, including the unbounded
preset marker and the no-series return in the relevant time-range symbols. Keep
valid numeric values unchanged and propagate the type change through local
consumers without introducing null checks.

Source: Coding guidelines

web/src/lib/timeRange.test.ts (1)

106-136: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Locale-dependent date assertions in both new suites. formatDay deliberately formats in the viewer's locale (correct for production), so every assertion on strings like "Jul 10" or "Jan 2" silently depends on the locale Node resolves from LANG/LC_ALL. Pinning the locale once for the test run keeps both suites deterministic across machines and CI images.

  • web/src/lib/timeRange.test.ts#L106-L136: pin the locale for the suite (e.g. set process.env.TZ-style config in the Vitest setup, or assert via a locale-explicit expected string) instead of relying on the ambient default.
  • web/src/components/ActivityTimeline.test.tsx#L43-L49: rely on the same pinned locale rather than hardcoding English month names.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/lib/timeRange.test.ts` around lines 106 - 136, Pin the locale used by
the test environment so date assertions are deterministic instead of relying on
ambient LANG/LC_ALL settings. Apply this to web/src/lib/timeRange.test.ts lines
106-136 and use the same pinned locale for
web/src/components/ActivityTimeline.test.tsx lines 43-49; update the shared
Vitest setup or locale-explicit expectations as appropriate, without changing
production formatDay behavior.
web/src/pages/OverviewPage.tsx (1)

146-153: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

previous is refetched but not counted as "refreshing".

refresh() kicks off previous.refetch(), yet isRefreshing leaves it out, so the button can report "done" while the vs-prev deltas are still landing. One extra term keeps the freshness label honest.

♻️ Suggested tweak
   const isRefreshing =
     today.isFetching ||
     period.isFetching ||
+    previous.isFetching ||
     health.isFetching ||
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/pages/OverviewPage.tsx` around lines 146 - 153, Update the
isRefreshing calculation in OverviewPage to include previous.isFetching
alongside the existing query-fetching flags, so refresh remains active until the
previous-period data completes.
src/gateway/static/dashboard/assets/index-C_gHYR5I.css (1)

1-1: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider excluding generated dashboard CSS from stylelint.

Every finding here (duplicate outline-style, to keyframe selectors, currentColor casing, unquoted Segoe UI) comes from Tailwind's own minified output, so there is nothing to fix in this file. Adding src/gateway/static/dashboard/assets/** to .stylelintignore keeps the lint signal meaningful instead of teaching everyone to scroll past ~60 expected errors. The authored source in web/src/styles/globals.css stays covered.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/gateway/static/dashboard/assets/index-C_gHYR5I.css` at line 1, Update the
Stylelint ignore configuration to exclude generated dashboard assets matching
src/gateway/static/dashboard/assets/**, rather than modifying the minified
Tailwind output. Keep authored styles such as web/src/styles/globals.css
included in linting.

Source: Linters/SAST tools

web/src/pages/UsagePage.test.tsx (1)

389-407: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Class-string assertions will drift if the collapse styling changes.

aria-expanded already proves the behavior; the hidden/flex checks pin the test to Tailwind internals, so a switch to hidden md:flex or a data-* attribute would break it for no functional reason. A data-state (or similar) hook on the region would make this assertion intent-revealing and style-agnostic. Totally optional, and the comment explaining the jsdom limitation is appreciated.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/pages/UsagePage.test.tsx` around lines 389 - 407, Make the filter
toggle test assert the region’s semantic state rather than Tailwind class names.
Update the UsagePage implementation and test around the “Add filter” toggle to
expose and verify a stable data-state (or equivalent) hook that changes with
aria-expanded, while preserving the collapsed and expanded behavior.
web/src/pages/ActivityPage.test.tsx (1)

407-420: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Two new tests share the same forgiving assertion shape. Both use listCalls(calls).some(url => !url.includes(...)), which is satisfied by any recorded request lacking the parameter rather than by the request the interaction produced, so each could pass for the wrong reason.

  • web/src/pages/ActivityPage.test.tsx#L407-L420: assert on listCalls(calls).at(-1) not containing model=gpt-4o after the chip is removed.
  • web/src/pages/ActivityPage.test.tsx#L422-L433: assert on listCalls(calls).at(-1) not containing start_date after clicking "All".
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/pages/ActivityPage.test.tsx` around lines 407 - 420, Strengthen the
request assertions in the tests around the chip-removal case at
web/src/pages/ActivityPage.test.tsx:407-420 and the “All” time-range case at
web/src/pages/ActivityPage.test.tsx:422-433 by checking listCalls(calls).at(-1)
after the interaction, ensuring the latest request omits model=gpt-4o and
start_date respectively instead of matching any earlier request.
web/src/pages/UsagePage.tsx (1)

554-560: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Manual refresh leaves the comparison and model-suggestion queries behind in custom mode.

In preset mode setStartDate shifts the query keys, so previous and modelSuggest refetch on their own. In custom mode the keys are unchanged, so pressing Refresh updates the tiles while the "vs previous" delta and the model typeahead keep serving whatever was cached. Refetching them too keeps the whole panel consistent.

♻️ Suggested tweak
   const refresh = () => {
     if (!customMode) {
       setStartDate(isoAgo(preset.seconds ?? 0));
     }
     void summary.refetch();
     void contextSummary.refetch();
+    if (previousFilters !== null) void previous.refetch();
+    void modelSuggest.refetch();
   };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/pages/UsagePage.tsx` around lines 554 - 560, Update the refresh
function to also refetch the comparison/previous and model-suggestion queries
when manual refresh runs, including custom mode. Reuse the existing query
references and preserve the current summary and context-summary refetch
behavior.
web/src/pages/ActivityPage.tsx (1)

347-363: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Share the option-label lookup for filter chips.

ActivityPage.tsx has labelFrom, UsagePage.tsx has labelFor, and both are the same tiny lookup. FilterSelect.tsx has a labelFor too, but it’s scoped inside that component. A small exported utility keeps these labels from drifting silently later; keep the existing helpers for any internal-only consumers if needed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/pages/ActivityPage.tsx` around lines 347 - 363, Extract the shared
option-label lookup used by ActivityPage’s labelFrom and UsagePage’s labelFor
into a small exported utility, then update both pages’ filter-chip label
resolution to reuse it. Preserve the fallback to the raw value and retain
FilterSelect’s component-scoped helper unless it can safely consume the shared
utility without unrelated changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/gateway/static/dashboard/assets/FilterChips-Wo8I8zEd.js`:
- Line 1: Restore the missing named Z export in the generated module, mapping it
to the existing we function that implements the expected (je, ke) lookup.
Regenerate the dashboard assets from source when possible, then verify
UsagePage-BECQyz4e.js can import Z as Ze and invoke it successfully.

In `@web/src/pages/UsagePage.tsx`:
- Around line 554-560: Update the refresh handler in web/src/pages/UsagePage.tsx
lines 554-560 to also refetch previous when previousFilters is not null and
refetch modelSuggest. Update the refresh handler in
web/src/pages/ActivityPage.tsx lines 444-455 to also refetch modelSummary,
ensuring all window-scoped queries refresh together.

---

Nitpick comments:
In `@src/gateway/static/dashboard/assets/index-C_gHYR5I.css`:
- Line 1: Update the Stylelint ignore configuration to exclude generated
dashboard assets matching src/gateway/static/dashboard/assets/**, rather than
modifying the minified Tailwind output. Keep authored styles such as
web/src/styles/globals.css included in linting.

In `@web/src/components/ActivityTimeline.tsx`:
- Around line 19-24: Memoize the Recharts subtree in ActivityTimeline using a
useMemo keyed by the stable series and bucket inputs, keeping the chart data and
BarChart element inside that memoized value so pointer-driven setSel updates do
not rebuild it. Replace the inline ResponsiveContainer block inside the
role="img" wrapper with the memoized chart element, while preserving the
existing chart configuration and drag behavior.

In `@web/src/components/FilterChips.tsx`:
- Around line 52-61: Add a visible keyboard focus treatment to the chip removal
button in FilterChips, using focus-visible styling consistent with the existing
design tokens while preserving its current hover appearance and accessibility
attributes.

In `@web/src/components/ui.tsx`:
- Around line 146-174: Update useDisplayClock and its RefreshButton call sites
so the clock does not create an interval when updatedAt is falsy and no label
renders. Accept a nullable or disabled interval, preserve the existing
visibility and cleanup behavior when enabled, and ensure each RefreshButton
passes the disabled state for missing updatedAt.

In `@web/src/lib/timeRange.test.ts`:
- Around line 106-136: Pin the locale used by the test environment so date
assertions are deterministic instead of relying on ambient LANG/LC_ALL settings.
Apply this to web/src/lib/timeRange.test.ts lines 106-136 and use the same
pinned locale for web/src/components/ActivityTimeline.test.tsx lines 43-49;
update the shared Vitest setup or locale-explicit expectations as appropriate,
without changing production formatDay behavior.

In `@web/src/lib/timeRange.ts`:
- Line 28: Update the time-range type and related absent-value handling to use
undefined instead of null, including the unbounded preset marker and the
no-series return in the relevant time-range symbols. Keep valid numeric values
unchanged and propagate the type change through local consumers without
introducing null checks.

In `@web/src/pages/ActivityPage.test.tsx`:
- Around line 407-420: Strengthen the request assertions in the tests around the
chip-removal case at web/src/pages/ActivityPage.test.tsx:407-420 and the “All”
time-range case at web/src/pages/ActivityPage.test.tsx:422-433 by checking
listCalls(calls).at(-1) after the interaction, ensuring the latest request omits
model=gpt-4o and start_date respectively instead of matching any earlier
request.

In `@web/src/pages/ActivityPage.tsx`:
- Around line 347-363: Extract the shared option-label lookup used by
ActivityPage’s labelFrom and UsagePage’s labelFor into a small exported utility,
then update both pages’ filter-chip label resolution to reuse it. Preserve the
fallback to the raw value and retain FilterSelect’s component-scoped helper
unless it can safely consume the shared utility without unrelated changes.

In `@web/src/pages/OverviewPage.tsx`:
- Around line 146-153: Update the isRefreshing calculation in OverviewPage to
include previous.isFetching alongside the existing query-fetching flags, so
refresh remains active until the previous-period data completes.

In `@web/src/pages/UsagePage.test.tsx`:
- Around line 389-407: Make the filter toggle test assert the region’s semantic
state rather than Tailwind class names. Update the UsagePage implementation and
test around the “Add filter” toggle to expose and verify a stable data-state (or
equivalent) hook that changes with aria-expanded, while preserving the collapsed
and expanded behavior.

In `@web/src/pages/UsagePage.tsx`:
- Around line 554-560: Update the refresh function to also refetch the
comparison/previous and model-suggestion queries when manual refresh runs,
including custom mode. Reuse the existing query references and preserve the
current summary and context-summary refetch behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: d943fc38-67b5-4c60-ac6f-eba85ee19e54

📥 Commits

Reviewing files that changed from the base of the PR and between a660931 and b6340b4.

📒 Files selected for processing (45)
  • src/gateway/static/dashboard/assets/ActivityPage-BFwaR2GB.js
  • src/gateway/static/dashboard/assets/ActivityPage-RaXV2Jg6.js
  • src/gateway/static/dashboard/assets/AliasesPage-Q7olOjhL.js
  • src/gateway/static/dashboard/assets/BudgetsPage-lTuTS-F9.js
  • src/gateway/static/dashboard/assets/ConfirmDialog-CWW3AYpK.js
  • src/gateway/static/dashboard/assets/DataTable-Bmn_eYSV.js
  • src/gateway/static/dashboard/assets/DocsPage-BAH_T1JY.js
  • src/gateway/static/dashboard/assets/Field-Dwp6u3t8.js
  • src/gateway/static/dashboard/assets/FilterChips-Wo8I8zEd.js
  • src/gateway/static/dashboard/assets/KeysPage-bUjPPYgf.js
  • src/gateway/static/dashboard/assets/ModelScopeControl-DmfR9Ylv.js
  • src/gateway/static/dashboard/assets/ModelsPage-BMtuCmJv.js
  • src/gateway/static/dashboard/assets/OverviewPage-CX9hXHG1.js
  • src/gateway/static/dashboard/assets/OverviewPage-Deh5RtuT.js
  • src/gateway/static/dashboard/assets/ProvidersPage-BRR1ZEsl.js
  • src/gateway/static/dashboard/assets/ProvidersPage-DQdazSS9.js
  • src/gateway/static/dashboard/assets/SettingsPage-CJyO6zWH.js
  • src/gateway/static/dashboard/assets/TablePagination-r1AimUKf.js
  • src/gateway/static/dashboard/assets/ToolsGuardrailsPage-1cTNaDRp.js
  • src/gateway/static/dashboard/assets/UsagePage-BD35XJZn.js
  • src/gateway/static/dashboard/assets/UsagePage-BECQyz4e.js
  • src/gateway/static/dashboard/assets/UsersPage-CAU0FXTt.js
  • src/gateway/static/dashboard/assets/charts-DIsd9m--.js
  • src/gateway/static/dashboard/assets/heroui-BI50yK5B.js
  • src/gateway/static/dashboard/assets/heroui-DZj66Arc.js
  • src/gateway/static/dashboard/assets/index-73ln6Ld4.css
  • src/gateway/static/dashboard/assets/index-CGnfXTEF.js
  • src/gateway/static/dashboard/assets/index-CI2beHfq.js
  • src/gateway/static/dashboard/assets/index-C_gHYR5I.css
  • src/gateway/static/dashboard/assets/recharts-C4kRDmvS.js
  • src/gateway/static/dashboard/assets/tableSelection-9hV37uhu.js
  • src/gateway/static/dashboard/index.html
  • web/src/components/ActivityTimeline.test.tsx
  • web/src/components/ActivityTimeline.tsx
  • web/src/components/FilterChips.test.tsx
  • web/src/components/FilterChips.tsx
  • web/src/components/ui.test.tsx
  • web/src/components/ui.tsx
  • web/src/lib/timeRange.test.ts
  • web/src/lib/timeRange.ts
  • web/src/pages/ActivityPage.test.tsx
  • web/src/pages/ActivityPage.tsx
  • web/src/pages/OverviewPage.tsx
  • web/src/pages/UsagePage.test.tsx
  • web/src/pages/UsagePage.tsx
💤 Files with no reviewable changes (5)
  • src/gateway/static/dashboard/assets/ActivityPage-RaXV2Jg6.js
  • src/gateway/static/dashboard/assets/ProvidersPage-BRR1ZEsl.js
  • src/gateway/static/dashboard/assets/UsagePage-BD35XJZn.js
  • src/gateway/static/dashboard/assets/OverviewPage-Deh5RtuT.js
  • src/gateway/static/dashboard/assets/index-CI2beHfq.js

@@ -0,0 +1 @@
import{j as e}from"./tanstack-query-1t81HyiD.js";import{r as y}from"./react-dgEcD0HR.js";import{B as k,S as V,$ as G,h as H,i as J}from"./heroui-BI50yK5B.js";import{R as Q,B as ee,X as te,T as ne,a as re}from"./recharts-C4kRDmvS.js";const P=3600,x=86400,ae=366,pe=[{key:"1h",label:"Last hour",seconds:P,bucket:"hour"},{key:"24h",label:"24h",seconds:x,bucket:"hour"},{key:"7d",label:"7d",seconds:7*x,bucket:"day"},{key:"30d",label:"30d",seconds:30*x,bucket:"day"},{key:"90d",label:"90d",seconds:90*x,bucket:"day"},{key:"12mo",label:"12mo",seconds:ae*x,bucket:"day"}],ve="30d",je=[{key:"1h",label:"1h",seconds:P,bucket:"hour"},{key:"24h",label:"24h",seconds:x,bucket:"hour"},{key:"7d",label:"7d",seconds:7*x,bucket:"day"},{key:"30d",label:"30d",seconds:30*x,bucket:"day"},{key:"all",label:"All",seconds:null,bucket:"day"}],ye="24h",ke="custom";function we(n,r){return n.find(a=>a.key===r)}function Te(n,r=Date.now()){return new Date(r-n*1e3).toISOString()}function se(n){return(n==="hour"?P:x)*1e3}function Ne(n,r,a=Date.now()){const i=new Date(n).getTime();return(r?new Date(r).getTime():a)-i<=x*1e3?"hour":"day"}function ie(n,r,a,i){if(n.length===0)return null;const c=Math.max(0,Math.min(r,a)),l=Math.min(n.length-1,Math.max(r,a)),o=new Date(n[c]).getTime(),m=new Date(n[l]).getTime()+se(i);return{startIso:new Date(o).toISOString(),endIso:new Date(m).toISOString()}}function oe(n,r,a){const i=n.length;if(i===0)return{startIndex:0,endIndex:0};const c=n.map(m=>new Date(m).getTime());let l=0;if(r){const m=new Date(r).getTime();for(let d=0;d<i;d++)c[d]<=m&&(l=d)}let o=i-1;if(a){const m=new Date(a).getTime();o=0;for(let d=0;d<i;d++)c[d]<m&&(o=d)}return o<l&&(o=l),{startIndex:l,endIndex:o}}function j(n,r){return new Date(n).toLocaleDateString(void 0,{month:"short",day:"numeric",...r?{year:"numeric"}:{},timeZone:"UTC"})}function le(n,r){if(!n&&!r)return"All time";if(n&&!r)return`Since ${j(new Date(n).getTime(),!0)}`;if(!n&&r)return`Up to ${j(new Date(r).getTime()-1,!0)}`;const a=new Date(n).getTime(),i=new Date(r).getTime()-1;if(j(a,!0)===j(i,!0))return j(a,!0);const l=new Date(a).getUTCFullYear()===new Date(i).getUTCFullYear();return`${j(a,!l)} – ${j(i,!0)}`}const ce="var(--otari-brand)",de=10,ue=22;function B(n,r){const a=new Date(n);return Number.isNaN(a.getTime())?n:r==="hour"?a.toLocaleTimeString(void 0,{hour:"2-digit",minute:"2-digit",timeZone:"UTC"}):a.toLocaleDateString(void 0,{month:"short",day:"numeric",timeZone:"UTC"})}function me({active:n,label:r,payload:a,bucket:i}){var l;const c=(l=a==null?void 0:a[0])==null?void 0:l.value;return!n||typeof c!="number"||typeof r!="string"?null:e.jsxs("div",{className:"rounded-md border border-[var(--otari-line)] bg-[var(--otari-surface)] px-2 py-1 text-xs shadow-sm",children:[e.jsxs("div",{className:"text-[var(--otari-muted)]",children:[B(r,i)," · UTC"]}),e.jsxs("div",{className:"font-medium tabular-nums text-[var(--otari-ink)]",children:[c.toLocaleString()," requests"]})]})}const he="group top-1/2 h-full w-3 cursor-ew-resize outline-none";function L({index:n,label:r}){return e.jsxs(J,{index:n,"aria-label":r,className:`${he} pointer-events-auto`,children:[e.jsx("span",{"aria-hidden":!0,className:"absolute inset-y-0 left-1/2 w-0.5 -translate-x-1/2 bg-[var(--otari-brand)]"}),e.jsx("span",{"aria-hidden":!0,className:"absolute left-1/2 top-1/2 h-6 w-2 -translate-x-1/2 -translate-y-1/2 rounded-full border border-[var(--otari-brand)] bg-[var(--otari-surface)] shadow-sm group-focus-visible:ring-2 group-focus-visible:ring-[var(--otari-brand)]"})]})}function Se({presets:n,extentKey:r,onPreset:a,onSelectRange:i,onSelectFull:c,series:l,bucket:o,windowStart:m,windowEnd:d,loading:F=!1,ariaLabel:Y="Request volume over the selected window",action:O}){const R=l.map(t=>t.bucketStart),W=l.map(t=>({x:t.bucketStart,requests:t.requests})),s=l.length,q=le(m,d),$=()=>{if(s===0)return[0,1];const{startIndex:t,endIndex:f}=oe(R,m,d);return[t,f+1]},[N,w]=y.useState($),S=y.useRef(!1);y.useEffect(()=>{S.current||w($())},[m,d,s]);const T=([t,f])=>{if(S.current=!1,s===0)return;const v=Math.max(0,Math.min(s-1,Math.floor(t+1e-6))),h=Math.max(v,Math.min(s-1,Math.ceil(f-1e-6)-1));if(v===0&&h===s-1){c(),w([0,s]);return}const u=ie(R,v,h,o);u&&i(u.startIso,u.endIso)},E=n.findIndex(t=>t.key===r),D=E>=0?n[E+1]:void 0,[g,b]=N,M=g<=.01&&b>=s-.01,I=t=>{const f=Math.max(1,Math.min(s,t)),v=(g+b)/2;let h=v-f/2,u=v+f/2;h<0&&(u-=h,h=0),u>s&&(h-=u-s,u=s);const z=[Math.max(0,h),Math.min(s,u)];w(z),T(z)},X=()=>{if(M){D&&a(D);return}I((b-g)*2)},K=()=>I((b-g)/2),C=y.useRef(null),p=y.useRef(null),Z=t=>{if(!p.current||!C.current)return;const f=C.current.getBoundingClientRect().width;if(f<=0)return;const v=(t.clientX-p.current.x)/f*s,h=p.current.sel[1]-p.current.sel[0],u=Math.max(0,Math.min(s-h,p.current.sel[0]+v));w([u,u+h])},A=s?Math.min(g,b)/s*100:0,U=s?Math.max(g,b)/s*100:100,_=!M;return e.jsxs("div",{className:"flex flex-col gap-2",children:[e.jsxs("div",{className:"flex flex-wrap items-center gap-2",children:[n.map(t=>e.jsx(k,{size:"sm",variant:r===t.key?"primary":"outline",onPress:()=>a(t),children:t.label},t.key)),e.jsxs("div",{className:"ml-auto flex items-center gap-3",children:[e.jsxs("span",{className:"text-xs text-[var(--otari-muted)]",children:["Showing ",q," · UTC"]}),O]})]}),e.jsxs("div",{className:"rounded-xl border border-[var(--otari-line)] bg-[var(--otari-surface)] p-2",children:[e.jsxs("div",{className:"flex items-center justify-between gap-2 px-1 pb-1",children:[e.jsxs("span",{className:"text-[11px] font-medium uppercase tracking-wide text-[var(--otari-muted)]",children:["Requests / ",o==="hour"?"hour":"day"]}),e.jsxs("div",{className:"flex items-center gap-1.5",children:[e.jsx("span",{className:"hidden text-[11px] text-[var(--otari-muted)] sm:inline",children:"drag the edges to zoom · the bottom strip to pan"}),e.jsx(k,{size:"sm",variant:"ghost",isIconOnly:!0,"aria-label":"Zoom in",isDisabled:s===0,onPress:K,children:e.jsx("svg",{viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",className:"h-4 w-4","aria-hidden":"true",children:e.jsx("path",{d:"M12 5v14M5 12h14",strokeLinecap:"round"})})}),e.jsx(k,{size:"sm",variant:"ghost",isIconOnly:!0,"aria-label":"Zoom out",isDisabled:s===0||M&&!D,onPress:X,children:e.jsx("svg",{viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",className:"h-4 w-4","aria-hidden":"true",children:e.jsx("path",{d:"M5 12h14",strokeLinecap:"round"})})}),_?e.jsx(k,{size:"sm",variant:"ghost",onPress:()=>T([0,s]),children:"Reset"}):null]})]}),F&&s===0?e.jsx("div",{className:"flex h-[90px] items-center justify-center",children:e.jsx(V,{size:"sm"})}):s===0?e.jsx("div",{className:"flex h-[90px] items-center justify-center text-xs text-[var(--otari-muted)]",children:"No activity in this range."}):e.jsxs("div",{ref:C,className:"relative w-full",children:[e.jsx("div",{role:"img","aria-label":Y,className:"w-full touch-pan-y select-none",children:e.jsx(Q,{width:"100%",height:90,children:e.jsxs(ee,{data:W,margin:{top:4,right:0,left:0,bottom:0},children:[e.jsx(te,{dataKey:"x",tickLine:!1,axisLine:!1,interval:"preserveStartEnd",minTickGap:40,tickFormatter:t=>B(t,o),tick:{fontSize:10,fill:"var(--otari-muted)"}}),e.jsx(ne,{cursor:{fill:"var(--otari-line)",opacity:.35},content:e.jsx(me,{bucket:o})}),e.jsx(re,{dataKey:"requests",fill:ce,radius:[2,2,0,0],isAnimationActive:!1})]})})}),_?e.jsxs(e.Fragment,{children:[e.jsx("div",{"aria-hidden":!0,className:"pointer-events-none absolute inset-y-0 left-0 bg-[var(--otari-bg)] opacity-70",style:{width:`${A}%`}}),e.jsx("div",{"aria-hidden":!0,className:"pointer-events-none absolute inset-y-0 right-0 bg-[var(--otari-bg)] opacity-70",style:{width:`${100-U}%`}})]}):null,e.jsx("div",{"aria-hidden":!0,className:"absolute bottom-0 z-[1] cursor-grab touch-none rounded bg-[var(--otari-brand)]/10 hover:bg-[var(--otari-brand)]/20 active:cursor-grabbing",style:{left:`${A}%`,width:`${Math.max(0,U-A)}%`,height:ue},onPointerDown:t=>{t.stopPropagation(),t.preventDefault(),S.current=!0,p.current={x:t.clientX,sel:[Math.min(g,b),Math.max(g,b)]},t.currentTarget.setPointerCapture(t.pointerId)},onPointerMove:Z,onPointerUp:t=>{t.currentTarget.releasePointerCapture(t.pointerId),p.current=null,T(N)},onPointerCancel:()=>{p.current=null,T(N)}}),e.jsx(G,{"aria-label":"Selected time range",minValue:0,maxValue:s,step:1/de,value:N,onChange:t=>{S.current=!0,Array.isArray(t)&&t.length===2&&w([t[0],t[1]])},onChangeEnd:t=>{Array.isArray(t)&&t.length===2&&T([t[0],t[1]])},className:"pointer-events-none absolute inset-0 z-[2]",children:e.jsxs(H,{className:"pointer-events-none relative h-full w-full",children:[e.jsx(L,{index:0,label:"Window start"}),e.jsx(L,{index:1,label:"Window end"})]})})]})]})]})}function De({chips:n,children:r,onClearAll:a}){const[i,c]=y.useState(!1),l=y.useId();return e.jsxs("div",{className:"flex flex-col gap-2",children:[e.jsxs("div",{className:"flex flex-wrap items-center gap-2",children:[e.jsx(k,{size:"sm",variant:"outline",onPress:()=>c(o=>!o),"aria-expanded":i,"aria-controls":l,children:i?"Done":"Add filter"}),n.map(o=>e.jsxs("span",{className:"inline-flex items-center gap-1 rounded-full border border-[var(--otari-line)] bg-[var(--otari-brand-tint)] py-0.5 pl-2.5 pr-1 text-xs text-[var(--otari-brand-dark)]",children:[e.jsxs("span",{className:"text-[var(--otari-muted)]",children:[o.label,":"]}),e.jsx("span",{className:"font-medium",children:o.value}),e.jsx("button",{type:"button",onClick:o.onClear,"aria-label":`Remove ${o.label} filter`,className:"ml-0.5 inline-flex h-4 w-4 items-center justify-center rounded-full text-[var(--otari-muted)] hover:bg-[var(--otari-line)] hover:text-[var(--otari-ink)]",children:e.jsx("svg",{viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2.5",className:"h-3 w-3","aria-hidden":"true",children:e.jsx("path",{d:"M6 6l12 12M18 6L6 18",strokeLinecap:"round"})})})]},o.key)),n.length>0&&a?e.jsx(k,{size:"sm",variant:"ghost",onPress:a,children:"Clear all"}):null]}),e.jsx("div",{id:l,className:i?"flex flex-wrap items-end gap-3":"hidden",children:r})]})}export{Se as A,ke as C,De as F,ve as U,je as a,ye as b,Ne as c,pe as d,we as f,Te as i};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Restore the missing Z export before shipping the generated bundle.

UsagePage-BECQyz4e.js imports Z as Ze and immediately calls Ze(je,ke), but this asset exports the matching function we only as f; no Z export exists. The Usage page will therefore fail during module initialization with TypeError: Ze is not a function. Regenerate the dashboard assets from the source, or restore the expected named export, then verify the generated import/export contract.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/gateway/static/dashboard/assets/FilterChips-Wo8I8zEd.js` at line 1,
Restore the missing named Z export in the generated module, mapping it to the
existing we function that implements the expected (je, ke) lookup. Regenerate
the dashboard assets from source when possible, then verify
UsagePage-BECQyz4e.js can import Z as Ze and invoke it successfully.

Comment thread web/src/pages/UsagePage.tsx
njbrake and others added 2 commits July 28, 2026 18:23
…-picker

# Conflicts:
#	src/gateway/static/dashboard/assets/ActivityPage-Byt0czqE.js
#	src/gateway/static/dashboard/assets/AliasesPage-C8gXJ6jx.js
#	src/gateway/static/dashboard/assets/AliasesPage-CX3eAAXN.js
#	src/gateway/static/dashboard/assets/AliasesPage-wZMHXIpE.js
#	src/gateway/static/dashboard/assets/BudgetsPage-p9qxuU3G.js
#	src/gateway/static/dashboard/assets/ConfirmDialog-D_mZT4k-.js
#	src/gateway/static/dashboard/assets/ConfirmDialog-Dnahrq6G.js
#	src/gateway/static/dashboard/assets/ConfirmDialog-nzYO6eyy.js
#	src/gateway/static/dashboard/assets/DocsPage-C8AK643M.js
#	src/gateway/static/dashboard/assets/DocsPage-D7XUEYdI.js
#	src/gateway/static/dashboard/assets/DocsPage-gss9QTdF.js
#	src/gateway/static/dashboard/assets/KeysPage-DZwI8ovn.js
#	src/gateway/static/dashboard/assets/ModelScopeControl-DY7OwwJM.js
#	src/gateway/static/dashboard/assets/ModelScopeControl-DedlZSDh.js
#	src/gateway/static/dashboard/assets/ModelScopeControl-L-_tKOtj.js
#	src/gateway/static/dashboard/assets/ModelsPage-BynwkYFh.js
#	src/gateway/static/dashboard/assets/ModelsPage-CAoQKgS_.js
#	src/gateway/static/dashboard/assets/ModelsPage-Cw8Xv_w2.js
#	src/gateway/static/dashboard/assets/SettingsPage-0IuzwARi.js
#	src/gateway/static/dashboard/assets/TablePagination-ByewydW5.js
#	src/gateway/static/dashboard/assets/TablePagination-Chra1w0V.js
#	src/gateway/static/dashboard/assets/TablePagination-Cz-ztZ68.js
#	src/gateway/static/dashboard/assets/ToolsGuardrailsPage-KUSgM6cX.js
#	src/gateway/static/dashboard/assets/UsersPage-DPrtdYVu.js
#	src/gateway/static/dashboard/assets/index-C6t-OIZc.css
#	src/gateway/static/dashboard/index.html
#	web/src/components/ui.test.tsx
#	web/src/components/ui.tsx
#	web/src/pages/OverviewPage.tsx
#	web/src/pages/UsagePage.tsx
CodeRabbit review finding on #445: the manual refresh handlers enumerated a
subset of the queries derived from the current window, so in custom mode
(where the query keys do not change) the model typeahead and the
period-over-period baseline silently stayed on cached data after an explicit
refresh. Usage now also refetches the model-suggestion summary and, when a
comparison window exists, the previous-period summary; Activity also refetches
its model-suggestion summary. The previous-period refetch is guarded because
refetch() ignores `enabled` and would otherwise fire a pointless request.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@njbrake
njbrake deployed to integration-tests July 28, 2026 18:24 — with GitHub Actions Active
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Rework Usage page time-range selection: "All" means 30 days, and the custom picker only filters if you enter a time

1 participant