From 88a2fd26c7560a2d2ccb63c21d575a6c72e9f94b Mon Sep 17 00:00:00 2001 From: Xiangzhe <32761048+xz-dev@users.noreply.github.com> Date: Thu, 6 Aug 2026 09:41:49 +0800 Subject: [PATCH] fix(dashboard): make quota card ordering deterministic (#9329) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Validated in local merge-train (devbox-vm-06-dev002) @ combined-tip (FAST gates — only pre-existing audit.test.ts flake). --- .../ProviderLimits/QuotaCardGrid.tsx | 84 ++++++----- .../usage/components/ProviderLimits/index.tsx | 39 +++-- .../usage/components/ProviderLimits/utils.tsx | 107 ++++++++++++++ tests/unit/quota-deterministic-order.test.ts | 136 ++++++++++++++++++ 4 files changed, 319 insertions(+), 47 deletions(-) create mode 100644 tests/unit/quota-deterministic-order.test.ts diff --git a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/QuotaCardGrid.tsx b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/QuotaCardGrid.tsx index ed524043c8..438e9f99c4 100644 --- a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/QuotaCardGrid.tsx +++ b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/QuotaCardGrid.tsx @@ -2,6 +2,9 @@ import type { ReactNode } from "react"; import QuotaCard from "./QuotaCard"; +import { PROVIDER_ORDER } from "./constants"; +import { compareProviderGroups } from "./utils"; +import { compareTr } from "@/shared/utils/turkishText"; interface Props { connections: any[]; @@ -47,50 +50,63 @@ export default function QuotaCardGrid({ }: Props) { if (connections.length === 0) return null; - // Group connections by provider, preserving the order from sortedConnections. + // Group connections by provider (preserving in-group order), then order the + // groups deterministically: PROVIDER_ORDER rank → label (locale-aware) → + // key. Without this the group order followed first-appearance in the + // status/reset-sorted list, so groups shuffled whenever quota refreshed. const groups = new Map(); for (const conn of connections) { const list = groups.get(conn.provider) ?? []; list.push(conn); groups.set(conn.provider, list); } + const orderedProviders = [...groups.keys()].sort((a, b) => + compareProviderGroups(a, b, { + providerOrder: PROVIDER_ORDER, + providerLabels, + compare: compareTr, + }) + ); return (
- {[...groups.entries()].map(([provider, conns]) => ( -
-

- {providerLabels[provider] || provider} - - ({conns.length} account{conns.length !== 1 ? "s" : ""}) - -

-
- {conns.map((conn) => ( - onRefresh(conn.id, conn.provider)} - onOpenCutoff={() => onOpenCutoff(conn)} - onOpenResetCredits={() => onOpenResetCredits?.(conn.id, conn.provider)} - onToggleActive={(nextActive) => onToggleActive(conn.id, nextActive)} - togglingActive={togglingActiveId === conn.id} - redeemingResetCredit={redeemingResetCreditId === conn.id} - loadingResetCredits={loadingResetCreditsId === conn.id} - quotaVisibility={quotaVisibility} - onHideQuota={onHideQuota ? (q) => onHideQuota(conn.provider, q) : undefined} - onShowQuota={onShowQuota ? (q) => onShowQuota(conn.provider, q) : undefined} - /> - ))} + {orderedProviders.map((provider) => { + const conns = groups.get(provider)!; + return ( +
+

+ {providerLabels[provider] || provider} + + ({conns.length} account{conns.length !== 1 ? "s" : ""}) + +

+
+ {conns.map((conn) => ( + onRefresh(conn.id, conn.provider)} + onOpenCutoff={() => onOpenCutoff(conn)} + onOpenResetCredits={() => onOpenResetCredits?.(conn.id, conn.provider)} + onToggleActive={(nextActive) => onToggleActive(conn.id, nextActive)} + togglingActive={togglingActiveId === conn.id} + redeemingResetCredit={redeemingResetCreditId === conn.id} + loadingResetCredits={loadingResetCreditsId === conn.id} + quotaVisibility={quotaVisibility} + onHideQuota={onHideQuota ? (q) => onHideQuota(conn.provider, q) : undefined} + onShowQuota={onShowQuota ? (q) => onShowQuota(conn.provider, q) : undefined} + /> + ))} +
-
- ))} + ); + })}
); } diff --git a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.tsx b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.tsx index e7c9dfed76..b70a126309 100644 --- a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.tsx +++ b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.tsx @@ -12,6 +12,7 @@ import { calculatePercentage, matchesProviderFilter, buildProviderOptions, + compareQuotaConnections, } from "./utils"; import Card from "@/shared/components/Card"; import { CardSkeleton } from "@/shared/components/Loading"; @@ -529,8 +530,12 @@ export default function ProviderLimits({ ); const sortedConnections = useMemo(() => { - return [...filteredConnections].sort( - (a, b) => (PROVIDER_ORDER[a.provider] || 99) - (PROVIDER_ORDER[b.provider] || 99) + return [...filteredConnections].sort((a, b) => + compareQuotaConnections(a, b, { + providerOrder: PROVIDER_ORDER, + providerLabels: PROVIDER_LABEL, + compare: compareTr, + }) ); }, [filteredConnections]); const visibleQuotaData = useVisibleQuotaData(sortedConnections, quotaData); @@ -650,9 +655,10 @@ export default function ProviderLimits({ return true; }); - // Inside each group we still want "critical first, then alert, then ok, - // then empty; tiebreak by soonest reset". Provider order between groups - // is enforced separately via PROVIDER_ORDER. + // Provider rank stays the outer sort key so each group keeps its fixed + // slot (mirrors dashboard/providers determinism); "critical first, then + // alert, then ok, then empty; tiebreak by soonest reset" only orders + // accounts inside their own provider group. const statusRank: Record = { critical: 0, alert: 1, @@ -660,14 +666,21 @@ export default function ProviderLimits({ empty: 3, all: 4, }; - return [...filtered].sort((a, b) => { - const sa = statusRank[statusByConnection[a.id] || "empty"]; - const sb = statusRank[statusByConnection[b.id] || "empty"]; - if (sa !== sb) return sa - sb; - const ra = getSoonestResetMs(visibleQuotaData[a.id]?.quotas); - const rb = getSoonestResetMs(visibleQuotaData[b.id]?.quotas); - return ra - rb; - }); + return [...filtered].sort((a, b) => + compareQuotaConnections(a, b, { + providerOrder: PROVIDER_ORDER, + providerLabels: PROVIDER_LABEL, + compare: compareTr, + accountCompare: (x, y) => { + const sx = statusRank[statusByConnection[x.id] || "empty"]; + const sy = statusRank[statusByConnection[y.id] || "empty"]; + if (sx !== sy) return sx - sy; + const rx = getSoonestResetMs(visibleQuotaData[x.id]?.quotas); + const ry = getSoonestResetMs(visibleQuotaData[y.id]?.quotas); + return rx - ry; + }, + }) + ); }, [ sortedConnections, tierByConnection, diff --git a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.tsx b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.tsx index 01750d89b0..66ecdbfa20 100644 --- a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.tsx +++ b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.tsx @@ -620,3 +620,110 @@ export function buildProviderOptions( } return Array.from(seen).sort(compare); } + +// --- Deterministic quota-card ordering ------------------------------------- +// Mirrors the dashboard/providers rule (`providerPageUtils.ts:: +// sortProviderEntriesByName`): every level of ordering must end in a stable, +// data-independent tiebreak so cards never re-flow between refreshes. +// +// Before this, `visibleConnections` globally sorted ALL connections by +// status then soonest reset, and QuotaCardGrid grouped by first-appearance — +// so each provider group's position was decided by whichever of its accounts +// happened to sort first (status/reset change every refresh → groups +// shuffled). Provider rank is now a sort key again, so a group's position is +// fixed by PROVIDER_ORDER and account status/reset only orders accounts +// inside their own group. + +export interface QuotaOrderConnection { + id?: unknown; + provider?: unknown; + name?: unknown; + email?: unknown; + displayName?: unknown; +} + +/** Label/name key: providers-page `getProviderSortLabel` — case-insensitive display name. */ +function quotaConnLabel(conn: QuotaOrderConnection): string { + const name = typeof conn.name === "string" ? conn.name : ""; + const provider = typeof conn.provider === "string" ? conn.provider : ""; + return (name || provider).toLowerCase(); +} + +/** Technical tiebreak key: providers-page `providerId.localeCompare(...)` — ASCII on purpose. */ +function quotaConnTiebreak(conn: QuotaOrderConnection): string { + const email = typeof conn.email === "string" ? conn.email : ""; + const id = typeof conn.id === "string" ? conn.id : String(conn.id ?? ""); + return email || id; +} + +function providerRank(provider: unknown, providerOrder: Record): number { + const key = typeof provider === "string" ? provider : ""; + return providerOrder[key] ?? 99; +} + +/** + * Order connections for the quota card grid. Levels (first non-zero wins): + * 1. `PROVIDER_ORDER` rank — keeps each provider group glued to its fixed slot. + * 2. Provider label (locale-aware, case-insensitive) — orders unranked providers. + * 3. Provider key ASCII — deterministic tiebreak between aliased/equal labels. + * 4. `accountCompare` (optional) — in-group intent (critical-first, soonest reset). + * 5. Account label, then email/id ASCII — so equal-status accounts never shuffle. + */ +export function compareQuotaConnections( + a: T, + b: T, + opts: { + providerOrder: Record; + providerLabels?: Record; + accountCompare?: (a: T, b: T) => number; + compare?: (a: string, b: string) => number; + } +): number { + const cmp = opts.compare ?? ((x: string, y: string) => x.localeCompare(y)); + const labels = opts.providerLabels ?? {}; + + const ra = providerRank(a.provider, opts.providerOrder); + const rb = providerRank(b.provider, opts.providerOrder); + if (ra !== rb) return ra - rb; + + const pa = typeof a.provider === "string" ? a.provider : ""; + const pb = typeof b.provider === "string" ? b.provider : ""; + const providerLabelCmp = cmp(labels[pa] ?? pa, labels[pb] ?? pb); + if (providerLabelCmp !== 0) return providerLabelCmp; + if (pa !== pb) return pa < pb ? -1 : 1; + + if (opts.accountCompare) { + const acc = opts.accountCompare(a, b); + if (acc !== 0) return acc; + } + + const accountLabelCmp = cmp(quotaConnLabel(a), quotaConnLabel(b)); + if (accountLabelCmp !== 0) return accountLabelCmp; + const ta = quotaConnTiebreak(a); + const tb = quotaConnTiebreak(b); + return ta < tb ? -1 : ta > tb ? 1 : 0; +} + +/** + * Order provider group keys for rendering. Same provider-level rule as + * `compareQuotaConnections` (rank → label → key), used by QuotaCardGrid to + * place group headers deterministically. + */ +export function compareProviderGroups( + a: string, + b: string, + opts: { + providerOrder: Record; + providerLabels?: Record; + compare?: (a: string, b: string) => number; + } +): number { + const cmp = opts.compare ?? ((x: string, y: string) => x.localeCompare(y)); + const labels = opts.providerLabels ?? {}; + const ra = providerRank(a, opts.providerOrder); + const rb = providerRank(b, opts.providerOrder); + if (ra !== rb) return ra - rb; + const labelCmp = cmp(labels[a] ?? a, labels[b] ?? b); + if (labelCmp !== 0) return labelCmp; + return a < b ? -1 : a > b ? 1 : 0; +} diff --git a/tests/unit/quota-deterministic-order.test.ts b/tests/unit/quota-deterministic-order.test.ts new file mode 100644 index 0000000000..f565854c43 --- /dev/null +++ b/tests/unit/quota-deterministic-order.test.ts @@ -0,0 +1,136 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +// Guards the deterministic quota-card ordering fix. Before this, the quota +// dashboard globally sorted ALL connections by status then soonest reset and +// grouped by first-appearance — so each provider group's position was decided +// by whichever account happened to sort first, and groups shuffled on every +// refresh. These tests pin the providers-page-style rule: provider rank → +// provider label → provider key fixes each group's slot, and status/reset only +// orders accounts inside their own group with a deterministic name/email/id +// tiebreak. + +const utils = + await import("../../src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.tsx"); +const constants = + await import("../../src/app/(dashboard)/dashboard/usage/components/ProviderLimits/constants.ts"); + +const { compareQuotaConnections, compareProviderGroups } = utils; +const { PROVIDER_ORDER, PROVIDER_LABEL } = constants; + +const OPTS = { providerOrder: PROVIDER_ORDER, providerLabels: PROVIDER_LABEL }; + +function ids(conns) { + return conns.map((c) => c.id); +} + +test("quota-order: provider rank dominates account status (group slot is fixed)", () => { + // antigravity (rank 1) ok account vs codex (rank 4) critical account. + // Pre-fix, the critical codex account floated above antigravity and dragged + // the codex group to the top. Now antigravity's rank wins. + const okAntigravity = { id: "a", provider: "antigravity", name: "Ag" }; + const criticalCodex = { id: "c", provider: "codex", name: "Cx" }; + // accountCompare that would put codex first if provider rank were ignored: + const criticalFirst = (x, y) => + (x.provider === "codex" ? 0 : 1) - (y.provider === "codex" ? 0 : 1); + + const sorted = [criticalCodex, okAntigravity].sort((a, b) => + compareQuotaConnections(a, b, { ...OPTS, accountCompare: criticalFirst }) + ); + assert.deepEqual(ids(sorted), ["a", "c"], "provider rank outranks account status"); +}); + +test("quota-order: accountCompare orders accounts within the same provider group", () => { + const a = { id: "a", provider: "codex", name: "Acct A" }; + const b = { id: "b", provider: "codex", name: "Acct B" }; + // b is "critical" (rank 0), a is "ok" (rank 2) via the injected comparator. + const statusOf = { a: 2, b: 0 }; + const criticalFirst = (x, y) => statusOf[x.id] - statusOf[y.id]; + + const sorted = [a, b].sort((x, y) => + compareQuotaConnections(x, y, { ...OPTS, accountCompare: criticalFirst }) + ); + assert.deepEqual(ids(sorted), ["b", "a"], "critical account first inside its group"); +}); + +test("quota-order: deterministic name tiebreak makes equal-status accounts stable", () => { + const make = () => [ + { id: "1", provider: "codex", name: "Zeta" }, + { id: "2", provider: "codex", name: "Alpha" }, + { id: "3", provider: "codex", name: "Mid" }, + ]; + const noStatus = () => 0; + const first = [...make()].sort((x, y) => + compareQuotaConnections(x, y, { ...OPTS, accountCompare: noStatus }) + ); + const second = [...make()] + .reverse() + .sort((x, y) => compareQuotaConnections(x, y, { ...OPTS, accountCompare: noStatus })); + assert.deepEqual(ids(first), ["2", "3", "1"], "name alphabetical"); + assert.deepEqual(ids(first), ids(second), "input order does not change output"); +}); + +test("quota-order: unranked providers fall back to label alphabetical, after ranked", () => { + // github is ranked 3; the two fake providers are unranked (→ 99). Two + // unranked providers order among themselves by label (here label falls back + // to the provider key since neither is in PROVIDER_LABEL). + const conns = [ + { id: "1", provider: "zzz-unranked", name: "X" }, + { id: "2", provider: "aaa-unranked", name: "Y" }, + { id: "3", provider: "github", name: "Z" }, + ]; + const sorted = [...conns].sort((a, b) => compareQuotaConnections(a, b, OPTS)); + assert.deepEqual( + ids(sorted), + ["3", "2", "1"], + "ranked first; unranked by label (labels fall back to provider key here)" + ); +}); + +test("quota-order: group keys order by rank then label then key", () => { + const keys = ["codex", "antigravity", "some-new-provider", "github"]; + const sorted = [...keys].sort((a, b) => compareProviderGroups(a, b, OPTS)); + assert.deepEqual( + sorted, + ["antigravity", "github", "codex", "some-new-provider"], + "PROVIDER_ORDER rank 1,3,4 then unranked provider last" + ); +}); + +test("quota-order: aliased providers with equal rank break tie by provider key", () => { + // xai-oauth and xao are both rank 16 in PROVIDER_ORDER. Deterministic key + // tiebreak (ASCII) must decide, not insertion order. + const a = ["xai-oauth", "xao"].sort((x, y) => compareProviderGroups(x, y, OPTS)); + const b = ["xao", "xai-oauth"].sort((x, y) => compareProviderGroups(x, y, OPTS)); + assert.deepEqual(a, b, "stable regardless of input order"); + assert.deepEqual(a[0], "xai-oauth", "ASCII: 'xai-oauth' < 'xao'"); +}); + +test("quota-order: full grid end-to-end keeps group slots fixed while ordering accounts inside", () => { + // Simulate the real pipeline: flat list across providers, mixed statuses. + const conns = [ + { id: "ag1", provider: "antigravity", name: "Ag One" }, + { id: "cx-ok", provider: "codex", name: "Codex Ok" }, + { id: "gh1", provider: "github", name: "Gh One" }, + { id: "cx-crit", provider: "codex", name: "Codex Crit" }, + { id: "ag2", provider: "antigravity", name: "Ag Two" }, + ]; + // codex-crit is critical, everything else ok. + const statusOf = { "cx-crit": 0 }; + const criticalFirst = (x, y) => (statusOf[x.id] ?? 2) - (statusOf[y.id] ?? 2); + + const sorted = [...conns].sort((a, b) => + compareQuotaConnections(a, b, { ...OPTS, accountCompare: criticalFirst }) + ); + + // Provider group slots follow rank: antigravity(1) → github(3) → codex(4). + // The critical codex account does NOT pull the codex group to the front. + assert.deepEqual( + sorted.map((c) => c.provider), + ["antigravity", "antigravity", "github", "codex", "codex"] + ); + // Inside codex, critical first. + assert.deepEqual(ids(sorted).slice(3), ["cx-crit", "cx-ok"]); + // Inside antigravity, name alphabetical. + assert.deepEqual(ids(sorted).slice(0, 2), ["ag1", "ag2"]); +});