Skip to content

fix(usage): make 7/30 day toggle work + redesign with Fluent SelectorBar#322

Merged
shanselman merged 1 commit into
openclaw:masterfrom
bkudiess:fix/usage-page-time-range-toggle
May 12, 2026
Merged

fix(usage): make 7/30 day toggle work + redesign with Fluent SelectorBar#322
shanselman merged 1 commit into
openclaw:masterfrom
bkudiess:fix/usage-page-time-range-toggle

Conversation

@bkudiess
Copy link
Copy Markdown
Contributor

@bkudiess bkudiess commented May 12, 2026

Problem

The 7 Days / 30 Days toggle on the Usage page only swapped button styles — it never re-fetched usage data. The page was effectively stuck on whatever period was loaded at startup. Initialize() also hardcoded RequestUsageCostAsync(30) while XAML highlighted the 7 Days button, so the UI label and the data on screen disagreed on first load.

What changed

Functional fix (the bug)

  • Wire the period selection to RequestUsageCostAsync(days), with an early-return guard so re-clicking the active period doesn't fire a duplicate request.
  • Initialize() now requests the same period the UI is highlighting.

UI redesign (Fluent guidance)

  • Replace the two Button controls with a SelectorBar — the official Fluent control for "switch between a small number of views/filters". The selection underline is preserved through hover/focus/press, which AccentButtonStyle was losing on PointerOver.
  • Move Provider Breakdown above the SelectorBar so it's visually clear the selector only scopes the Daily Cost data below it (provider status is real-time, not period-based).
  • Wrap both Provider Breakdown and Daily Cost in full-width Fluent data cards so the page reads as three consistent cards (Total Cost / Providers / Daily Cost) instead of a mix of cards and floating headers.
  • Co-locate the SelectorBar with the Daily Cost label inside the same card header row, so the filter is unambiguously scoped to the data in that card.
  • Add a ProgressRing + "Loading providers…" skeleton inside the Provider Breakdown card so the first-load gateway round-trip feels intentional.
  • Add a "No providers configured" empty-state for the Provider Breakdown card so a zero-provider response doesn't render as a blank section.

Correctness fixes (Hanselman dual-model review)

  • UpdateUsage no longer writes TotalCostText / TokenCountText. Those fields are now owned exclusively by UpdateUsageCost (the period-scoped data the toggle controls). Previously both methods raced on every load — whichever response arrived last clobbered the hero numbers with the wrong period (or all-time).
  • Initialize only applies cached LastUsageCost when its Days field matches the current selection. Avoids briefly rendering 30-day data while the SelectorBar reads "7 Days" when the user returns to the page.

Code clarity

  • Rename misleading ProviderRow properties: RequestsPlan, TokensUsage, CostStatus. The bindings now describe what they actually show (plan tier, current rate-limit window usage, error/status text).

Localization

  • Rename resw keys Period7DaysButton.Content / Period30DaysButton.ContentPeriod7DaysItem.Text / Period30DaysItem.Text across all 5 languages (en-us, fr-fr, zh-cn, zh-tw, nl-nl) to match SelectorBarItem.Text.
  • Add UsagePage_ProviderLoading.Text and UsagePage_NoProviders.Text in all 5 languages.

Before / after

Before After
7 Days / 30 Days = AccentButton + DefaultButton; click only restyles SelectorBar with underline indicator; click refetches usage.cost {days}
Selector floats above an unrelated "Daily Cost" header Selector lives inside the Daily Cost card header
Provider Breakdown is a single bordered row below the selector — looks period-scoped Provider Breakdown is a full-width card above the selector with loading + empty states
Hero Total Cost / Tokens race between two async sources UpdateUsageCost is the sole owner of period-scoped totals

Validation

  • ./build.ps1 succeeds for all 4 projects (Shared, Cli, WinNodeCli, WinUI).
  • OpenClaw.Shared.Tests: 1455 passed, 0 failed, 25 skipped.
  • OpenClaw.Tray.Tests: 959 passed, 0 failed.
  • Manual smoke: launched tray, opened Usage page, toggled 7 ↔ 30 days, observed Daily Cost list refresh, observed selection underline persist through hover.

The 7 Days / 30 Days buttons on the Usage page only swapped button styles —
they never re-fetched usage data, so the page was effectively stuck on whatever
period was loaded at startup. Initialize() also hardcoded RequestUsageCostAsync(30)
while the XAML highlighted the 7 Days button, so the UI label and the data on
screen disagreed on first load.

This change fixes the bug and refreshes the page layout to match Fluent guidance.

Functional fix:
- Wire the period selection to RequestUsageCostAsync(days), with an early-return
  guard so re-clicking the active period doesn't fire a duplicate request.
- Initialize() now requests the same period the UI is highlighting.

UI redesign:
- Replace the two Button controls with a SelectorBar (the official Fluent
  control for "switch between a small number of views/filters"). Preserves the
  selection underline through hover/focus/press, which AccentButtonStyle was
  losing on PointerOver.
- Move Provider Breakdown above the SelectorBar so it's visually clear that
  the selector only scopes the Daily Cost data below it (provider status is
  real-time, not period-based).
- Wrap both Provider Breakdown and Daily Cost in full-width Fluent data cards
  so the page reads as three consistent cards (Total Cost / Providers / Daily
  Cost) instead of a mix of cards and floating headers.
- Co-locate the SelectorBar with the Daily Cost label inside the same card
  header row, so the filter is unambiguously scoped to the data in that card.
- Add a ProgressRing + "Loading providers…" skeleton inside the Provider
  Breakdown card so the first-load gateway round-trip feels intentional.
- Add a "No providers configured" empty-state for the Provider Breakdown card
  so a zero-provider response doesn't render as a blank section.

Correctness fixes (Hanselman dual-model review):
- UpdateUsage no longer writes TotalCostText / TokenCountText. Those fields are
  now owned exclusively by UpdateUsageCost (the period-scoped data the toggle
  controls). Previously both methods raced on every load — whichever response
  arrived last clobbered the hero numbers with the wrong period (or all-time).
- Initialize only applies cached LastUsageCost when its Days field matches the
  current selection. Avoids briefly rendering 30-day data while the SelectorBar
  reads "7 Days" when the user returns to the page.

Code clarity:
- Rename misleading ProviderRow properties: Requests -> Plan, Tokens -> Usage,
  Cost -> Status. The bindings now describe what they actually show (plan tier,
  current rate-limit window usage, error/status text).

Localization:
- Rename resw keys Period7DaysButton.Content / Period30DaysButton.Content to
  Period7DaysItem.Text / Period30DaysItem.Text across all 5 languages
  (en-us, fr-fr, zh-cn, zh-tw, nl-nl) to match SelectorBarItem.Text.
- Add UsagePage_ProviderLoading.Text and UsagePage_NoProviders.Text in all
  5 languages.

Validation:
- build.ps1 succeeds for all 4 projects (Shared, Cli, WinNodeCli, WinUI).
- OpenClaw.Shared.Tests: 1455 passed, 0 failed, 25 skipped.
- OpenClaw.Tray.Tests: 959 passed, 0 failed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@shanselman shanselman merged commit 650f220 into openclaw:master May 12, 2026
23 checks passed
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.

2 participants