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-05-19 19:21:43 -04:00
< div
v - if = "appUrl && !iframeBlocked"
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-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-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 >