Merge runtime token recovery and quota review fixes

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Devin FoleyandPaperclip committed 2026-10-04 00:33:31 -07:00
commit 60f7e0981d
10 files changed
+195 -56

No files matched your search

@@ -1,5 +1,6 @@
import { renderToStaticMarkup } from "react-dom/server";
import { describe, expect, it } from "vitest";
import { formatDateTime } from "../lib/utils";
import { AccountQuotaPanels } from "./AccountQuotaPanels";
describe("account quota panels", () => {
@@ -18,7 +19,7 @@ describe("account quota panels", () => {
{ provider: "openai", accountKey: "a", ok: true, windows: [{ label: "Credits", usedPercent: null, resetsAt: "2026-10-05T00:00:00Z", valueLabel: "$5 remaining" }] },
]} />);
expect(html).toContain("$5 remaining");
expect(html).toContain(`Resets ${new Date("2026-10-05T00:00:00Z").toLocaleString()}`);
expect(html).toContain(`Resets ${formatDateTime("2026-10-05T00:00:00Z")}`);
expect(html).not.toContain("0%");
expect(html).not.toContain('role="progressbar"');
});
+6 -4
View File
@@ -1,4 +1,5 @@
import type { ProviderQuotaResult } from "@paperclipai/shared";
import { formatDateTime } from "../lib/utils";
import { QuotaBar } from "./QuotaBar";
import { quotaUnavailableMessage } from "../lib/quota-refresh";
@@ -32,11 +33,12 @@ export function AccountQuotaPanels({
<span>{window.label}</span>
<span className="text-muted-foreground">{window.valueLabel ?? "Usage not reported"}</span>
</div>
{window.resetsAt && <p className="text-xs text-muted-foreground">Resets {new Date(window.resetsAt).toLocaleString()}</p>}
{window.resetsAt && <p className="text-xs font-mono text-muted-foreground">Resets {formatDateTime(window.resetsAt)}</p>}
</div>
) : (
<QuotaBar
key={`${window.label}:${index}`}
className="font-mono"
label={window.label}
percentUsed={window.usedPercent}
leftLabel={
@@ -47,14 +49,14 @@ export function AccountQuotaPanels({
}
rightLabel={
window.resetsAt
? `Resets ${new Date(window.resetsAt).toLocaleString()}`
? `Resets ${formatDateTime(window.resetsAt)}`
: undefined
}
/>
))}
{account.capturedAt && (
<p className="text-xs text-muted-foreground">
Last checked {new Date(account.capturedAt).toLocaleString()}
<p className="text-xs font-mono text-muted-foreground">
Last checked {formatDateTime(account.capturedAt)}
</p>
)}
</section>
+3 -1
View File
@@ -35,6 +35,7 @@ interface ProviderQuotaCardProps {
quotaSource?: string | null;
quotaLoading?: boolean;
quotaAccounts?: ProviderQuotaResult[];
quotaRequestFailed?: boolean;
}
export function ProviderQuotaCard({
@@ -50,6 +51,7 @@ export function ProviderQuotaCard({
quotaSource = null,
quotaLoading = false,
quotaAccounts,
quotaRequestFailed,
}: ProviderQuotaCardProps) {
// single-pass aggregation over rows — memoized so the 8 derived values are not
// recomputed on every parent render tick (providers tab polls every 30s, and each
@@ -325,7 +327,7 @@ export function ProviderQuotaCard({
{quotaLoading ? (
<QuotaPanelSkeleton />
) : quotaAccounts ? (
<AccountQuotaPanels accounts={quotaAccounts} />
<AccountQuotaPanels accounts={quotaAccounts} failed={quotaRequestFailed} />
) : isClaudeQuotaPanel ? (
<ClaudeSubscriptionPanel windows={quotaWindows} source={quotaSource} error={quotaError} />
) : isCodexQuotaPanel ? (
+8 -2
View File
@@ -332,7 +332,7 @@ export function Costs() {
staleTime: 10_000,
});
const { data: quotaData, isLoading: quotaLoading } = useQuery({
const { data: quotaData, isLoading: quotaLoading, error: quotaFetchError } = useQuery({
queryKey: queryKeys.usageQuotaWindows(companyId),
queryFn: () => costsApi.quotaWindows(companyId),
structuralSharing: (previous, incoming) => retainQuotaWindows(
@@ -405,8 +405,12 @@ export function Costs() {
for (const result of quotaData ?? []) {
if (!result.ok && result.error) map.set(result.provider, result.error);
}
if (quotaFetchError) {
map.set("anthropic", "unavailable");
map.set("openai", "unavailable");
}
return map;
}, [quotaData]);
}, [quotaData, quotaFetchError]);
const quotaSourcesByProvider = useMemo(() => {
const map = new Map<string, string>();
@@ -980,6 +984,7 @@ export function Costs() {
windowRows={windowSpendByProvider.get(provider) ?? []}
showDeficitNotch={deficitNotchByProvider.get(provider) ?? false}
quotaAccounts={quotaData?.filter(account => account.provider === provider)}
quotaRequestFailed={Boolean(quotaFetchError)}
quotaWindows={quotaWindowsByProvider.get(provider) ?? []}
quotaError={quotaErrorsByProvider.get(provider) ?? null}
quotaSource={quotaSourcesByProvider.get(provider) ?? null}
@@ -1001,6 +1006,7 @@ export function Costs() {
windowRows={windowSpendByProvider.get(provider) ?? []}
showDeficitNotch={deficitNotchByProvider.get(provider) ?? false}
quotaAccounts={quotaData?.filter(account => account.provider === provider)}
quotaRequestFailed={Boolean(quotaFetchError)}
quotaWindows={quotaWindowsByProvider.get(provider) ?? []}
quotaError={quotaErrorsByProvider.get(provider) ?? null}
quotaSource={quotaSourcesByProvider.get(provider) ?? null}
+15
View File
@@ -5,6 +5,7 @@ import { MemoryRouter } from "react-router-dom";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { Costs } from "./Costs";
import { Costs as ProductionCosts } from "./Costs.production";
import { SidebarProvider } from "../context/SidebarContext";
const api = vi.hoisted(() => Object.fromEntries([
@@ -34,6 +35,20 @@ describe("Costs account quota polling", () => {
});
afterEach(() => { act(() => root.unmount()); client.clear(); container.remove(); vi.unstubAllGlobals(); });
const flush = async () => { await act(async () => { await new Promise(resolve => setTimeout(resolve, 10)); }); };
it.each([Costs, ProductionCosts])("keeps quota readings with a safe notice when the endpoint rejects (%#)", async (Page) => {
await act(async () => root.render(<MemoryRouter><SidebarProvider><QueryClientProvider client={client}><Page /></QueryClientProvider></SidebarProvider></MemoryRouter>));
await flush();
const tab = [...container.querySelectorAll<HTMLButtonElement>('[role="tab"]')].find(button => button.textContent === "Providers")!;
await act(async () => { tab.dispatchEvent(new MouseEvent("mousedown", { bubbles: true, button: 0 })); tab.click(); });
await flush();
expect(container.textContent).toContain("37% used");
api.quotaWindows.mockRejectedValue(new Error("Raw provider diagnostic: secret command"));
await act(async () => { await client.invalidateQueries({ queryKey }); }); await flush();
expect(container.textContent).toContain("37% used");
expect(container.textContent).toContain("Showing the last available quota");
expect(container.textContent).not.toContain("Raw provider diagnostic");
expect(client.getQueryData(queryKey)).toMatchObject([{ capturedAt: observed.capturedAt }]);
});
it.each(["provider_unavailable", "permission_denied"])("retains the observation on %s but clears rejected or rotated credentials", async (errorFamily) => {
await act(async () => root.render(<MemoryRouter><SidebarProvider><QueryClientProvider client={client}><Costs initialTab="providers" /></QueryClientProvider></SidebarProvider></MemoryRouter>));
await flush();
+8 -2
View File
@@ -353,7 +353,7 @@ export function Costs({
staleTime: 10_000,
});
const { data: quotaData, isLoading: quotaLoading } = useQuery({
const { data: quotaData, isLoading: quotaLoading, error: quotaFetchError } = useQuery({
queryKey: queryKeys.usageQuotaWindows(companyId),
queryFn: () => costsApi.quotaWindows(companyId),
structuralSharing: (previous, incoming) => retainQuotaWindows(
@@ -426,8 +426,12 @@ export function Costs({
for (const result of quotaData ?? []) {
if (!result.ok && result.error) map.set(result.provider, result.error);
}
if (quotaFetchError) {
map.set("anthropic", "unavailable");
map.set("openai", "unavailable");
}
return map;
}, [quotaData]);
}, [quotaData, quotaFetchError]);
const quotaSourcesByProvider = useMemo(() => {
const map = new Map<string, string>();
@@ -1008,6 +1012,7 @@ export function Costs({
windowRows={windowSpendByProvider.get(provider) ?? []}
showDeficitNotch={deficitNotchByProvider.get(provider) ?? false}
quotaAccounts={quotaData?.filter(account => account.provider === provider)}
quotaRequestFailed={Boolean(quotaFetchError)}
quotaWindows={quotaWindowsByProvider.get(provider) ?? []}
quotaError={quotaErrorsByProvider.get(provider) ?? null}
quotaSource={quotaSourcesByProvider.get(provider) ?? null}
@@ -1029,6 +1034,7 @@ export function Costs({
windowRows={windowSpendByProvider.get(provider) ?? []}
showDeficitNotch={deficitNotchByProvider.get(provider) ?? false}
quotaAccounts={quotaData?.filter(account => account.provider === provider)}
quotaRequestFailed={Boolean(quotaFetchError)}
quotaWindows={quotaWindowsByProvider.get(provider) ?? []}
quotaError={quotaErrorsByProvider.get(provider) ?? null}
quotaSource={quotaSourcesByProvider.get(provider) ?? null}