Files
archy-demo/neode-ui/src/stores/container.ts
2026-03-17 02:14:04 +00:00

313 lines
9.0 KiB
TypeScript

// 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<string, string[]> = {
'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<ContainerStatus[]>([])
const healthStatus = ref<Record<string, string>>({})
const loading = ref(false)
const loadingApps = ref<Set<string>>(new Set()) // Track loading state per app
const error = ref<string | null>(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,
}
})