From 01f6076bcb867193a38d39e9ee20d131a0034366 Mon Sep 17 00:00:00 2001 From: jelveh Date: Sat, 18 Jul 2026 16:57:53 -0700 Subject: [PATCH] fix: keep the Dashboard Usage tab stable through transient failures - A failed refresh no longer wipes an already-rendered usage table; the unavailable note only shows when there is nothing on screen yet (mirrors the Apps grid's error handling). - The plan-button check now re-checks indefinitely with backoff instead of giving up after ~10s, so an arbitrarily late UIUpgradeAccount attachment can't leave a subscriber without the button all session. --- src/gui/src/UI/Dashboard/TabUsage.js | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/src/gui/src/UI/Dashboard/TabUsage.js b/src/gui/src/UI/Dashboard/TabUsage.js index 114fd912b..11836cff6 100644 --- a/src/gui/src/UI/Dashboard/TabUsage.js +++ b/src/gui/src/UI/Dashboard/TabUsage.js @@ -139,20 +139,21 @@ function refreshUsageDetails ($el_window) { let planBtnRetryTimer = null; -function setupPlanButton ($el_window, attempts = 40) { +function setupPlanButton ($el_window, retryDelay = 250) { const $planBtn = $($el_window).find('.usage-plan-btn'); clearTimeout(planBtnRetryTimer); // UIUpgradeAccount is only present on hosted puter.com; on a self-hosted // install the button has no working target, so hide it entirely rather // than showing a dead control. Hosted deployments can attach it *after* - // the dashboard initializes, though — keep re-checking for a while so a - // load-order race can't leave a subscriber without the button for the - // whole session. + // the dashboard initializes, though — keep re-checking (backing off to a + // slow poll) so a load-order race can't leave a subscriber without the + // button for the whole session, no matter how late the script lands. if ( typeof window.UIUpgradeAccount !== 'function' ) { $planBtn.hide(); - if ( attempts > 0 ) { - planBtnRetryTimer = setTimeout(() => setupPlanButton($el_window, attempts - 1), 250); - } + planBtnRetryTimer = setTimeout( + () => setupPlanButton($el_window, Math.min(retryDelay * 1.5, 2000)), + retryDelay, + ); return; } const hasSubscription = window.user?.subscription?.active; @@ -316,10 +317,14 @@ async function update_usage_details ($el_window) { renderUsageTable(); }).catch(err => { console.error('Failed to load monthly usage:', err); - usageTableData = []; - $('.driver-usage-details-content').html( - '

Usage details are unavailable right now.

', - ); + // Only show the failure note when nothing has rendered yet — a + // transient refresh error must not wipe a table the user is already + // looking at (mirrors the Apps grid's error handling). + if ( usageTableData.length === 0 ) { + $('.driver-usage-details-content').html( + '

Usage details are unavailable right now.

', + ); + } }); const spacePromise = puter.fs.space().then(res => {