Skip to content

Commit 47cae08

Browse files
committed
Mark accounts as errored on refresh
1 parent 45d15bc commit 47cae08

3 files changed

Lines changed: 105 additions & 22 deletions

File tree

frontend/src/App.css

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -670,6 +670,13 @@ select {
670670
-webkit-user-select: none;
671671
}
672672

673+
.account-main-error {
674+
color: var(--danger);
675+
text-decoration: underline dotted;
676+
text-underline-offset: 0.15em;
677+
cursor: help;
678+
}
679+
673680
.drag-handle {
674681
width: 2.25rem;
675682
height: 2.25rem;

frontend/src/App.tsx

Lines changed: 68 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,39 @@ const accountMetaLine = (account: AccountSummary): string | null => {
304304
return null;
305305
};
306306

307+
const DEFAULT_REFRESH_ERROR_MESSAGE = "Credits refresh failed.";
308+
309+
const normalizeRefreshErrorMessage = (value: string | null | undefined): string => {
310+
const trimmed = (value ?? "").trim();
311+
return trimmed.length > 0 ? trimmed : DEFAULT_REFRESH_ERROR_MESSAGE;
312+
};
313+
314+
const makeErroredCreditsInfo = (message: string, previous?: CreditsInfo): CreditsInfo => ({
315+
available: null,
316+
used: null,
317+
total: null,
318+
currency: previous?.currency ?? "USD",
319+
source: previous?.source ?? "wham_usage",
320+
mode: previous?.mode ?? "balance",
321+
unit: previous?.unit ?? "USD",
322+
planType: previous?.planType ?? null,
323+
isPaidPlan: previous?.isPaidPlan ?? false,
324+
hourlyRemainingPercent: null,
325+
weeklyRemainingPercent: null,
326+
hourlyRefreshAt: null,
327+
weeklyRefreshAt: null,
328+
status: "error",
329+
message: normalizeRefreshErrorMessage(message),
330+
checkedAt: nowEpoch(),
331+
});
332+
333+
const refreshErrorMessageForCredits = (credits: CreditsInfo | undefined): string | null => {
334+
if (!credits || credits.status !== "error") {
335+
return null;
336+
}
337+
return normalizeRefreshErrorMessage(credits.message);
338+
};
339+
307340
const applyTheme = (theme: Theme) => {
308341
document.documentElement.dataset.theme = theme;
309342
localStorage.setItem("codex-manager-theme", theme);
@@ -543,6 +576,15 @@ function App() {
543576
});
544577
};
545578

