Skip to content

Commit e43fce7

Browse files
committed
fix(gui): identify partial quota windows
1 parent 4c376d3 commit e43fce7

11 files changed

Lines changed: 248 additions & 8 deletions

File tree

gui/src/components/QuotaBars.tsx

Lines changed: 57 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,15 @@ import { type AccountQuota, normalizeQuotaForPlan } from "../codex-quota-utils";
77
/* Helpers are co-located with QuotaBars for overview sorting / stacked layout. */
88
/* eslint-disable react-refresh/only-export-components */
99

10-
export type QuotaBarRow = { label: string; limitLabel: string; percent: number; resetAt?: number };
10+
export type QuotaWindowKey = "fiveHour" | "weekly" | "monthly";
11+
export type QuotaBarRow = {
12+
windowKey?: QuotaWindowKey;
13+
customLabel?: string;
14+
label: string;
15+
limitLabel: string;
16+
percent: number;
17+
resetAt?: number;
18+
};
1119

1220
/**
1321
* Window ordering is computed from RAW wire identities BEFORE localization
@@ -43,6 +51,7 @@ export function buildQuotaRows(quota: AccountQuota | null, plan: string | null |
4351
ranked.push({
4452
rank: 0,
4553
row: {
54+
windowKey: "fiveHour",
4655
label: t("codexAuth.fiveHour"),
4756
limitLabel: t("quota.fiveHourLimit"),
4857
percent: displayQuota.fiveHourPercent,
@@ -54,6 +63,7 @@ export function buildQuotaRows(quota: AccountQuota | null, plan: string | null |
5463
ranked.push({
5564
rank: 1,
5665
row: {
66+
windowKey: "weekly",
5767
label: t("codexAuth.weekly"),
5868
limitLabel: t("quota.weeklyLimit"),
5969
percent: displayQuota.weeklyPercent,
@@ -65,6 +75,7 @@ export function buildQuotaRows(quota: AccountQuota | null, plan: string | null |
6575
ranked.push({
6676
rank: 4,
6777
row: {
78+
windowKey: "monthly",
6879
label: t("codexAuth.monthly"),
6980
limitLabel: t("quota.monthlyLimit"),
7081
percent: displayQuota.monthlyPercent,
@@ -76,7 +87,13 @@ export function buildQuotaRows(quota: AccountQuota | null, plan: string | null |
7687
const localized = localizeCustomQuotaLabel(w.label, t);
7788
ranked.push({
7889
rank: rawCustomWindowRank(w.label),
79-
row: { label: localized, limitLabel: localized, percent: w.percent, resetAt: w.resetAt },
90+
row: {
91+
customLabel: w.label,
92+
label: localized,
93+
limitLabel: localized,
94+
percent: w.percent,
95+
resetAt: w.resetAt,
96+
},
8097
});
8198
}
8299
return ranked.sort((a, b) => a.rank - b.rank).map(entry => entry.row);
@@ -138,7 +155,17 @@ function barFillStyle(percent: number): CSSProperties {
138155
return { ["--bar-scale" as string]: String(barWidth(percent) / 100) };
139156
}
140157

141-
export default function QuotaBars({ quota, plan, threshold, t, className, layout = "compact", pending = false }: {
158+
export default function QuotaBars({
159+
quota,
160+
plan,
161+
threshold,
162+
t,
163+
className,
164+
layout = "compact",
165+
pending = false,
166+
incompleteWindowKeys,
167+
incompleteCustomWindowLabels,
168+
}: {
142169
quota: AccountQuota | null;
143170
plan?: string | null;
144171
threshold: number;
@@ -151,6 +178,9 @@ export default function QuotaBars({ quota, plan, threshold, t, className, layout
151178
* bar slot so deferred fill does not shove the page down.
152179
*/
153180
pending?: boolean;
181+
/** Optional overview-only coverage status. Other quota surfaces remain unchanged when omitted. */
182+
incompleteWindowKeys?: ReadonlySet<QuotaWindowKey>;
183+
incompleteCustomWindowLabels?: ReadonlySet<string>;
154184
}) {
155185
const { locale } = useI18n();
156186
const rows = buildQuotaRows(quota, plan, t);
@@ -201,7 +231,16 @@ export default function QuotaBars({ quota, plan, threshold, t, className, layout
201231
return (
202232
<div className={`quota-stacked${className ? ` ${className}` : ""}`}>
203233
{rows.map(row => (
204-
<StackedQuotaRow key={row.limitLabel} row={row} threshold={threshold} t={t} locale={locale} />
234+
<StackedQuotaRow
235+
key={row.limitLabel}
236+
row={row}
237+
threshold={threshold}
238+
t={t}
239+
locale={locale}
240+
incomplete={row.windowKey
241+
? incompleteWindowKeys?.has(row.windowKey) === true
242+
: row.customLabel !== undefined && incompleteCustomWindowLabels?.has(row.customLabel) === true}
243+
/>
205244
))}
206245
</div>
207246
);
@@ -254,11 +293,12 @@ function QuotaRow({ label, percent, resetAt, threshold, t, locale }: {
254293
);
255294
}
256295

257-
function StackedQuotaRow({ row, threshold, t, locale }: {
296+
function StackedQuotaRow({ row, threshold, t, locale, incomplete }: {
258297
row: QuotaBarRow;
259298
threshold: number;
260299
t: TFn;
261300
locale: Locale;
301+
incomplete: boolean;
262302
}) {
263303
const exhausted = isQuotaExhausted(row.percent);
264304
const warn = isQuotaWarn(row.percent, threshold);
@@ -267,7 +307,18 @@ function StackedQuotaRow({ row, threshold, t, locale }: {
267307
return (
268308
<div className={`quota-stacked-row${warn ? " quota-stacked-row--warn" : ""}${exhausted ? " quota-stacked-row--exhausted" : ""}`}>
269309
<div className="quota-stacked-head">
270-
<span className="quota-stacked-limit">{row.limitLabel}</span>
310+
<span className="quota-stacked-limit-group">
311+
<span className="quota-stacked-limit">{row.limitLabel}</span>
312+
{incomplete && (
313+
<span
314+
className="quota-window-partial"
315+
aria-label={t("pws.capacity.windowPartialA11y", { window: row.limitLabel })}
316+
title={t("pws.capacity.windowPartialA11y", { window: row.limitLabel })}
317+
>
318+
{t("pws.capacity.windowPartial")}
319+
</span>
320+
)}
321+
</span>
271322
<span className="quota-stacked-reset muted">{resetText}</span>
272323
</div>
273324
<div className="quota-stacked-bar-row">

gui/src/components/provider-workspace/ProviderOverviewDashboard.tsx

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import {
2222
relativeTimeLabelsFromT,
2323
type ProviderUsageTotals,
2424
} from "../../provider-workspace/usage";
25-
import { maxQuotaUtilisation } from "../QuotaBars";
25+
import { maxQuotaUtilisation, type QuotaWindowKey } from "../QuotaBars";
2626
import { ProviderIcon } from "./ProviderRail";
2727
import { formatProviderDisplayName } from "../../provider-icons";
2828
import QuotaBars from "../QuotaBars";
@@ -242,6 +242,16 @@ function ProviderCapacityQuota({ report, pending }: { report: ProviderQuotaRepor
242242
const aggregation = capacityAggregationFromReport(report);
243243
const primaryQuota = accountQuotaFromReport(report);
244244
const showsAggregate = aggregation?.presentation === "aggregate";
245+
const incompleteWindowKeys = new Set<QuotaWindowKey>();
246+
const incompleteCustomWindowLabels = new Set<string>();
247+
if (showsAggregate && aggregation) {
248+
if (aggregation.fiveHour?.incomplete) incompleteWindowKeys.add("fiveHour");
249+
if (aggregation.weekly?.incomplete) incompleteWindowKeys.add("weekly");
250+
if (aggregation.monthly?.incomplete) incompleteWindowKeys.add("monthly");
251+
for (const window of aggregation.customWindows ?? []) {
252+
if (window.incomplete) incompleteCustomWindowLabels.add(window.label);
253+
}
254+
}
245255
const recoveryRows: Array<{ label: string; window: CapacityWindowView }> = showsAggregate && aggregation ? [
246256
...(aggregation.fiveHour ? [{ label: t("codexAuth.fiveHour"), window: aggregation.fiveHour }] : []),
247257
...(aggregation.weekly ? [{ label: t("codexAuth.weekly"), window: aggregation.weekly }] : []),
@@ -257,7 +267,17 @@ function ProviderCapacityQuota({ report, pending }: { report: ProviderQuotaRepor
257267
return (
258268
<>
259269
{showsAggregate && <div className="pws-capacity-label">{t("pws.capacity.estimate")}</div>}
260-
{(primaryQuota || pending) && <QuotaBars quota={primaryQuota} threshold={80} t={t} layout="stacked" pending={pending} />}
270+
{(primaryQuota || pending) && (
271+
<QuotaBars
272+
quota={primaryQuota}
273+
threshold={80}
274+
t={t}
275+
layout="stacked"
276+
pending={pending}
277+
incompleteWindowKeys={showsAggregate ? incompleteWindowKeys : undefined}
278+
incompleteCustomWindowLabels={showsAggregate ? incompleteCustomWindowLabels : undefined}
279+
/>
280+
)}
261281
{aggregation && (
262282
<div className="pws-capacity-details">
263283
{recoveryRows.flatMap(({ label, window }) => (

gui/src/i18n/de.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1386,6 +1386,8 @@ export const de: Record<TKey, string> = {
13861386
"pws.capacity.recoveryShare": "+{percent} % Pool-Kapazität",
13871387
"pws.capacity.incomplete": "Unvollständige Abdeckung: {excluded} Konten ausgeschlossen, davon {unknown} mit unbekanntem Tarif",
13881388
"pws.capacity.partial": "Teilweise Fensterabdeckung: {count} Konten melden nicht jedes angezeigte Limitfenster",
1389+
"pws.capacity.windowPartial": "Teilweise",
1390+
"pws.capacity.windowPartialA11y": "{window}: unvollständige Kontoabdeckung",
13891391
"pws.dashboard.recentlyUsed": "KÜRZLICH VERWENDET",
13901392
"pws.dashboard.requests": "{count} Anfragen",
13911393
"pws.dashboard.checkedAgo": "Geprüft {time}",

gui/src/i18n/en.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1052,6 +1052,8 @@ export const en = {
10521052
"pws.capacity.recoveryShare": "+{percent}% pool capacity",
10531053
"pws.capacity.incomplete": "Incomplete coverage: {excluded} account(s) excluded, including {unknown} unknown plan(s)",
10541054
"pws.capacity.partial": "Partial window coverage: {count} account(s) do not report every displayed limit window",
1055+
"pws.capacity.windowPartial": "Partial",
1056+
"pws.capacity.windowPartialA11y": "{window}: incomplete account coverage",
10551057
"pws.dashboard.recentlyUsed": "RECENTLY USED",
10561058
"pws.dashboard.requests": "{count} requests",
10571059
"pws.dashboard.checkedAgo": "Checked {time}",

gui/src/i18n/ja.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1002,6 +1002,8 @@ export const ja: Record<TKey, string> = {
10021002
"pws.capacity.recoveryShare": "+{percent}% のプール容量",
10031003
"pws.capacity.incomplete": "対象範囲が不完全です: {excluded} 件を除外(不明なプラン {unknown} 件)",
10041004
"pws.capacity.partial": "一部の期間の対象範囲が不完全です: {count} 件のアカウントでは表示中のすべての制限期間を取得できません",
1005+
"pws.capacity.windowPartial": "一部のみ",
1006+
"pws.capacity.windowPartialA11y": "{window}: アカウントの対象範囲が不完全です",
10051007
"pws.dashboard.recentlyUsed": "最近の使用",
10061008
"pws.dashboard.requests": "{count} リクエスト",
10071009
"pws.dashboard.checkedAgo": "{time} に確認",

gui/src/i18n/ko.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1413,6 +1413,8 @@ export const ko: Record<TKey, string> = {
14131413
"pws.capacity.recoveryShare": "+{percent}% 풀 용량",
14141414
"pws.capacity.incomplete": "불완전한 범위: {excluded}개 계정 제외, 알 수 없는 요금제 {unknown}개 포함",
14151415
"pws.capacity.partial": "일부 기간의 범위가 불완전합니다: {count}개 계정에서 표시된 모든 한도 기간을 확인할 수 없습니다",
1416+
"pws.capacity.windowPartial": "일부만",
1417+
"pws.capacity.windowPartialA11y": "{window}: 계정 범위가 불완전합니다",
14161418
"pws.dashboard.recentlyUsed": "최근 사용",
14171419
"pws.dashboard.requests": "{count}건 요청",
14181420
"pws.dashboard.checkedAgo": "{time} 전 확인",

gui/src/i18n/ru.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1044,6 +1044,8 @@ export const ru: Record<TKey, string> = {
10441044
"pws.capacity.recoveryShare": "+{percent}% ёмкости пула",
10451045
"pws.capacity.incomplete": "Неполное покрытие: исключено аккаунтов: {excluded}, неизвестных планов: {unknown}",
10461046
"pws.capacity.partial": "Частичное покрытие окон: для {count} аккаунтов доступны не все показанные окна лимитов",
1047+
"pws.capacity.windowPartial": "Частично",
1048+
"pws.capacity.windowPartialA11y": "{window}: неполное покрытие аккаунтов",
10471049
"pws.dashboard.recentlyUsed": "Недавно использованные",
10481050
"pws.dashboard.requests": "{count} запросов",
10491051
"pws.dashboard.checkedAgo": "Проверено {time}",

gui/src/i18n/zh.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1406,6 +1406,8 @@ export const zh: Record<TKey, string> = {
14061406
"pws.capacity.recoveryShare": "+{percent}% 账户池容量",
14071407
"pws.capacity.incomplete": "覆盖不完整:已排除 {excluded} 个账户,其中 {unknown} 个套餐未知",
14081408
"pws.capacity.partial": "部分窗口覆盖不完整:{count} 个账户未报告所有显示的限额窗口",
1409+
"pws.capacity.windowPartial": "部分",
1410+
"pws.capacity.windowPartialA11y": "{window}:账户覆盖不完整",
14091411
"pws.dashboard.recentlyUsed": "最近使用",
14101412
"pws.dashboard.requests": "{count} 个请求",
14111413
"pws.dashboard.checkedAgo": "{time} 前检查",

gui/src/styles/provider-quota.css

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,31 @@
4444
font-weight: 600;
4545
}
4646

47+
.quota-stacked-limit-group {
48+
display: inline-flex;
49+
flex: 1 1 auto;
50+
flex-wrap: wrap;
51+
align-items: baseline;
52+
gap: 4px;
53+
min-width: 0;
54+
overflow-wrap: anywhere;
55+
}
56+
57+
.quota-window-partial {
58+
flex: 0 0 auto;
59+
padding: 1px 5px;
60+
border: 1px solid color-mix(in srgb, var(--amber) 45%, transparent);
61+
border-radius: 999px;
62+
color: var(--amber);
63+
font-size: 10px;
64+
font-weight: 600;
65+
line-height: 1.3;
66+
white-space: nowrap;
67+
}
68+
4769
.quota-stacked-reset {
70+
min-width: 0;
71+
overflow-wrap: anywhere;
4872
font-size: 12px;
4973
}
5074

gui/tests/provider-capacity-shell.test.tsx

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,44 @@ function aggregatePayload() {
5656
};
5757
}
5858

59+
function aggregateWindowPayload(weeklyIncomplete: boolean, monthlyIncomplete: boolean) {
60+
const now = Date.now();
61+
const incomplete = weeklyIncomplete || monthlyIncomplete;
62+
return {
63+
reports: [{
64+
provider: "openai",
65+
label: "OpenAI (Codex login)",
66+
source: "chatgpt:wham",
67+
updatedAt: now,
68+
quota: { weeklyPercent: 20, monthlyPercent: 40, updatedAt: now },
69+
aggregation: {
70+
kind: "capacity-weighted-v1",
71+
scope: "routable-known",
72+
presentation: "aggregate",
73+
includedAccounts: 2,
74+
excludedAccounts: 0,
75+
unknownPlanAccounts: 0,
76+
partialWindowAccounts: incomplete ? 1 : 0,
77+
incomplete,
78+
weekly: {
79+
usedPercent: 20,
80+
includedAccounts: weeklyIncomplete ? 1 : 2,
81+
excludedAccounts: weeklyIncomplete ? 1 : 0,
82+
incomplete: weeklyIncomplete,
83+
updatedAt: now,
84+
},
85+
monthly: {
86+
usedPercent: 40,
87+
includedAccounts: monthlyIncomplete ? 1 : 2,
88+
excludedAccounts: monthlyIncomplete ? 1 : 0,
89+
incomplete: monthlyIncomplete,
90+
updatedAt: now,
91+
},
92+
},
93+
}],
94+
};
95+
}
96+
5997
beforeEach(() => {
6098
previous = Object.fromEntries(globals.map(key => [key, Reflect.get(globalThis, key)])) as typeof previous;
6199
originalFetch = globalThis.fetch;
@@ -246,3 +284,64 @@ test("mixed-window coverage uses a distinct warning without whole-account exclus
246284
expect(text).toContain("Partial window coverage: 2 account(s) do not report every displayed limit window");
247285
expect(text).not.toContain("Incomplete coverage: 0 account(s) excluded");
248286
});
287+
288+
test("only the monthly aggregate window receives a localized partial marker", async () => {
289+
quotaPayload = aggregateWindowPayload(false, true);
290+
await mountShell();
291+
292+
const markers = [...host.querySelectorAll<HTMLElement>(".quota-window-partial")];
293+
expect(markers).toHaveLength(1);
294+
expect(markers[0]?.textContent).toBe("Partial");
295+
expect(markers[0]?.getAttribute("aria-label")).toBe("30-day limit: incomplete account coverage");
296+
});
297+
298+
test("only the weekly aggregate window receives a localized partial marker", async () => {
299+
quotaPayload = aggregateWindowPayload(true, false);
300+
await mountShell();
301+
302+
const markers = [...host.querySelectorAll<HTMLElement>(".quota-window-partial")];
303+
expect(markers).toHaveLength(1);
304+
expect(markers[0]?.getAttribute("aria-label")).toBe("Weekly limit: incomplete account coverage");
305+
});
306+
307+
test("complete aggregate windows do not receive partial markers", async () => {
308+
quotaPayload = aggregateWindowPayload(false, false);
309+
await mountShell();
310+
311+
expect(host.querySelectorAll(".quota-window-partial")).toHaveLength(0);
312+
expect(host.textContent ?? "").not.toContain("Partial window coverage");
313+
});
314+
315+
test("five-hour and custom aggregate windows can be marked independently", async () => {
316+
const now = Date.now();
317+
quotaPayload = {
318+
reports: [{
319+
provider: "openai",
320+
label: "OpenAI (Codex login)",
321+
source: "chatgpt:wham",
322+
updatedAt: now,
323+
quota: {
324+
fiveHourPercent: 10,
325+
customWindows: [{ label: "Burst", percent: 30 }],
326+
updatedAt: now,
327+
},
328+
aggregation: {
329+
kind: "capacity-weighted-v1",
330+
scope: "routable-known",
331+
presentation: "aggregate",
332+
includedAccounts: 2,
333+
excludedAccounts: 0,
334+
unknownPlanAccounts: 0,
335+
partialWindowAccounts: 1,
336+
incomplete: true,
337+
fiveHour: { usedPercent: 10, includedAccounts: 2, excludedAccounts: 0, incomplete: false, updatedAt: now },
338+
customWindows: [{ label: "Burst", usedPercent: 30, includedAccounts: 1, excludedAccounts: 1, incomplete: true, updatedAt: now }],
339+
},
340+
}],
341+
};
342+
await mountShell();
343+
344+
const markers = [...host.querySelectorAll<HTMLElement>(".quota-window-partial")];
345+
expect(markers).toHaveLength(1);
346+
expect(markers[0]?.getAttribute("aria-label")).toBe("Burst: incomplete account coverage");
347+
});

0 commit comments

Comments
 (0)