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>
This commit is contained in:
Dorian
2026-03-31 07:03:57 +01:00
co-authored by Claude Opus 4.6
parent 795e74bc50
commit 1e283daf13
65 changed files with 3950 additions and 298 deletions
+84 -32
View File
@@ -38,16 +38,29 @@
@open-new-tab-and-back="openNewTabAndBack"
/>
<!-- Mobile: floating glass close button -->
<button
class="md:hidden app-session-mobile-close"
aria-label="Close"
@click="closeSession"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
<!-- Mobile bottom browser bar part of flex layout, doesn't overlay content -->
<div class="md:hidden app-session-mobile-bar">
<button class="app-session-bar-btn" aria-label="Back" @click="iframeGoBack">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
</svg>
</button>
<button class="app-session-bar-btn" aria-label="Forward" @click="iframeGoForward">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</button>
<button class="app-session-bar-btn" aria-label="Open in new tab" @click="openNewTab">
<svg 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>
</button>
<button class="app-session-bar-btn" aria-label="Close" @click="closeSession">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
<NostrIdentityPicker
@@ -116,7 +129,10 @@ const appId = computed(() => {
})
const appTitle = computed(() => resolveAppTitle(appId.value))
const mustOpenNewTab = computed(() => NEW_TAB_APPS.has(appId.value))
const isMobile = typeof window !== 'undefined' && window.innerWidth < 768
// On mobile (Android WebView), all apps load in the iframe — X-Frame-Options
// doesn't apply since the WebView is the top-level browsing context.
const mustOpenNewTab = computed(() => isMobile ? false : NEW_TAB_APPS.has(appId.value))
const appUrl = computed(() => {
return resolveAppUrl(appId.value, route.query.path as string | undefined)
@@ -347,6 +363,7 @@ onBeforeUnmount(() => {
width: 100%;
height: 100%;
border-radius: 0;
border: none;
}
@media (min-width: 768px) {
@@ -389,7 +406,8 @@ onBeforeUnmount(() => {
width: 100%;
height: 100%;
border-radius: 0;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.6);
border: none;
box-shadow: none;
}
@media (min-width: 768px) {
@@ -472,29 +490,63 @@ onBeforeUnmount(() => {
opacity: 0;
}
/* Mobile floating glass close button */
.app-session-mobile-close {
position: fixed;
bottom: calc(24px + env(safe-area-inset-bottom, 0px));
left: 50%;
transform: translateX(-50%);
z-index: 2500;
width: 48px;
height: 48px;
/* Mobile: full-bleed app sessions — no border, no radius, no shadow */
@media (max-width: 767px) {
.app-session-panel.glass-card {
border: none !important;
border-radius: 0 !important;
box-shadow: none !important;
}
.app-session-backdrop-overlay {
padding: 0;
backdrop-filter: none;
background: black;
}
/* Iframe frame: push content below status bar on mobile */
.app-session-frame-safe {
padding-top: var(--safe-area-top, env(safe-area-inset-top, 0px));
}
/* Iframe within padded container: fill remaining space */
.app-session-frame-safe iframe {
top: var(--safe-area-top, env(safe-area-inset-top, 0px));
height: calc(100% - var(--safe-area-top, env(safe-area-inset-top, 0px)));
}
}
/* Mobile bottom browser bar — sized like the main tab bar.
Uses !important-free display so Tailwind md:hidden can override. */
@media (min-width: 768px) {
.app-session-mobile-bar { display: none !important; }
}
.app-session-mobile-bar {
display: flex;
justify-content: space-around;
align-items: center;
flex-shrink: 0;
padding: 12px 16px;
padding-bottom: calc(12px + var(--safe-area-bottom, env(safe-area-inset-bottom, 0px)));
background: rgba(0, 0, 0, 0.25);
backdrop-filter: blur(18px);
-webkit-backdrop-filter: blur(18px);
border-top: 1px solid rgba(255, 255, 255, 0.06);
}
.app-session-bar-btn {
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: rgba(0, 0, 0, 0.45);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border: 1px solid rgba(255, 255, 255, 0.15);
color: rgba(255, 255, 255, 0.85);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
transition: background 0.15s ease, transform 0.15s ease;
width: 56px;
height: 56px;
border-radius: 14px;
color: rgba(255, 255, 255, 0.65);
transition: color 0.15s ease, background 0.15s ease;
}
.app-session-mobile-close:active {
background: rgba(0, 0, 0, 0.65);
transform: translateX(-50%) scale(0.9);
.app-session-bar-btn svg {
width: 24px;
height: 24px;
}
.app-session-bar-btn:active {
color: white;
background: rgba(255, 255, 255, 0.12);
}
</style>