From 4c29f7268f1c2bc1ed24baba244f4a832b9bd40e Mon Sep 17 00:00:00 2001 From: Nariman Jelveh Date: Fri, 23 Oct 2026 14:15:35 -0700 Subject: [PATCH] fix(dashboard): don't show a failed usage request as zero usage getMonthlyUsage() resolves with the error body when the request fails, and Usage and Home read it as a month with nothing used: $0.00 of $0.00, 0%, and on Usage an empty table replacing the one already on screen. A body without a usage report is treated as the failure it is, so the last good numbers stay and a first load shows the "unavailable" note. --- src/gui/src/UI/Dashboard/TabHome.js | 4 ++-- src/gui/src/UI/Dashboard/TabUsage.js | 4 ++-- src/gui/src/UI/Dashboard/usageBudget.js | 15 +++++++++++++++ src/gui/src/UI/Dashboard/usageBudget.test.js | 16 +++++++++++++++- 4 files changed, 34 insertions(+), 5 deletions(-) diff --git a/src/gui/src/UI/Dashboard/TabHome.js b/src/gui/src/UI/Dashboard/TabHome.js index f08de1933..a69a7ea97 100644 --- a/src/gui/src/UI/Dashboard/TabHome.js +++ b/src/gui/src/UI/Dashboard/TabHome.js @@ -19,7 +19,7 @@ import UIWindowSaveAccount from '../UIWindowSaveAccount.js'; import { formatCredits, formatDollarsFromMicrocents, usageIsCredits } from './credits.js'; -import { addonCreditsRemaining, usageBudget } from './usageBudget.js'; +import { addonCreditsRemaining, monthlyUsageOrThrow, usageBudget } from './usageBudget.js'; import { appIconAttrs } from '../../helpers/appIcon.js'; import { filterLaunchableApps } from '../../helpers/launchAppsList.js'; import { isOrgSeat, orgSeatTeamName } from './orgSeat.js'; @@ -644,7 +644,7 @@ const TabHome = { // Load monthly usage data try { - const res = await puter.auth.getMonthlyUsage(); + const res = monthlyUsageOrThrow(await puter.auth.getMonthlyUsage()); const budget = usageBudget(res.usage, res.allowanceInfo); const addonLeft = addonCreditsRemaining(res.allowanceInfo); // The server reports credits (already scaled) or raw amounts diff --git a/src/gui/src/UI/Dashboard/TabUsage.js b/src/gui/src/UI/Dashboard/TabUsage.js index 1436b2f03..c57290fc6 100644 --- a/src/gui/src/UI/Dashboard/TabUsage.js +++ b/src/gui/src/UI/Dashboard/TabUsage.js @@ -19,7 +19,7 @@ import { formatCredits, formatDollarsFromMicrocents, usageIsCredits } from './credits.js'; import { isOrgSeat } from './orgSeat.js'; -import { addonCreditsRemaining, usageBudget } from './usageBudget.js'; +import { addonCreditsRemaining, monthlyUsageOrThrow, usageBudget } from './usageBudget.js'; // Whether the server reported credits — decides whether the usage surfaces // render credits or fall back to dollars. @@ -293,7 +293,7 @@ function renderUsageTable () { } async function update_usage_details ($el_window) { - const monthlyUsagePromise = puter.auth.getMonthlyUsage().then(res => { + const monthlyUsagePromise = puter.auth.getMonthlyUsage().then(monthlyUsageOrThrow).then(res => { const budget = usageBudget(res.usage, res.allowanceInfo); const addonLeft = addonCreditsRemaining(res.allowanceInfo); // The server reports credits (already scaled) or raw amounts (no diff --git a/src/gui/src/UI/Dashboard/usageBudget.js b/src/gui/src/UI/Dashboard/usageBudget.js index 3e82f8471..a9ec1ac7c 100644 --- a/src/gui/src/UI/Dashboard/usageBudget.js +++ b/src/gui/src/UI/Dashboard/usageBudget.js @@ -74,3 +74,18 @@ export const addonCreditsRemaining = (allowanceInfo) => { if ( ! (purchased > 0) ) return null; return Math.max(0, purchased - nonNegative(addons.consumedPurchaseCredits)); }; + +/** + * `getMonthlyUsage()` resolves with whatever body the server answered, an + * error included. Throws unless that body is a usage report, so a failed + * request isn't shown as a month with nothing used. + * + * @param {Object | null | undefined} res - What `getMonthlyUsage()` resolved with. + * @returns {Object} `res`, once checked. + */ +export const monthlyUsageOrThrow = (res) => { + if ( res === null || typeof res?.usage !== 'object' || res.usage === null ) { + throw new Error(res?.message || 'Usage report unavailable'); + } + return res; +}; diff --git a/src/gui/src/UI/Dashboard/usageBudget.test.js b/src/gui/src/UI/Dashboard/usageBudget.test.js index 25daf112c..718e38647 100644 --- a/src/gui/src/UI/Dashboard/usageBudget.test.js +++ b/src/gui/src/UI/Dashboard/usageBudget.test.js @@ -18,7 +18,7 @@ */ import { describe, expect, it } from 'vitest'; -import { addonCreditsRemaining, usageBudget } from './usageBudget.js'; +import { addonCreditsRemaining, monthlyUsageOrThrow, usageBudget } from './usageBudget.js'; const info = (allowance, addons = {}) => ({ monthUsageAllowance: allowance, @@ -147,3 +147,17 @@ describe('addonCreditsRemaining', () => { }))).toBe(1_000); }); }); + +describe('monthlyUsageOrThrow', () => { + it('passes a usage report through', () => { + const res = { usage: { total: 0 }, allowanceInfo: null }; + expect(monthlyUsageOrThrow(res)).toBe(res); + }); + + it('throws on an error body instead of reading it as zero usage', () => { + expect(() => monthlyUsageOrThrow({ message: 'boom' })).toThrow('boom'); + expect(() => monthlyUsageOrThrow({ usage: null })).toThrow(); + expect(() => monthlyUsageOrThrow(null)).toThrow(); + expect(() => monthlyUsageOrThrow(undefined)).toThrow(); + }); +});