feat(ui): auto-tab fallback — embed-refusing apps become tab apps
Demo images / Build & push demo images (push) Successful in 3m49s

An app whose frame never loads while its backend reports Running (the
embed-refusal signature: frame-busting JS, top-level-origin apps,
SameSite=Strict logins — everything the gate's header stripping cannot
fix) is remembered in localStorage; every later launch opens a tab
straight from the click (user gesture, so no popup blocker), and
opensInTab() gives it the tab-launch icon. A successful iframe load
clears the memory and entries expire after 7 days, so nodes that gain
embedding (gate improvements) get re-probed instead of being remembered
broken forever. Dev guide updated; v1.8.2 changelog + What's New curated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
archipelago
2026-08-13 20:23:04 -04:00
co-authored by Claude Fable 5
parent 63cb9dd22c
commit 2399eeac66
6 changed files with 103 additions and 1 deletions
+23
View File
@@ -105,6 +105,7 @@ import { useAppLauncherStore } from '@/stores/appLauncher'
import { useAppStore } from '@/stores/app'
import { useScreensaverStore } from '@/stores/screensaver'
import NostrIdentityPicker from '@/components/NostrIdentityPicker.vue'
import { isAutoTabApp, rememberAutoTabApp, forgetAutoTabApp } from '@/utils/autoTabApps'
import AppSessionHeader from './appSession/AppSessionHeader.vue'
import AppSessionFrame from './appSession/AppSessionFrame.vue'
import MobileGamepad from './appSession/MobileGamepad.vue'
@@ -199,9 +200,27 @@ function updateIsMobile() { isMobile.value = window.innerWidth < 768 }
// same-origin by the mock backend, so the prod new-tab list doesn't apply.
const mustOpenNewTab = computed(() =>
(NEW_TAB_APPS.has(appId.value) && !(IS_DEMO && isDemoApp(appId.value))) ||
// Remembered embed-refusers (frame-busting, top-level-origin apps): the
// first blocked encounter records them, every later launch is a tab app.
isAutoTabApp(appId.value) ||
(IS_DEMO && isDemoExternal(appId.value))
)
// The auto-tab detector: the load-timeout marked the frame blocked, the
// warming-up retry loop has given up, and the backend says the app is
// actually RUNNING — that combination is the embed-refusal signature (a
// down app is "warming up" or shows a blocked reason instead). Remember it
// so this app never shows the dead pane again; a later successful iframe
// load (onLoad) clears the memory, and entries expire on their own.
watch([iframeBlocked, warmingUp], ([blocked, warming]) => {
if (
blocked && !warming && !blockedReason.value &&
(packageEntry.value?.state === PackageState.Running)
) {
rememberAutoTabApp(appId.value)
}
})
// ElectrumX shows a sync screen before its real UI (the Electrum server only
// serves clients once its index is built). Poll /electrs-status while this is
// the Electrum app; pass the status to the frame only while still syncing.
@@ -344,6 +363,10 @@ function onLoad() {
loading.value = false
isRefreshing.value = false
autoRetryCount.value = 0
// A frame that loads embeds fine — heal any stale auto-tab memory (for
// example an app remembered as blocked before the gate learned to strip
// frame headers).
forgetAutoTabApp(appId.value)
// TV/keyboard: hand focus to the app so keys (incl. the gamepad bridge's
// virtual keyboard) flow into the iframe without needing a pointer click.
try { frameRef.value?.iframeRef?.focus() } catch { /* cross-origin is fine */ }