fix: Show actionable error details for account health check failures

Setup tokens from `claude setup-token` can't query the usage API,
resulting in a useless "Usage API request failed" message. Now shows
the actual HTTP status and guides users to complete OAuth setup.
Also distinguishes warning state (yellow) from error state (red)
in the AccountCard UI.
This commit is contained in:
Lukas May
2026-02-10 13:16:03 +01:00
parent 06f443ebc8
commit 783a07bfb7
2 changed files with 52 additions and 16 deletions

View File

@@ -93,10 +93,14 @@ export type AccountData = {
};
export function AccountCard({ account }: { account: AccountData }) {
const hasWarning = account.credentialsValid && !account.isExhausted && account.error;
const statusIcon = !account.credentialsValid ? (
<XCircle className="h-5 w-5 shrink-0 text-destructive" />
) : account.isExhausted ? (
<AlertTriangle className="h-5 w-5 shrink-0 text-yellow-500" />
) : hasWarning ? (
<AlertTriangle className="h-5 w-5 shrink-0 text-yellow-500" />
) : (
<CheckCircle2 className="h-5 w-5 shrink-0 text-green-500" />
);
@@ -105,7 +109,9 @@ export function AccountCard({ account }: { account: AccountData }) {
? "Invalid credentials"
: account.isExhausted
? `Exhausted until ${account.exhaustedUntil ? new Date(account.exhaustedUntil).toLocaleTimeString() : "unknown"}`
: "Available";
: hasWarning
? "Setup incomplete"
: "Available";
const usage = account.usage;
@@ -189,9 +195,11 @@ export function AccountCard({ account }: { account: AccountData }) {
</div>
)}
{/* Error message */}
{/* Error / warning message */}
{account.error && (
<p className="pl-8 text-xs text-destructive">{account.error}</p>
<p className={`pl-8 text-xs ${hasWarning ? 'text-yellow-600 dark:text-yellow-500' : 'text-destructive'}`}>
{account.error}
</p>
)}
</CardContent>
</Card>

View File

@@ -149,7 +149,12 @@ async function refreshToken(
}
}
async function fetchUsage(accessToken: string): Promise<AccountUsage | null> {
type FetchUsageResult =
| { ok: true; usage: AccountUsage }
| { ok: false; status: number; statusText: string }
| { ok: false; status: 0; statusText: string };
async function fetchUsage(accessToken: string): Promise<FetchUsageResult> {
try {
const response = await fetch(USAGE_API_URL, {
method: 'GET',
@@ -159,17 +164,22 @@ async function fetchUsage(accessToken: string): Promise<AccountUsage | null> {
'Content-Type': 'application/json',
},
});
if (!response.ok) return null;
if (!response.ok) {
return { ok: false, status: response.status, statusText: response.statusText };
}
const data = await response.json();
return {
five_hour: data.five_hour ?? null,
seven_day: data.seven_day ?? null,
seven_day_sonnet: data.seven_day_sonnet ?? null,
seven_day_opus: data.seven_day_opus ?? null,
extra_usage: data.extra_usage ?? null,
ok: true,
usage: {
five_hour: data.five_hour ?? null,
seven_day: data.seven_day ?? null,
seven_day_sonnet: data.seven_day_sonnet ?? null,
seven_day_opus: data.seven_day_opus ?? null,
extra_usage: data.extra_usage ?? null,
},
};
} catch {
return null;
} catch (err) {
return { ok: false, status: 0, statusText: err instanceof Error ? err.message : 'Network error' };
}
}
@@ -280,12 +290,30 @@ export async function checkAccountHealth(
}
}
const usage = await fetchUsage(accessToken);
if (!usage) {
const isSetupToken = !currentExpiresAt;
const usageResult = await fetchUsage(accessToken);
if (!usageResult.ok) {
const statusDetail = usageResult.status > 0
? `HTTP ${usageResult.status} ${usageResult.statusText}`
: usageResult.statusText;
if (isSetupToken) {
// Setup tokens often can't query the usage API — not a hard error
return {
...base,
credentialsValid: true,
tokenValid: true,
tokenExpiresAt: null,
subscriptionType,
error: `Usage API unavailable for setup token (${statusDetail}). Run \`claude\` with this account to complete OAuth setup.`,
};
}
return {
...base,
credentialsValid: true,
error: 'Usage API request failed',
error: `Usage API request failed: ${statusDetail}`,
};
}
@@ -295,7 +323,7 @@ export async function checkAccountHealth(
tokenValid: true,
tokenExpiresAt: currentExpiresAt ? new Date(currentExpiresAt).toISOString() : null,
subscriptionType,
usage,
usage: usageResult.usage,
};
} catch (err) {
return {