// Pinia store for container management import { defineStore } from 'pinia' import { ref, computed } from 'vue' import { containerClient, type ContainerStatus } from '@/api/container-client' // Bundled apps that come pre-loaded with Archipelago export interface BundledApp { id: string name: string image: string description: string icon: string ports: { host: number; container: number }[] volumes: { host: string; container: string }[] category: 'bitcoin' | 'lightning' | 'home' | 'other' lan_address?: string // Runtime launch URL from backend } /** Map bundled app ID to the podman container name(s) used for status matching. * Some apps have a different container name than their app ID, or use a * separate UI container (e.g., bitcoin-knots node → bitcoin-ui web container). */ const CONTAINER_NAME_MAP: Record = { 'bitcoin-knots': ['bitcoin-knots', 'bitcoin-ui'], 'lnd': ['lnd', 'archy-lnd-ui'], 'btcpay-server': ['btcpay-server'], 'mempool': ['archy-mempool-web'], 'electrumx': ['archy-electrs-ui', 'electrumx', 'mempool-electrs'], } export const BUNDLED_APPS: BundledApp[] = [ { id: 'bitcoin-knots', name: 'Bitcoin Knots', image: 'localhost/bitcoinknots/bitcoin:29', description: 'Full Bitcoin node with additional features', icon: '₿', ports: [{ host: 8334, container: 80 }], volumes: [{ host: '/var/lib/archipelago/bitcoin', container: '/data' }], category: 'bitcoin', }, { id: 'lnd', name: 'Lightning (LND)', image: 'docker.io/lightninglabs/lnd:v0.18.4-beta', description: 'Lightning Network Daemon for fast Bitcoin payments', icon: '⚡', ports: [{ host: 8081, container: 80 }], volumes: [{ host: '/var/lib/archipelago/lnd', container: '/root/.lnd' }], category: 'lightning', }, { id: 'homeassistant', name: 'Home Assistant', image: 'ghcr.io/home-assistant/home-assistant:stable', description: 'Open source home automation platform', icon: '🏠', ports: [{ host: 8123, container: 8123 }], volumes: [{ host: '/var/lib/archipelago/homeassistant', container: '/config' }], category: 'home', }, { id: 'btcpay-server', name: 'BTCPay Server', image: 'docker.io/btcpayserver/btcpayserver:latest', description: 'Self-hosted Bitcoin payment processor', icon: '💳', ports: [{ host: 23000, container: 49392 }], volumes: [{ host: '/var/lib/archipelago/btcpay', container: '/datadir' }], category: 'bitcoin', }, { id: 'mempool', name: 'Mempool Explorer', image: 'docker.io/mempool/frontend:latest', description: 'Bitcoin blockchain and mempool visualizer', icon: '🔍', ports: [{ host: 4080, container: 8080 }], volumes: [{ host: '/var/lib/archipelago/mempool', container: '/data' }], category: 'bitcoin', }, { id: 'tailscale', name: 'Tailscale VPN', image: 'docker.io/tailscale/tailscale:latest', description: 'Zero-config VPN mesh network', icon: '🔒', ports: [], volumes: [{ host: '/var/lib/archipelago/tailscale', container: '/var/lib/tailscale' }], category: 'other', }, ] export const useContainerStore = defineStore('container', () => { // State const containers = ref([]) const healthStatus = ref>({}) const loading = ref(false) const loadingApps = ref>(new Set()) // Track loading state per app const error = ref(null) // Getters const runningContainers = computed(() => containers.value.filter(c => c.state === 'running') ) const stoppedContainers = computed(() => containers.value.filter(c => c.state === 'stopped' || c.state === 'exited') ) const getContainerById = computed(() => (id: string) => containers.value.find(c => c.name.includes(id)) ) const getHealthStatus = computed(() => (appId: string) => healthStatus.value[appId] || 'unknown' ) // Get container for a bundled app (matches by explicit name map, then by exact name) const getContainerForApp = computed(() => (appId: string) => { const nameList = CONTAINER_NAME_MAP[appId] if (nameList) { // Try each known container name in priority order for (const n of nameList) { const found = containers.value.find(c => c.name === n) if (found) return found } } // Fallback: exact match on app ID return containers.value.find(c => c.name === appId) }) // Check if an app is currently loading (starting/stopping) const isAppLoading = computed(() => (appId: string) => loadingApps.value.has(appId) ) // Get app state: 'running', 'stopped', 'not-installed' const getAppState = computed(() => (appId: string) => { const container = getContainerForApp.value(appId) if (!container) return 'not-installed' return container.state }) // Get enriched bundled apps with runtime data (like lan_address) const enrichedBundledApps = computed(() => { return BUNDLED_APPS.map(app => { const container = getContainerForApp.value(app.id) return { ...app, lan_address: container?.lan_address } }) }) // Actions async function fetchContainers() { loading.value = true error.value = null try { containers.value = await containerClient.listContainers() } catch (e) { error.value = e instanceof Error ? e.message : 'Failed to fetch containers' if (import.meta.env.DEV) console.error('Failed to fetch containers:', e) } finally { loading.value = false } } async function fetchHealthStatus() { try { healthStatus.value = await containerClient.getHealthStatus() } catch (e) { if (import.meta.env.DEV) console.error('Failed to fetch health status:', e) } } async function installApp(manifestPath: string) { loading.value = true error.value = null try { const containerName = await containerClient.installApp(manifestPath) await fetchContainers() return containerName } catch (e) { error.value = e instanceof Error ? e.message : 'Failed to install app' throw e } finally { loading.value = false } } async function startContainer(appId: string) { loadingApps.value.add(appId) error.value = null try { await containerClient.startContainer(appId) await fetchContainers() await fetchHealthStatus() } catch (e) { error.value = e instanceof Error ? e.message : 'Failed to start container' throw e } finally { loadingApps.value.delete(appId) } } async function stopContainer(appId: string) { loadingApps.value.add(appId) error.value = null try { await containerClient.stopContainer(appId) await fetchContainers() } catch (e) { error.value = e instanceof Error ? e.message : 'Failed to stop container' throw e } finally { loadingApps.value.delete(appId) } } // Start a bundled app (creates and starts container) async function startBundledApp(app: BundledApp) { loadingApps.value.add(app.id) error.value = null try { await containerClient.startBundledApp(app) await fetchContainers() await fetchHealthStatus() } catch (e) { error.value = e instanceof Error ? e.message : 'Failed to start app' throw e } finally { loadingApps.value.delete(app.id) } } // Stop a bundled app async function stopBundledApp(appId: string) { loadingApps.value.add(appId) error.value = null try { await containerClient.stopBundledApp(appId) await fetchContainers() } catch (e) { error.value = e instanceof Error ? e.message : 'Failed to stop app' throw e } finally { loadingApps.value.delete(appId) } } async function removeContainer(appId: string) { loading.value = true error.value = null try { await containerClient.removeContainer(appId) await fetchContainers() } catch (e) { error.value = e instanceof Error ? e.message : 'Failed to remove container' throw e } finally { loading.value = false } } async function getContainerLogs(appId: string, lines: number = 100) { try { return await containerClient.getContainerLogs(appId, lines) } catch (e) { error.value = e instanceof Error ? e.message : 'Failed to get logs' throw e } } async function getContainerStatus(appId: string) { try { return await containerClient.getContainerStatus(appId) } catch (e) { error.value = e instanceof Error ? e.message : 'Failed to get status' throw e } } return { // State containers, healthStatus, loading, loadingApps, error, // Getters runningContainers, stoppedContainers, getContainerById, getHealthStatus, getContainerForApp, isAppLoading, getAppState, enrichedBundledApps, // Actions fetchContainers, fetchHealthStatus, installApp, startContainer, stopContainer, removeContainer, getContainerLogs, getContainerStatus, startBundledApp, stopBundledApp, } })