Skip to content

feat(cluster): cluster plan usage card + Usage tab [#1297] - #1559

Open
dawsontoth wants to merge 7 commits into
stagefrom
claude/cluster-usage-metrics-383596
Open

feat(cluster): cluster plan usage card + Usage tab [#1297]#1559
dawsontoth wants to merge 7 commits into
stagefrom
claude/cluster-usage-metrics-383596

Conversation

@dawsontoth

@dawsontoth dawsontoth commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Cluster usage-vs-plan display (#1297)

Adds a Plan usage card to the managed cluster overview and a Usage tab, backed by the central-manager endpoint from HarperFast/central-manager#503 (GET /Cluster/:id/usage, approved and merged).

Closes #1297.

Verified against live data

The endpoint is deployed to dev + stage, and both surfaces render real cluster data (screenshots below): actual plan name and renewal date, real limits, and real storage sourced from InstanceMonitoring.

Model

Quota is enforced per region, never cluster-wide (exhaustion, renewal, billing and the signed license ceiling are all per-region), so the UI meters per region rather than summing into one misleading bar.

Overview card

  • Single region → the four headline meters (Reads / Writes / Storage / Compute).
  • Multiple regions → the single most-constrained region×metric, so a hot region can't hide behind an average.

Usage tab

  • One collapsible card per region, with status driving the header:
    • active → "renews {date}"
    • exhausted (burned through a counter cap → re-billing) → amber "Cycle exhausted"
    • lapsed (expired / not renewed, under cap) → "No active license", collapsed by default
  • All 8 metered metrics as used/limit bars.
  • Rate limits (per-minute counts and bandwidth) + per-instance resources in a single shared "Plan limits & resources" card when uniform across regions; per-region when they differ.
  • Region ids and plan id shown subtly (monospace, right-aligned) in the card headers.

Meter states

Finite ceiling → used/limit bar, amber at ≥90%. Plan limit -1 → hatched "Unlimited". Unresolved plan → "—" (never the reassuring "Unlimited" — this is a billing surface). A zero/negative limit can't produce NaN/Infinity.

Completeness

Cross-checked field-by-field against the new-cluster plan modal: everything it shows is here, plus used-vs-limit rather than limits alone. Only Cloud Instance Type is out of scope (needs a CM field; dedicated/GPU only).

Tests

22 new cases (vitest, jsdom) covering the meter's three ceiling states and guards, the card's single- vs multi-region branches and its four hide conditions, and the tab's collapse/expand, status badges, shared-vs-per-region plan info, and empty/loading states. Full cluster-feature suite: 105 passing. tsc, oxlint, dprint clean.

Follow-up

Regenerate the SDK types from #503's OpenAPI to drop the hand-typed response (works fine as-is; a regen is a large unrelated diff, so it belongs on its own).

🤖 Generated with Claude Code

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a new cluster usage monitoring feature, adding a "Usage" tab and a "Plan usage" overview card to the cluster home page. It includes the UsageMeter component to render progress bars for metered metrics, alongside mock data for various usage metrics, rate limits, and per-instance resources. Feedback on the PR suggests making the UsageMeter component more robust by guarding against division by zero and treating negative limits as unlimited.

Comment thread src/features/cluster/components/UsageMeter.tsx Outdated
@github-actions

github-actions Bot commented Jul 24, 2026

Copy link
Copy Markdown

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 53.46% 5967 / 11160
🔵 Statements 54.04% 6401 / 11843
🔵 Functions 46.35% 1502 / 3240
🔵 Branches 46.59% 4057 / 8707
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
src/features/cluster/ClusterHome.tsx 0% 0% 0% 0% 39-487
src/features/cluster/routes.ts 75% 100% 55% 75% 17, 24, 31, 38, 45, 89, 97, 105, 115
src/features/cluster/components/ClusterPageLayout.tsx 0% 0% 0% 0% 17-51
src/features/cluster/components/ClusterUsageCard.tsx 100% 100% 100% 100%
src/features/cluster/components/UsageMeter.tsx 100% 100% 100% 100%
src/features/cluster/usage/Page.tsx 97.87% 81.53% 100% 100% 213
src/integrations/api/cluster/getClusterUsage.ts 42.85% 50% 40% 42.85% 87-101
Generated in workflow #1614 for commit afd6fec by the Vitest Coverage Report Action

@dawsontoth

Copy link
Copy Markdown
Contributor Author

Backend endpoint that will replace the placeholder data here: HarperFast/central-manager#503

dawsontoth added a commit that referenced this pull request Jul 24, 2026
Treat a negative limit as unlimited (server sends -1 for that) and skip
the percentage calc when limit is 0, so the bar never divides by zero
(NaN) — per gemini-code-assist review on #1559.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@dawsontoth dawsontoth changed the title feat(cluster): plan usage card + Usage tab (mockup) [#1297] feat(cluster): cluster plan usage card + Usage tab [#1297] Jul 24, 2026
dawsontoth and others added 6 commits July 28, 2026 13:17
UI-only mockup with placeholder data for the cluster usage-vs-plan
display. Pending the central-manager GET /Cluster/:id/usage endpoint
that will supply real data — swapping the SAMPLE constants for the
response keeps the components and layout unchanged.

- ClusterUsageCard on the managed cluster overview: Reads / Writes /
  Storage / Compute, with a "View all usage" link into the tab.
- Usage item in the cluster sub-nav rail (managed clusters only) and a
  Usage tab with the full breakdown: metered-this-cycle meters,
  rate-limit ceilings, and per-instance resources.
- Shared UsageMeter primitive: used/limit bar, ">=90%" amber warning,
  and hatched "Unlimited" state for plan limits of -1.

Framing is "used X of Y this cycle": purchased blocks auto-renew and
re-bill on exhaustion for paid tiers, while for the free tier the same
bar doubles as the hard-limit warning.

Only the 8 "this cycle" metrics have real usage counters server-side;
per-minute rate limits, TLS handshakes, and per-instance resources are
limits/provisioning only (shown as info, never as meters).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Treat a negative limit as unlimited (server sends -1 for that) and skip
the percentage calc when limit is 0, so the bar never divides by zero
(NaN) — per gemini-code-assist review on #1559.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the mockup's placeholder SAMPLE data with a real useClusterUsage
query against the central-manager endpoint (HarperFast/central-manager#503).
Components and layout are unchanged — only the data source.

- getClusterUsage.ts: typed ClusterUsage response, query hook, and a
  usageSubtitle helper (plan name / cycle renewal / freshness).
- ClusterUsageCard: renders totals; hides while loading, on error, for
  self-hosted, or when there's no active plan/usage.
- Usage tab: loading spinner, self-hosted + no-usage empty states, and
  the metered / rate-limit / per-instance sections from live data.

The endpoint isn't on stage yet (CM #503 in review), so today the query
errors and the UI degrades gracefully (card hidden, tab shows the empty
state); it lights up once the endpoint deploys.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Track the central-manager contract redesign (HarperFast/central-manager#503):
usage is metered per region, not cluster-wide.

- getClusterUsage: per-metric value is now { used, limit, unlimited,
  limitKnown }; regions carry region name + regionIds + exhausted +
  block counts; add mostConstrained; drop the misleading asOf freshness.
- UsageMeter: render three ceiling states — finite %, hatched Unlimited,
  and '—' for an unresolved limit (never show unknown as Unlimited).
  Shared METRIC_LABEL/FORMAT/ORDER + toMeter() helper.
- Overview card: single region → the four headline meters; multiple
  regions → the most-constrained region×metric (no cluster-wide average).
- Usage tab: one section per region (name, plan, renews/exhausted badge,
  the eight meters, rate limits, per-instance resources).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…dth + IDs

Follows the central-manager contract update (HarperFast/central-manager#503) and
a UX pass:

- Region status: 'active' | 'exhausted' | 'lapsed' (was a bool); the Usage tab
  renders exhausted vs lapsed as distinct badges, and totals are { used } only.
- Usage tab: each region is a collapsible grouped card (lapsed collapsed by
  default); rate limits + per-instance resources hoist into one shared 'Plan
  limits & resources' card when uniform across regions (was repeated per region).
- Completeness pass vs the new-cluster plan modal: add the Read/Write/Real-time
  bandwidth (per-minute bytes) rate rows, and surface region IDs / plan ID subtly
  in the card headers.
- Overview card: tightened multi-region 'most constrained' callout.
- Storage/memory render in decimal GB to match the plan's stated disk; extra
  bottom padding on the tab.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Now that the endpoint contract is locked (HarperFast/central-manager#503 approved),
pin the UI behaviour that matters:

- UsageMeter: the three ceiling states (finite %, hatched Unlimited, '—' for an
  unresolved plan), the >=90% warning, capping at 100%, and that a zero or
  negative limit can't render NaN/Infinity or masquerade as Unlimited.
- ClusterUsageCard: four headline meters for a single region vs the
  most-constrained callout for several, the Usage-tab link, and that it renders
  nothing while loading, on error, for self-managed, or with no regions.
- Usage tab: every metered metric, region/plan ids in the headers, collapse and
  re-expand, exhausted vs lapsed badges (lapsed collapsed by default), the
  shared 'Plan limits & resources' card when uniform (per-region when not), and
  the self-hosted / no-usage / loading states.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@dawsontoth
dawsontoth force-pushed the claude/cluster-usage-metrics-383596 branch from 7839585 to 0c152ba Compare July 28, 2026 17:18
@dawsontoth
dawsontoth marked this pull request as ready for review July 28, 2026 17:19
@dawsontoth
dawsontoth requested a review from a team as a code owner July 28, 2026 17:19
@dawsontoth

Copy link
Copy Markdown
Contributor Author

Now verified end-to-end against live stage data — HarperFast/central-manager#503 is deployed to dev + stage, so this is no longer running on placeholders.

GET /Cluster/:id/usage on stage returns 200 with the expected shape, and both surfaces render it:

Overview card — real plan name and renewal (Fabric Managed Service Block Level 0 plan · renews Dec 4), real ceilings (10M reads, 2M writes, 2 hr compute), and real storage 4 GB / 5 GB (70%) sourced from InstanceMonitoring.

Usage tab — the region card keyed on the real region (us-1) and plan (fabric-block-level-0), all 8 metered metrics against their real limits, and the shared Plan limits & resources card showing the plan's actual rate limits (1,000 reads/min, 1 MB read bandwidth/min, …).

Also in this push:

  • Rebased onto stage (was 38 behind) — linear history, no merge commit, no conflicts. All UI files verified byte-identical to their pre-rebase state.
  • 22 component tests added now that the contract is locked: the meter's three ceiling states + its zero/negative-limit guards, the card's single- vs multi-region branches and all four hide conditions, and the tab's collapse/expand, exhausted-vs-lapsed badges, shared-vs-per-region plan info, and empty/loading states. Cluster-feature suite is 105 passing; tsc/oxlint/dprint clean.
  • Contract re-checked field-by-field against the merged endpoint — no drift.

Out of draft and ready for review.

Comment thread src/features/cluster/components/ClusterUsageCard.tsx Outdated
Comment thread src/features/cluster/usage/Page.tsx Outdated
Two review comments from the #1559 review, both customer-facing on a billing
surface:

- ClusterUsageCard: `mostConstrained.region` is nullable, so an unnamed region
  rendered the literal string "null · Reads" on the overview card. Falls back to
  'Region', matching how the Usage tab already guards the identical field.

- Usage tab: a request that came back with nothing was folded into the empty
  state, telling the user "No usage has been recorded for the current cycle yet"
  when the truth is we never loaded it — on a billing surface that reads as a
  zero bill. Now renders a distinct error instead.

  The review suggested keying that on `isError`. Verifying it against stage
  showed `isError` alone is not enough: when a retry is paused (browser offline)
  react-query parks the query at pending/paused, so `isLoading` and `isError` are
  BOTH false with no data, and the page still claimed an empty cycle. Keyed on
  `!data` instead, which covers the errored, paused and never-enabled cases
  alike. Because it keys on absent data rather than the error flag, a failed
  *background* refetch keeps rendering the cached numbers.

Tests: 5 new cases (110 passing in the cluster suite). The two bug reproductions
were confirmed to fail against the pre-fix source, printing exactly the reported
strings ("null · Reads", "No usage has been recorded ...").

Verified in the browser against live stage data: the meters and overview card
render unchanged, and forcing the usage request to fail now shows the error where
it previously claimed an empty cycle.

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

Copy link
Copy Markdown
Contributor Author

Acted on both review comments from @cb1kenobi's pass — pushed in afd6fec, threads resolved.

  • null · Reads on the overview card — fixed as suggested (mc.region ?? 'Region'), matching how the Usage tab already guards that field.
  • Fetch failure shown as "no usage this cycle" — fixed, but not the way the comment suggested, and that's worth flagging: an isError check alone doesn't close it. Forcing the request to fail on stage parked the query at status: 'pending' / fetchStatus: 'paused' (react-query pausing an offline retry), which leaves isLoading and isError both false with no data — so it fell through to the empty state again. Keyed on !data instead, which covers the errored, paused and never-enabled cases together, and left the empty state for data that actually arrived reporting zero regions. A failed background refetch still shows the cached numbers rather than blanking them behind an error. Details in the thread.

Verification: both bug reproductions were confirmed to fail against the pre-fix source, printing exactly the reported strings. Verified in the browser on live stage data — meters and overview card render unchanged, and the forced failure now shows "Couldn't load usage data — refresh to try again." where it previously claimed an empty cycle. 5 new tests (cluster suite 110 passing); full suite 1881 passing, tsc/oxlint/dprint clean.

🤖 Generated with Claude Code

@dawsontoth
dawsontoth requested a review from cb1kenobi July 28, 2026 18:07

@kriszyp kriszyp left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is awesome, going to be great to have this!
A couple of inline comments to consider.
🤖 Reviewed with GPT 5.6


export type UsageMetricKey = keyof UsageMetrics;

export interface UsageRateLimits {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can we normalize these limits in the endpoint before presenting them as regional throughput ceilings? central-manager's rateLimitsFromPlan currently copies raw Plan.planLimits fields. That makes this page wrong in two supported cases: managed fabric-block-dedicated-unlimited-{2..5} plans return -1, so rowsFrom displays -1 / -1 B; and the existing plan UI defines read/read-bandwidth/real-time/TLS regional rates as the plan value multiplied by Region.purchasedBlockMultiplier, so a tier with multiplier >1 will show a lower number here. Please have the usage endpoint return effective per-region values with explicit unlimited/unknown semantics, consume that normalized shape here, and add coverage for both cases. Multiplying activeBlockCount in Studio is not safe because a mixed cohort after a plan change can contain more live blocks than the current region tier.

}

/** "Standard plan · renews Aug 12" — shared by the card + tab. */
export function usageSubtitle(data: ClusterUsage): string {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Could we make this subtitle conditional on the regions being uniform? The endpoint sorts regions alphabetically and supports a different current plan per region, while renewsAt is the earliest expiry among active regions. As written, a Europe/Standard + US/Enterprise cluster can display Standard plan · renews Aug 12 as a cluster-wide statement even when the US region renews later. The current copy is accurate for one region; for multiple regions, only show the plan when all names match and label the date next renewal (or omit these summary facts), then cover heterogeneous plans/expiries in the card test.

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.

Usage remaining display in the UI

4 participants