diff --git a/docs/post-1.9.0-progress-20261006.md b/docs/post-1.9.0-progress-20261006.md
index 9b76b2af..a01ab3c8 100644
--- a/docs/post-1.9.0-progress-20261006.md
+++ b/docs/post-1.9.0-progress-20261006.md
@@ -741,3 +741,11 @@ passed 1,299 tests but timed out in a Bitcoin modal case and then failed its nex
case; all 20 targeted tests including both modal cases pass on rerun. A new full
run with reduced concurrency and no simultaneous build is required and running.
No failing full run is recorded as passed.
+
+The reduced-concurrency full rerun passed **1,301/1,301 tests in 163 files**
+(`/tmp/archy-slow-app-launch-full-tests-final.log`). Review then found the same
+iframe-removal defect in the custom-app overlay's 15-second timeout. It now keeps
+the frame alive too, sharing the same small loading notice. Three additional
+legacy-overlay tests pass, together with all nine frame tests; both launch paths
+are included in the served-browser harness. The final build and deployment for
+this extension are pending; physical companion acceptance is not implied.
diff --git a/neode-ui/src/components/AppLauncherOverlay.vue b/neode-ui/src/components/AppLauncherOverlay.vue
index 73bc97bc..03ced061 100644
--- a/neode-ui/src/components/AppLauncherOverlay.vue
+++ b/neode-ui/src/components/AppLauncherOverlay.vue
@@ -92,6 +92,9 @@
@error="onIframeError"
/>
+
+
@@ -210,6 +213,7 @@ import { useAppLauncherStore } from '@/stores/appLauncher'
import NostrSignConsent from '@/components/NostrSignConsent.vue'
import NostrIdentityPicker from '@/components/NostrIdentityPicker.vue'
import AppLoadingScreen from '@/components/AppLoadingScreen.vue'
+import AppSlowLoadNotice from '@/components/AppSlowLoadNotice.vue'
import PaymentSuccessPane, { type SuccessRow } from '@/components/PaymentSuccessPane.vue'
import { DEFAULT_APP_ICON } from '@/views/apps/appsConfig'
import { rpcClient } from '@/api/rpc-client'
@@ -234,6 +238,7 @@ const iframeRefreshKey = ref(0)
const isRefreshing = ref(false)
const iframeLoading = ref(true)
const iframeBlocked = ref(false)
+const slowLoad = ref(false)
// Best-guess icon for the loading screen — resolved from the /app/{id}/ path
// when present; AppLoadingScreen's falls back to the default icon if the
@@ -366,12 +371,14 @@ function refreshIframe() {
isRefreshing.value = true
iframeLoading.value = true
iframeBlocked.value = false
+ slowLoad.value = false
clearTimers()
iframeRefreshKey.value++
loadTimeoutId = setTimeout(() => {
if (iframeLoading.value) {
iframeLoading.value = false
- iframeBlocked.value = true
+ isRefreshing.value = false
+ slowLoad.value = true
}
}, 15000)
}
@@ -400,6 +407,8 @@ function openInNewTabAndClose() {
}
function onIframeLoad() {
+ slowLoad.value = false
+ iframeBlocked.value = false
injectScrollbarHideIfSameOrigin()
isRefreshing.value = false
iframeLoading.value = false
@@ -413,6 +422,8 @@ function onIframeLoad() {
}
function onIframeError() {
+ slowLoad.value = false
+ isRefreshing.value = false
clearTimers()
iframeLoading.value = false
iframeBlocked.value = true
@@ -654,12 +665,14 @@ watch(
if (open) {
iframeLoading.value = true
iframeBlocked.value = false
+ slowLoad.value = false
clearTimers()
- // Set max load timeout — if iframe never fires load, show fallback
+ // Keep a slow navigation alive; its eventual load clears this notice.
loadTimeoutId = setTimeout(() => {
if (iframeLoading.value) {
iframeLoading.value = false
- iframeBlocked.value = true
+ isRefreshing.value = false
+ slowLoad.value = true
}
}, 15000)
closeBtnRef.value?.focus()
@@ -667,6 +680,7 @@ watch(
isRefreshing.value = false
iframeLoading.value = true
iframeBlocked.value = false
+ slowLoad.value = false
clearTimers()
// Clear any pending payment when closing
if (pendingPayment.value) {
diff --git a/neode-ui/src/components/AppSlowLoadNotice.vue b/neode-ui/src/components/AppSlowLoadNotice.vue
new file mode 100644
index 00000000..bf2b14b9
--- /dev/null
+++ b/neode-ui/src/components/AppSlowLoadNotice.vue
@@ -0,0 +1,15 @@
+
+
+ {{ title }} is taking longer to load. You can keep waiting.
+
+
+