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.
This commit is contained in:
Nariman Jelveh committed 2026-10-23 14:15:35 -07:00
1 parent d364509b91
commit 4c29f7268f
4 files changed
+34 -5

No files matched your search

+2 -2
View File
@@ -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
+2 -2
View File
@@ -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
+15
View File
@@ -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;
};
+15 -1
View File
@@ -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();
});
});