From c8b3300904348dfdee715b8cbd43e60cc00aa21e Mon Sep 17 00:00:00 2001 From: Keysat Date: Fri, 8 May 2026 12:26:29 -0500 Subject: [PATCH] UI: persistent upgrade banner; surface render errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two bugs reported in 0.1.16 testing: 1. Library button shows a toast "Library is a paid feature ... Tap upgrade to unlock" but no Upgrade button is visible anywhere on the page. The free-tier banner only rendered when isLicensed() was false, so a licensed-but-missing-entitlements user got the toast directing them at an invisible button. 2. Clicking the gear (Settings) icon does nothing — the modal never appears. The Activity Log button works on the same page. Fix 1: replace the unlicensed-only renderFreeBanner with renderUpgrade Banner driven by shouldShowUpgradeBanner() = !isProTier(). Copy adapts by tier: • Free: "Free mode · one video at a time · no library, no subs" • Limited license (missing core entitlements): "Limited license — your license is missing some Core features" • Core tier: "Core tier — upgrade to Pro for subs, auto-queue, clips" Pro tier shows nothing. Fix 1b: rewordtoasts on Library / Subscribe clicks to drop the "tap Upgrade" instruction — the persistent banner is the action path now, no need to direct users at a maybe-not-there button. Fix 2 (diagnostic): wrap the main app.innerHTML build in try/catch. A thrown exception in any of the ${...} template substitutions silently aborts the innerHTML assignment, leaving the previous DOM in place — which looks exactly like a button doing nothing when it actually fired and called render(). Now an exception lands in a visible error-box with the message + console trace, so the next reproduction tells us what's actually throwing. --- public/index.html | 71 ++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 58 insertions(+), 13 deletions(-) diff --git a/public/index.html b/public/index.html index 6abc92a..e94f8ee 100644 --- a/public/index.html +++ b/public/index.html @@ -1360,7 +1360,7 @@ // free-tier users, surface the upgrade prompt instead of opening an // empty sidebar. if (!hasEntitlement("history")) { - showToast("Library is a paid feature — keep every summary you process. Tap upgrade to unlock.", "📚"); + showToast("Library requires a paid license — every summary you process gets saved.", "📚"); return; } toggleHistory(); @@ -1369,7 +1369,7 @@ function handleSubscribeClick() { // Subscriptions / channel auto-queue is Pro-only. if (!hasEntitlement("subscriptions")) { - showToast("Channel & podcast subscriptions are a Pro feature. Upgrade to auto-summarize new uploads.", "📡"); + showToast("Channel & podcast subscriptions are a Pro-tier feature.", "📡"); return; } addSubscriptionFromInput(); @@ -1666,10 +1666,37 @@ render(); } - function renderFreeBanner() { + // True for anyone who isn't on the top tier — drives whether to show + // the persistent upgrade banner. Covers: unlicensed, unlicensed-skipped, + // Core (full or partial), and any "licensed but missing entitlements" + // anomaly. Pro users see no banner. + function shouldShowUpgradeBanner() { + if (!state.license || !state.license.loaded) return false; + return !isProTier(); + } + + function renderUpgradeBanner() { const buyUrl = upgradeToProUrl(); + const free = !isLicensed(); + const partialCore = isLicensed() && (!hasEntitlement("history") || !hasEntitlement("library")); + const fullCore = isLicensed() && hasEntitlement("history") && hasEntitlement("library") && !isProTier(); + + let label, descr; + if (free) { + label = "Free mode"; + descr = "one video at a time · no library, no subscriptions"; + } else if (partialCore) { + label = "Limited license"; + descr = "your license is missing some Core features — contact the seller or upgrade"; + } else if (fullCore) { + label = "Core tier"; + descr = "upgrade to Pro for channel & podcast subscriptions, auto-queue, and clips"; + } else { + return ""; // shouldn't reach + } + return ` -
- Free mode - · one video at a time · - no library, no subscriptions + ${label} + · ${descr} Upgrade - + ${free ? ` + + ` : ""}
`; } @@ -1741,7 +1769,9 @@ // a server-side key set via the StartOS config action. const submitDisabled = !state.url.trim() || (!isSubscribeUrl(state.url) && !state.apiKey.trim() && !state.hasServerKey); - app.innerHTML = ` + let __renderedHtml; + try { + __renderedHtml = `
@@ -1835,7 +1865,7 @@ ${state.settingsOpen ? renderSettingsModal() : ""} - ${free ? renderFreeBanner() : ""} + ${shouldShowUpgradeBanner() ? renderUpgradeBanner() : ""} ${isSubscribeUrl(state.url) ? `
${renderSubscribePrompt()}
` : ""} ${state.queue.length > 0 ? renderQueue() : ""} @@ -1852,6 +1882,21 @@ ${state.historyOpen ? renderHistorySidebar() : ""} ${state.clipPanelOpen ? renderClipPanel() : ""} `; + app.innerHTML = __renderedHtml; + } catch (renderErr) { + // A thrown exception inside any ${...} expression silently aborts + // the innerHTML assignment, leaving the previous DOM in place — so + // a button click that calls render() looks like a no-op. Surface + // the actual error instead of leaving the user wondering. + console.error("[render] failed to build HTML:", renderErr); + app.innerHTML = `
+ Render error: ${escHtml(renderErr && renderErr.message || String(renderErr))} +
+ Check the browser console for the full stack trace, or share it with the developer. +
+
`; + return; + } // Re-init player if the yt-player div exists if (state.videoId && ytReady && document.getElementById("yt-player")) { setTimeout(() => initPlayer(state.videoId), 50);