/** Static configuration maps for app session routing and display */ export type DisplayMode = 'panel' | 'overlay' | 'fullscreen' export const DISPLAY_MODE_KEY = 'archipelago_app_display_mode' /** Container apps: direct port access (avoids root-relative asset breakage under /app/xxx/ proxy) */ export const APP_PORTS: Record = { 'bitcoin-knots': 8334, 'bitcoin-ui': 8334, 'electrumx': 50002, 'electrs': 50002, 'archy-electrs-ui': 50002, 'mempool-electrs': 50002, 'btcpay-server': 23000, 'lnd': 8081, 'archy-lnd-ui': 8081, 'mempool': 4080, 'mempool-web': 4080, 'archy-mempool-web': 4080, 'homeassistant': 8123, 'grafana': 3000, 'searxng': 8888, 'ollama': 11434, 'onlyoffice': 8044, 'penpot': 9001, 'nextcloud': 8085, 'vaultwarden': 8082, 'jellyfin': 8096, 'photoprism': 2342, 'immich': 2283, 'immich_server': 2283, 'filebrowser': 8083, 'nginx-proxy-manager': 8181, 'portainer': 9000, 'uptime-kuma': 3001, 'fedimint': 8175, 'fedimintd': 8175, 'fedimint-gateway': 8176, 'nostr-rs-relay': 18081, 'nostr-vpn': 8201, 'fips': 8202, 'routstr': 8200, 'indeedhub': 7778, 'botfights': 9100, 'gitea': 3000, 'dwn': 3100, 'endurain': 8080, } /** Apps that need nginx proxy for iframe embedding. * IndeedHub loads via /app/indeedhub/ proxy for nostr-provider.js injection * from the container's internal nginx so iframe works on all servers. */ export const PROXY_APPS: Record = { 'gitea': '/app/gitea/', } /** Nginx proxy paths -- used on HTTPS to avoid mixed content (HTTPS parent + HTTP port iframe). * On HTTP, direct port access is used instead (faster, no proxy). */ export const HTTPS_PROXY_PATHS: Record = { 'bitcoin-knots': '/app/bitcoin-ui/', 'bitcoin-ui': '/app/bitcoin-ui/', 'lnd': '/app/lnd/', 'electrumx': '/app/electrs/', 'electrs': '/app/electrs/', 'mempool-electrs': '/app/electrs/', 'mempool': '/app/mempool/', 'mempool-web': '/app/mempool/', 'archy-mempool-web': '/app/mempool/', 'fedimint': '/app/fedimint/', 'fedimintd': '/app/fedimint/', 'fedimint-gateway': '/app/fedimint-gateway/', 'jellyfin': '/app/jellyfin/', 'searxng': '/app/searxng/', 'filebrowser': '/app/filebrowser/', 'ollama': '/app/ollama/', 'onlyoffice': '/app/onlyoffice/', 'immich': '/app/immich/', 'immich_server': '/app/immich/', 'portainer': '/app/portainer/', 'nginx-proxy-manager': '/app/nginx-proxy-manager/', 'uptime-kuma': '/app/uptime-kuma/', 'homeassistant': '/app/homeassistant/', 'vaultwarden': '/app/vaultwarden/', 'photoprism': '/app/photoprism/', 'endurain': '/app/endurain/', 'dwn': '/app/dwn/', 'btcpay-server': '/app/btcpay/', 'nextcloud': '/app/nextcloud/', 'penpot': '/app/penpot/', 'grafana': '/app/grafana/', 'indeedhub': '/app/indeedhub/', 'botfights': '/app/botfights/', 'gitea': '/app/gitea/', 'routstr': '/app/routstr/', 'nostr-vpn': '/app/nostr-vpn/', 'fips': '/app/fips/', } /** External HTTPS apps -- always loaded directly */ export const EXTERNAL_URLS: Record = { 'nwnn': 'https://nwnn.l484.com', '484-kitchen': 'https://484.kitchen', 'call-the-operator': 'https://cta.tx1138.com', 'syntropy-institute': 'https://syntropy.institute', 't-zero': 'https://teeminuszero.net', 'nostrudel': 'https://nostrudel.ninja', 'tailscale': 'https://login.tailscale.com/admin/machines', } export const APP_TITLES: Record = { 'bitcoin-knots': 'Bitcoin', 'btcpay-server': 'BTCPay Server', 'indeedhub': 'Indeehub', 'botfights': 'BotFights', 'gitea': 'Gitea', '484-kitchen': '484 Kitchen', 'arch-presentation': 'Presentation', 'nostr-vpn': 'Nostr VPN', 'fips': 'FIPS', 'routstr': 'Routstr', 'homeassistant': 'Home Assistant', 'uptime-kuma': 'Uptime Kuma', 'nginx-proxy-manager': 'Nginx Proxy Manager', 'nostr-rs-relay': 'Nostr Relay', 'call-the-operator': 'Call The Operator', 'syntropy-institute': 'Syntropy Institute', 't-zero': 'T-Zero', 'nostrudel': 'noStrudel', } /** Apps that set X-Frame-Options and MUST open in a new tab (can't iframe) */ export const NEW_TAB_APPS = new Set([ 'btcpay-server', 'grafana', 'photoprism', 'homeassistant', 'vaultwarden', 'nextcloud', 'uptime-kuma', 'penpot', 'portainer', 'onlyoffice', 'nginx-proxy-manager', 'tailscale', 'routstr', ]) /** Sites known to block iframes -- skip the timeout and go straight to fallback */ export const IFRAME_BLOCKED_APPS = new Set([]) /** Resolve the app URL given its ID and current route query */ export function resolveAppUrl(id: string, routeQueryPath?: string): string { // External HTTPS apps const ext = EXTERNAL_URLS[id] if (ext) return ext // Apps that need nginx proxy (nostr-provider.js injection for NIP-07) const proxyPath = PROXY_APPS[id] if (proxyPath) return `${window.location.origin}${proxyPath}` // IndeedHub: direct port access (nostr-provider.js baked into container image) if (id === 'indeedhub') { const port = APP_PORTS[id] if (port) { let base = `${window.location.protocol}//${window.location.hostname}:${port}` if (routeQueryPath) base += routeQueryPath return base } } // HTTPS: use nginx proxy to avoid mixed content if (window.location.protocol === 'https:') { const httpsProxy = HTTPS_PROXY_PATHS[id] if (httpsProxy) return `${window.location.origin}${httpsProxy}` } // HTTP: direct port access (iframes break with proxy paths due to root-relative assets) const port = APP_PORTS[id] if (!port) return '' let base = `http://${window.location.hostname}:${port}` if (routeQueryPath) base += routeQueryPath return base } /** Resolve a human-readable title for an app */ export function resolveAppTitle(id: string): string { return APP_TITLES[id] || id.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase()) }