/** * AppDetails data: URL maps, route-to-package mappings, aliases, and status helpers. * Extracted from AppDetails.vue to keep the view under 500 lines. */ import { PackageState } from '@/types/api' /** Web-only app detection (no container -- external websites) */ export const WEB_ONLY_APP_URLS: Record = { 'nwnn': 'https://nwnn.l484.com', '484-kitchen': 'https://484.kitchen', 'call-the-operator': 'https://cta.tx1138.com', 'arch-presentation': 'https://present.l484.com', 'syntropy-institute': 'https://syntropy.institute', 't-zero': 'https://teeminuszero.net', } /** Map route/marketplace app IDs to backend package keys (container names). */ export const ROUTE_TO_PACKAGE_KEY: Record = { mempool: 'mempool', 'mempool-electrs': 'mempool-electrs', electrs: 'mempool-electrs', btcpay: 'btcpay-server', 'btcpay-server': 'btcpay-server', fedimint: 'fedimint', 'fedimint-gateway': 'fedimint-gateway', lnd: 'lnd', 'lnd-ui': 'lnd', bitcoin: 'bitcoin-knots', 'bitcoin-knots': 'bitcoin-knots', homeassistant: 'homeassistant', 'home-assistant': 'homeassistant', grafana: 'grafana', searxng: 'searxng', ollama: 'ollama', onlyoffice: 'onlyoffice', nextcloud: 'nextcloud', vaultwarden: 'vaultwarden', jellyfin: 'jellyfin', photoprism: 'photoprism', immich: 'immich', filebrowser: 'filebrowser', 'nginx-proxy-manager': 'nginx-proxy-manager', 'gitea': 'gitea', portainer: 'portainer', 'uptime-kuma': 'uptime-kuma', tailscale: 'tailscale', netbird: 'netbird', } /** Backend may register under variant container names */ export const PACKAGE_ALIASES: Record = { immich: ['immich_server', 'immich-server'], nextcloud: ['nextcloud-aio', 'nextcloud-server'], } export function resolvePackageKey(routeId: string): string { return ROUTE_TO_PACKAGE_KEY[routeId] ?? routeId } /** Apps that depend on Bitcoin being synced */ export const BITCOIN_DEPENDENT_APPS = ['lnd', 'electrumx', 'electrs', 'mempool-electrs', 'btcpay-server', 'btcpayserver'] /** App launch URLs for dev and prod environments */ export const APP_URLS: Record = { 'lorabell': { dev: 'http://192.168.1.166', prod: 'http://192.168.1.166' }, 'atob': { dev: 'http://localhost:8102', prod: 'https://app.atobitcoin.io' }, 'k484': { dev: 'http://localhost:8103', prod: 'http://localhost:8103' }, 'bitcoin': { dev: 'http://localhost:8332', prod: 'http://localhost:8332' }, 'btcpay-server': { dev: 'http://localhost:23000', prod: 'http://localhost:23000' }, 'homeassistant': { dev: 'http://localhost:8123', prod: 'http://localhost:8123' }, 'grafana': { dev: 'http://localhost:3000', prod: 'http://localhost:3000' }, 'endurain': { dev: 'http://localhost:8080', prod: 'http://localhost:8080' }, 'fedimint': { dev: 'http://localhost:8175', prod: 'http://192.168.1.228:8175' }, 'fedimint-gateway': { dev: 'http://localhost:8176', prod: 'http://192.168.1.228:8176' }, 'morphos-server': { dev: 'http://localhost:8081', prod: 'http://localhost:8081' }, 'lightning-stack': { dev: 'http://localhost:9735', prod: 'http://localhost:9735' }, 'mempool': { dev: 'http://localhost:4080', prod: 'http://localhost:4080' }, 'ollama': { dev: 'http://localhost:11434', prod: 'http://localhost:11434' }, 'searxng': { dev: 'http://localhost:8888', prod: 'http://localhost:8888' }, 'onlyoffice': { dev: 'http://localhost:9980', prod: 'http://localhost:9980' }, 'nextcloud': { dev: 'http://localhost:8085', prod: 'http://localhost:8085' }, 'vaultwarden': { dev: 'http://localhost:8082', prod: 'http://localhost:8082' }, 'jellyfin': { dev: 'http://localhost:8096', prod: 'http://localhost:8096' }, 'photoprism': { dev: 'http://localhost:2342', prod: 'http://localhost:2342' }, 'immich': { dev: 'http://localhost:2283', prod: 'http://localhost:2283' }, 'filebrowser': { dev: 'http://localhost:8083', prod: 'http://localhost:8083' }, 'nginx-proxy-manager': { dev: 'http://localhost:8081', prod: 'http://localhost:8081' }, 'gitea': { dev: 'http://localhost:3001', prod: 'http://localhost:3001' }, 'portainer': { dev: 'http://localhost:9000', prod: 'http://localhost:9000' }, 'uptime-kuma': { dev: 'http://localhost:3002', prod: 'http://localhost:3002' }, 'tailscale': { dev: 'http://localhost:8240', prod: 'http://localhost:8240' }, 'lnd': { dev: 'http://localhost:18083', prod: 'http://localhost:18083' }, 'bitcoin-knots': { dev: 'http://localhost:8334', prod: 'http://localhost:8334' }, 'botfights': { dev: 'http://localhost:9100', prod: 'http://localhost:9100' }, 'nwnn': { dev: 'https://nwnn.l484.com', prod: 'https://nwnn.l484.com' }, '484-kitchen': { dev: 'https://484.kitchen', prod: 'https://484.kitchen' }, 'call-the-operator': { dev: 'https://cta.tx1138.com', prod: 'https://cta.tx1138.com' }, 'arch-presentation': { dev: 'https://present.l484.com', prod: 'https://present.l484.com' }, 'syntropy-institute': { dev: 'https://syntropy.institute', prod: 'https://syntropy.institute' }, 't-zero': { dev: 'https://teeminuszero.net', prod: 'https://teeminuszero.net' }, } /** V3 onion addresses are 56+ chars + .onion. Placeholders like "btcpay.onion" are not real. */ export function isRealOnionAddress(addr: string | undefined): boolean { return !!(addr && addr.endsWith('.onion') && addr.length >= 60 && addr.length <= 70) } export function getStatusClass(state: PackageState, health?: string | null, exitCode?: number | null): string { if (state === PackageState.Running && health === 'starting') return 'bg-yellow-500/20 text-yellow-200 border border-yellow-500/30' if (state === PackageState.Running && health === 'unhealthy') return 'bg-orange-500/20 text-orange-200 border border-orange-500/30' switch (state) { case PackageState.Running: return 'bg-green-500/20 text-green-200 border border-green-500/30' case PackageState.Stopped: return 'bg-gray-500/20 text-gray-200 border border-gray-500/30' case PackageState.Exited: return exitCode != null && exitCode !== 0 ? 'bg-red-500/20 text-red-200 border border-red-500/30' : 'bg-gray-500/20 text-gray-200 border border-gray-500/30' case PackageState.Starting: case PackageState.Stopping: case PackageState.Restarting: return 'bg-yellow-500/20 text-yellow-200 border border-yellow-500/30' case PackageState.Installing: return 'bg-blue-500/20 text-blue-200 border border-blue-500/30' case PackageState.Updating: return 'bg-orange-500/20 text-orange-200 border border-orange-500/30' default: return 'bg-gray-500/20 text-gray-200 border border-gray-500/30' } } export function getStatusDotClass(state: PackageState, health?: string | null, exitCode?: number | null): string { if (state === PackageState.Running && health === 'starting') return 'bg-yellow-400 animate-pulse' if (state === PackageState.Running && health === 'unhealthy') return 'bg-orange-400 animate-pulse' switch (state) { case PackageState.Running: return 'bg-green-400' case PackageState.Stopped: return 'bg-gray-400' case PackageState.Exited: return exitCode != null && exitCode !== 0 ? 'bg-red-400 animate-pulse' : 'bg-gray-400' case PackageState.Starting: case PackageState.Stopping: case PackageState.Restarting: return 'bg-yellow-400 animate-pulse' case PackageState.Installing: return 'bg-blue-400 animate-pulse' case PackageState.Updating: return 'bg-orange-400 animate-pulse' default: return 'bg-gray-400' } } export function getStatusLabel(state: PackageState, health?: string | null, exitCode?: number | null): string { if (state === PackageState.Updating) return 'updating...' if (state === PackageState.Running && health === 'starting') return 'starting up' if (state === PackageState.Running && health === 'unhealthy') return 'unhealthy' if (state === PackageState.Running && health === 'healthy') return 'healthy' if (state === PackageState.Exited) { if (exitCode === 137) return 'killed (SIGKILL)' if (exitCode != null && exitCode !== 0) return 'crashed' return 'stopped' } return state }