2026-03-22 03:30:21 +00:00
< template >
fix: overhaul container lifecycle — recovery, health, uninstall, UI state
Container recovery:
- Health monitor: MAX_RESTART_ATTEMPTS 3→10, interval 60s→120s
- Dependency-aware restarts: won't restart services before their deps
- Reset dependent counters when a dependency recovers
- Handle "created" state containers (were invisible to health monitor)
- Added IndeedHub, mempool-api, mysql to tier system
- Crash recovery: podman start timeout 30s→120s with retry
- Podman client: socket timeout 5s→30s, added restart policy
UI state representation:
- Exit code 0 shows "stopped" (gray), not "crashed" (red)
- Exit code 137 shows "killed (OOM)"
- Non-zero exit shows "crashed" (red)
- Added exit_code field to PackageDataEntry
Install/uninstall fixes:
- Install returns error when container doesn't start (was silent success)
- Post-install hooks awaited instead of fire-and-forget tokio::spawn
- Uninstall: graceful rm before force, volume prune, network cleanup
- Uninstall returns error on partial failure (was 200 OK)
Config consistency:
- DB passwords read from /var/lib/archipelago/secrets/ (was hardcoded)
- Bitcoin: added ZMQ ports 28332/28333 for LND block notifications
- IndeedHub port 7777→8190 (was conflicting with strfry)
- Marketplace versions: LND 0.17.4→0.18.4, Mempool 2.5.0→3.0.0
Performance:
- Metrics collector interval 60s→300s (was duplicating health monitor)
- Podman client: proper error propagation instead of unwrap_or_default
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 07:03:57 +01:00
< div class = "relative flex-1 min-h-0 bg-black/40 overflow-hidden app-session-frame-safe" >
2026-03-22 03:30:21 +00:00
< Transition name = "content-fade" >
< div v-if = "loading" class="absolute inset-0 z-10 flex items-center justify-center bg-black/40" >
< svg class = "animate-spin h-8 w-8 text-blue-400" viewBox = "0 0 24 24" fill = "none" >
< circle class = "opacity-25" cx = "12" cy = "12" r = "10" stroke = "currentColor" stroke -width = " 4 " / >
< path class = "opacity-75" fill = "currentColor" d = "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" / >
< / svg >
< / div >
< / Transition >
2026-06-13 04:49:32 -04:00
<!-- ElectrumX sync screen — shown before the real UI while the on - chain
index is still being built ( the Electrum server can ' t serve clients
until then ) . Mirrors the Fedimint Guardian "wait page" design . -- >
< Transition name = "content-fade" >
< div v-if = "electrsSync" class="absolute inset-0 z-10 flex flex-col items-center justify-center" >
< div class = "text-center px-8 w-full max-w-md" >
< div class = "w-16 h-16 mx-auto mb-4 rounded-2xl bg-white/5 border border-white/10 flex items-center justify-center" >
< svg class = "w-8 h-8 text-orange-300 animate-pulse" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >
< path stroke -linecap = " round " stroke -linejoin = " round " stroke -width = " 1.5 " d = "M4 7v10a2 2 0 002 2h12a2 2 0 002-2V7M4 7l8 5 8-5M4 7l8-4 8 4" / >
< / svg >
< / div >
< h3 class = "text-lg font-semibold text-white mb-2" > { { appTitle } } is syncing < / h3 >
< p class = "text-white/50 text-sm mb-5" >
ElectrumX is building its index from the blockchain . The UI opens
automatically once it ' s ready — you can keep using the rest of Archipelago .
< / p >
< div class = "w-full h-2 rounded-full bg-white/10 overflow-hidden mb-2" >
< div
class = "h-full bg-orange-400/80 transition-all duration-700"
: style = "{ width: `${Math.min(100, Math.max(2, electrsSync.progress_pct)).toFixed(1)}%` }"
> < / div >
< / div >
< p class = "text-white/70 text-sm font-medium mb-1" > { { electrsSync . progress _pct . toFixed ( 1 ) } } % < / p >
< p class = "text-white/40 text-xs" >
Block { { electrsSync . indexed _height . toLocaleString ( ) } } of { { electrsSync . network _height . toLocaleString ( ) } }
< template v-if = "electrsSync.index_size" > · {{ electrsSync.index_size }} indexed < / template >
< / p >
< p v-if = "electrsSync.stale" class="text-yellow-400/70 text-xs mt-2" > Reconnecting to ElectrumX … < / p >
< / div >
< / div >
< / Transition >
2026-05-19 19:21:43 -04:00
< div
2026-06-13 04:49:32 -04:00
v - if = "appUrl && !iframeBlocked && !electrsSync"
2026-05-19 19:21:43 -04:00
class = "absolute inset-0 app-session-frame-scroll-host"
tabindex = "-1"
@ pointerdown = "focusIframe"
@ focusin = "focusIframe"
>
2026-05-19 18:29:04 -04:00
< iframe
ref = "iframeRef"
: key = "refreshKey"
: src = "appUrl"
class = "w-full h-full border-0 iframe-scrollbar-hide"
title = "App content"
tabindex = "0"
2026-05-19 19:21:43 -04:00
@ load = "handleIframeLoad"
2026-05-19 18:29:04 -04:00
@ error = "$emit('iframeError')"
/ >
< / div >
2026-03-22 03:30:21 +00:00
<!-- Iframe blocked fallback -- >
< Transition name = "content-fade" >
< div v-if = "iframeBlocked" class="absolute inset-0 z-10 flex flex-col items-center justify-center" >
< div class = "text-center px-8" >
< div class = "w-16 h-16 mx-auto mb-4 rounded-2xl bg-white/5 border border-white/10 flex items-center justify-center" >
< svg class = "w-8 h-8 text-white/40" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >
< path stroke -linecap = " round " stroke -linejoin = " round " stroke -width = " 1.5 " d = "M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" / >
< / svg >
< / div >
2026-06-11 00:24:40 -04:00
< h3 class = "text-lg font-semibold text-white mb-2" > { { blockedReason ? blockedTitle : ( mustOpenNewTab ? 'This app opens in a new tab' : 'App not reachable' ) } } < / h3 >
2026-03-22 03:30:21 +00:00
< p class = "text-white/50 text-sm mb-6" >
< template v-if = "mustOpenNewTab" > {{ appTitle }} sets security headers that prevent iframe embedding. < br > Open it in a new browser tab instead . < / template >
2026-06-11 00:24:40 -04:00
< template v -else -if = " blockedReason " > { { blockedReason } } < br > < span v-if = "autoRetryCount > 0" class="text-yellow-400/70" > Checking again automatically ( {{ autoRetryCount }} ) ... < / span > < / template >
2026-03-22 03:30:21 +00:00
< template v-else > {{ appTitle }} may still be starting up or the container is stopped. < br > < span v-if = "autoRetryCount > 0" class="text-yellow-400/70" > Retrying automatically ( {{ autoRetryCount }} ) ... < / span > < / template >
< / p >
2026-05-06 09:23:57 -04:00
< div class = "flex flex-wrap items-center justify-center gap-3" >
2026-03-22 03:30:21 +00:00
< button
v - if = "!mustOpenNewTab"
@ click = "$emit('refresh')"
class = "glass-button px-6 py-3 rounded-lg text-sm font-semibold inline-flex items-center gap-2"
>
< svg class = "w-4 h-4" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >
< path stroke -linecap = " round " stroke -linejoin = " round " stroke -width = " 2 " d = "M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" / >
< / svg >
Retry now
< / button >
< button
@ click = "$emit('openNewTabAndBack')"
class = "glass-button px-6 py-3 rounded-lg text-sm font-semibold inline-flex items-center gap-2"
>
< svg class = "w-4 h-4" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >
< path stroke -linecap = " round " stroke -linejoin = " round " stroke -width = " 2 " d = "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" / >
< / svg >
Open in new tab
< / button >
< / div >
< / div >
< / div >
< / Transition >
< div v-if = "!appUrl" class="absolute inset-0 flex items-center justify-center" >
< div class = "text-center px-8" >
< h3 class = "text-lg font-semibold text-white mb-2" > App not configured < / h3 >
< p class = "text-white/50 text-sm" > No URL found for { { appId } } < / p >
< / div >
< / div >
< / div >
< / template >
< script setup lang = "ts" >
2026-05-19 18:29:04 -04:00
import { nextTick , ref , watch } from 'vue'
2026-06-13 04:49:32 -04:00
import type { ElectrsSyncStatus } from '@/composables/useElectrsSync'
2026-03-22 03:30:21 +00:00
2026-05-19 18:29:04 -04:00
const props = defineProps < {
2026-03-22 03:30:21 +00:00
appUrl : string
appId : string
appTitle : string
loading : boolean
iframeBlocked : boolean
mustOpenNewTab : boolean
autoRetryCount : number
refreshKey : number
2026-06-11 00:24:40 -04:00
blockedReason ? : string
blockedTitle ? : string
2026-06-13 04:49:32 -04:00
// Non-null only for ElectrumX while its index is still building — shows the
// sync screen and gates the iframe until status flips to "synced".
electrsSync ? : ElectrsSyncStatus | null
2026-03-22 03:30:21 +00:00
} > ( )
2026-05-19 19:21:43 -04:00
const emit = defineEmits < {
2026-03-22 03:30:21 +00:00
iframeLoad : [ ]
iframeError : [ ]
refresh : [ ]
openNewTabAndBack : [ ]
} > ( )
const iframeRef = ref < HTMLIFrameElement | null > ( null )
2026-05-19 19:21:43 -04:00
function focusIframe ( ) {
iframeRef . value ? . focus ( { preventScroll : true } )
}
async function handleIframeLoad ( ) {
emit ( 'iframeLoad' )
await nextTick ( )
requestAnimationFrame ( focusIframe )
}
2026-05-19 18:29:04 -04:00
watch ( ( ) => [ props . appUrl , props . refreshKey , props . iframeBlocked ] , async ( ) => {
if ( ! props . appUrl || props . iframeBlocked ) return
await nextTick ( )
2026-05-19 19:21:43 -04:00
requestAnimationFrame ( focusIframe )
2026-05-19 18:29:04 -04:00
} , { immediate : true } )
2026-03-22 03:30:21 +00:00
defineExpose ( { iframeRef } )
< / script >