579+
const setAccountRefreshError = (accountId: string, reason: unknown): string => {
580+
const message = normalizeRefreshErrorMessage(reason instanceof Error ? reason.message : String(reason));
581+
setCreditsById((current) => ({
582+
...current,
583+
[accountId]: makeErroredCreditsInfo(message, current[accountId]),
584+
}));
585+
return message;
586+
};
587+
546588
const accountStateForBucket = (bucket: AccountBucket): AccountSummary["state"] => {
547589
if (bucket === "depleted") {
548590
return "archived";
@@ -710,8 +752,8 @@ function App() {
710752
failureMessages.push(`Credits check issue: ${credits.message}`);
711753
}
712754
} catch (creditsError) {
713-
const rendered = creditsError instanceof Error ? creditsError.message : String(creditsError);
714-
failureMessages.push(rendered);
755+
const message = setAccountRefreshError(id, creditsError);
756+
failureMessages.push(`Credits check issue: ${message}`);
715757
} finally {
716758
markRefreshing([id], false);
717759
}
@@ -747,6 +789,9 @@ function App() {
747789
} else {
748790
setError(`Credits check issue: ${credits.message}`);
749791
}
792+
} catch (creditsError) {
793+
const message = setAccountRefreshError(id, creditsError);
794+
setError(`Credits check issue: ${message}`);
750795
} finally {
751796
markRefreshing([id], false);
752797
}
@@ -1853,6 +1898,7 @@ function App() {
18531898
<For each={activeAccounts()}>
18541899
{(account) => {
18551900
const credits = () => creditsById()[account.id];
1901+
const refreshErrorMessage = () => refreshErrorMessageForCredits(credits());
18561902
const refreshRows = () => usageRefreshRows(credits(), nowTick(), usageRefreshDisplayMode());
18571903
const isCurrent = () => view()?.activeAccountId === account.id;
18581904

@@ -1874,7 +1920,12 @@ function App() {
18741920
<IconDragHandle />
18751921
</span>
18761922
<div class="account-main">
1877-
<p class="account-title account-main-value">{accountTitle(account)}</p>
1923+
<p
1924+
class={`account-title account-main-value ${refreshErrorMessage() ? "account-main-error" : ""}`}
1925+
title={refreshErrorMessage() ?? undefined}
1926+
>
1927+
{accountTitle(account)}
1928+
</p>
18781929
</div>
18791930
<Show when={isCurrent()}>
18801931
<p class="pill pill-active">ACTIVE</p>
@@ -2047,6 +2098,7 @@ function App() {
20472098
<For each={depletedAccounts()}>
20482099
{(account) => {
20492100
const credits = () => creditsById()[account.id];
2101+
const refreshErrorMessage = () => refreshErrorMessageForCredits(credits());
20502102
const refreshRows = () => usageRefreshRows(credits(), nowTick(), usageRefreshDisplayMode());
20512103

20522104
return (
@@ -2065,7 +2117,12 @@ function App() {
20652117
<IconDragHandle />
20662118
</span>
20672119
<div class="account-main">
2068-
<p class="account-title account-main-value">{accountTitle(account)}</p>
2120+
<p
2121+
class={`account-title account-main-value ${refreshErrorMessage() ? "account-main-error" : ""}`}
2122+
title={refreshErrorMessage() ?? undefined}
2123+
>
2124+
{accountTitle(account)}
2125+
</p>
20692126
</div>
20702127
</header>
20712128

@@ -2229,6 +2286,7 @@ function App() {
22292286
{(account) => {
22302287
const credits = () => creditsById()[account.id];
22312288
const availablePercent = () => quotaRemainingPercent(credits());
2289+
const refreshErrorMessage = () => refreshErrorMessageForCredits(credits());
22322290
const refreshRows = () => usageRefreshRows(credits(), nowTick(), usageRefreshDisplayMode());
22332291

22342292
return (
@@ -2247,7 +2305,12 @@ function App() {
22472305
<IconDragHandle />
22482306
</span>
22492307
<div class="account-main">
2250-
<p class="account-title account-main-value">{accountTitle(account)}</p>
2308+
<p
2309+
class={`account-title account-main-value ${refreshErrorMessage() ? "account-main-error" : ""}`}
2310+
title={refreshErrorMessage() ?? undefined}
2311+
>
2312+
{accountTitle(account)}
2313+
</p>
22512314
</div>
22522315
</header>
22532316

frontend/src/lib/codexAuth.ts

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ const normalizeUsageRefreshDisplayMode = (value: unknown): "date" | "remaining"
172172
return value === "remaining" ? "remaining" : "date";
173173
};
174174

175-
const defaultCreditsInfo = (message: string): CreditsInfo => ({
175+
const errorCreditsInfo = (message: string): CreditsInfo => ({
176176
available: null,
177177
used: null,
178178
total: null,
@@ -186,8 +186,8 @@ const defaultCreditsInfo = (message: string): CreditsInfo => ({
186186
weeklyRemainingPercent: null,
187187
hourlyRefreshAt: null,
188188
weeklyRefreshAt: null,
189-
status: "unavailable",
190-
message,
189+
status: "error",
190+
message: message.trim().length > 0 ? message.trim() : "Credits refresh failed.",
191191
checkedAt: nowEpoch(),
192192
});
193193

@@ -634,24 +634,37 @@ export const getRemainingCreditsForAccount = async (id: string): Promise<Credits
634634
}
635635

636636
const pending = (async (): Promise<CreditsInfo> => {
637-
const tauri = await loadBackendApis();
638-
639-
while (true) {
640-
const payload = await tauri.invoke<unknown>("refresh_account_usage", { accountId: id });
641-
const record = asRecord(payload);
642-
if (record) {
643-
const inFlight = valueAsBoolean(record.inFlight) ?? false;
644-
if (inFlight) {
645-
await new Promise((resolve) => window.setTimeout(resolve, 500));
646-
continue;
637+
try {
638+
const tauri = await loadBackendApis();
639+
640+
while (true) {
641+
let payload: unknown;
642+
try {
643+
payload = await tauri.invoke<unknown>("refresh_account_usage", { accountId: id });
644+
} catch (invokeError) {
645+
const rendered = invokeError instanceof Error ? invokeError.message : String(invokeError);
646+
return errorCreditsInfo(rendered);
647647
}
648648

649-
const credits = asCreditsInfo(record.credits);
650-
if (credits) {
651-
return credits;
649+
const record = asRecord(payload);
650+
if (record) {
651+
const inFlight = valueAsBoolean(record.inFlight) ?? false;
652+
if (inFlight) {
653+
await new Promise((resolve) => window.setTimeout(resolve, 500));
654+
continue;
655+
}
656+
657+
const credits = asCreditsInfo(record.credits);
658+
if (credits) {
659+
return credits;
660+
}
652661
}
662+
663+
return errorCreditsInfo("Usage refresh returned an invalid response.");
653664
}
654-
return defaultCreditsInfo("No usage data available for this account.");
665+
} catch (refreshError) {
666+
const rendered = refreshError instanceof Error ? refreshError.message : String(refreshError);
667+
return errorCreditsInfo(rendered);
655668
}
656669
})();
657670

0 commit comments

Comments
 (0)