feat(FN-767): add Claude session reset time to usage modal

- Preserve Claude reset timestamps in the usage data model (usage.ts)
- Display session reset time in the UsageIndicator dashboard component
- Add component comments explaining reset-time rendering logic
- Add styles for the reset time display in the usage modal
- Add tests for usage data model and UsageIndicator component
This commit is contained in:
gsxdsm
2026-04-02 23:32:22 -07:00
parent 95374b7c3d
commit 3697bd0d71
6 changed files with 234 additions and 6 deletions

View File

@@ -734,6 +734,66 @@ describe("usage", () => {
_resetSleepFn();
});
it("populates resetAt timestamp for session window from API response", async () => {
const resetTime = new Date(Date.now() + 2 * 60 * 60 * 1000);
setupClaudeMocks({
credFileContent: {
accessToken: "test-token",
scopes: ["user:profile"],
subscriptionType: "pro",
},
});
setupClaudeApiResponse({
five_hour: {
utilization: 45.5,
resets_at: resetTime.toISOString(),
},
seven_day: {
utilization: 23.0,
resets_at: new Date(Date.now() + 5 * 24 * 60 * 60 * 1000).toISOString(),
},
});
const providers = await fetchAllProviderUsage();
const claude = providers.find((p) => p.name === "Claude")!;
expect(claude.status).toBe("ok");
expect(claude.windows).toHaveLength(2);
const sessionWindow = claude.windows.find((w) => w.label.includes("Session"))!;
expect(sessionWindow.resetAt).toBe(resetTime.toISOString());
expect(sessionWindow.resetText).toContain("resets in");
const weeklyWindow = claude.windows.find((w) => w.label === "Weekly")!;
expect(weeklyWindow.resetAt).toBeDefined();
});
it("omits resetAt when API response has no resets_at field", async () => {
setupClaudeMocks({
credFileContent: {
accessToken: "test-token",
scopes: ["user:profile"],
subscriptionType: "pro",
},
});
setupClaudeApiResponse({
five_hour: {
utilization: 30.0,
// no resets_at field
},
});
const providers = await fetchAllProviderUsage();
const claude = providers.find((p) => p.name === "Claude")!;
expect(claude.status).toBe("ok");
const sessionWindow = claude.windows.find((w) => w.label.includes("Session"))!;
expect(sessionWindow.resetAt).toBeUndefined();
expect(sessionWindow.resetText).toBeNull();
});
it("handles empty JSON object from API gracefully", async () => {
setupClaudeMocks({
credFileContent: {

View File

@@ -22,6 +22,7 @@ export interface UsageWindow {
percentLeft: number; // 0-100
resetText: string | null; // e.g., "resets in 2h"
resetMs?: number; // ms until reset
resetAt?: string; // ISO 8601 timestamp of when the window resets (machine-readable)
windowDurationMs?: number; // total window length
pace?: UsagePace; // pace indicator for weekly windows
}
@@ -651,6 +652,7 @@ async function fetchClaudeUsageViaCli(): Promise<ProviderUsage> {
if (iso) {
const msLeft = new Date(iso).getTime() - Date.now();
window.resetMs = msLeft > 0 ? msLeft : 0;
window.resetAt = iso;
if (!window.resetText) {
window.resetText = msLeft > 0 ? `resets in ${formatDuration(msLeft)}` : "resetting now";
}
@@ -833,9 +835,9 @@ async function fetchClaudeUsage(): Promise<ProviderUsage> {
let resetText: string | null = null;
let resetMs: number | undefined;
const resetAt = w.resets_at || w.reset_at || w.resetAt;
if (resetAt) {
const msLeft = new Date(resetAt).getTime() - Date.now();
const resetAtValue = w.resets_at || w.reset_at || w.resetAt;
if (resetAtValue) {
const msLeft = new Date(resetAtValue).getTime() - Date.now();
resetMs = msLeft > 0 ? msLeft : 0;
resetText = msLeft > 0 ? `resets in ${formatDuration(msLeft)}` : "resetting now";
}
@@ -847,6 +849,7 @@ async function fetchClaudeUsage(): Promise<ProviderUsage> {
resetText,
windowDurationMs,
resetMs,
resetAt: resetAtValue || undefined,
};
};