frontend: polish app launch and release experience

This commit is contained in:
archipelago
2026-06-11 00:24:40 -04:00
parent c393b96da3
commit 1a3d726eac
140 changed files with 5930 additions and 920 deletions
@@ -37,9 +37,10 @@
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
</svg>
</div>
<h3 class="text-lg font-semibold text-white mb-2">{{ mustOpenNewTab ? 'This app opens in a new tab' : 'App not reachable' }}</h3>
<h3 class="text-lg font-semibold text-white mb-2">{{ blockedReason ? blockedTitle : (mustOpenNewTab ? 'This app opens in a new tab' : 'App not reachable') }}</h3>
<p class="text-white/50 text-sm mb-6">
<template v-if="mustOpenNewTab">{{ appTitle }} sets security headers that prevent iframe embedding.<br>Open it in a new browser tab instead.</template>
<template v-else-if="blockedReason">{{ blockedReason }}<br><span v-if="autoRetryCount > 0" class="text-yellow-400/70">Checking again automatically ({{ autoRetryCount }})...</span></template>
<template v-else>{{ appTitle }} may still be starting up or the container is stopped.<br><span v-if="autoRetryCount > 0" class="text-yellow-400/70">Retrying automatically ({{ autoRetryCount }})...</span></template>
</p>
<div class="flex flex-wrap items-center justify-center gap-3">
@@ -88,6 +89,8 @@ const props = defineProps<{
mustOpenNewTab: boolean
autoRetryCount: number
refreshKey: number
blockedReason?: string
blockedTitle?: string
}>()
const emit = defineEmits<{
@@ -1,11 +1,17 @@
import { describe, expect, it } from 'vitest'
import { NEW_TAB_APPS, resolveAppUrl } from '../appSessionConfig'
import { GENERATED_NEW_TAB_APPS } from '../generatedAppSessionConfig'
describe('appSessionConfig', () => {
it('keeps new-tab apps marked on every viewport', () => {
it('keeps manifest-owned new-tab apps marked on every viewport', () => {
expect(NEW_TAB_APPS.has('btcpay-server')).toBe(true)
expect(NEW_TAB_APPS.has('grafana')).toBe(true)
expect(NEW_TAB_APPS.has('vaultwarden')).toBe(true)
expect(NEW_TAB_APPS.has('photoprism')).toBe(true)
expect(GENERATED_NEW_TAB_APPS.has('photoprism')).toBe(true)
})
it('keeps frontend-only new-tab overrides for apps without generated metadata', () => {
expect(NEW_TAB_APPS.has('tailscale')).toBe(true)
expect(GENERATED_NEW_TAB_APPS.has('tailscale')).toBe(false)
})
it('resolves direct app ports against the current browser host', () => {
@@ -17,7 +23,17 @@ describe('appSessionConfig', () => {
expect(resolveAppUrl('mempool')).toBe('http://192.168.1.228:4080')
expect(resolveAppUrl('indeedhub')).toBe('http://192.168.1.228:7778')
expect(resolveAppUrl('saleor')).toBe('http://192.168.1.228:9011')
expect(resolveAppUrl('botfights')).toBe('http://192.168.1.228:9100')
})
it('uses manifest-generated launch ports for apps outside the manual override list', () => {
Object.defineProperty(window, 'location', {
value: { hostname: '192.168.1.228' },
writable: true,
configurable: true,
})
expect(resolveAppUrl('meshtastic')).toBe('http://192.168.1.228:4403')
})
it('keeps NetBird on the unified dashboard proxy port', () => {
@@ -29,4 +45,14 @@ describe('appSessionConfig', () => {
expect(resolveAppUrl('netbird', undefined, 'http://localhost:8086')).toBe('http://192.168.1.228:8087')
})
it('uses backend runtime URLs for apps with dynamic launch surfaces', () => {
Object.defineProperty(window, 'location', {
value: { hostname: '192.168.1.228' },
writable: true,
configurable: true,
})
expect(resolveAppUrl('filebrowser', undefined, 'http://localhost:18083')).toBe('http://192.168.1.228:18083')
})
})
@@ -1,11 +1,14 @@
/** Static configuration maps for app session routing and display */
import { GENERATED_APP_PORTS, GENERATED_APP_TITLES, GENERATED_NEW_TAB_APPS } from './generatedAppSessionConfig'
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) */
/** Container apps: manifest-generated launch ports plus overrides for companions and aliases. */
export const APP_PORTS: Record<string, number> = {
...GENERATED_APP_PORTS,
'bitcoin-knots': 8334,
'bitcoin-core': 8334,
'bitcoin-ui': 8334,
@@ -14,7 +17,6 @@ export const APP_PORTS: Record<string, number> = {
'archy-electrs-ui': 50002,
'mempool-electrs': 50002,
'btcpay-server': 23000,
'saleor': 9011,
'lnd': 18083,
'archy-lnd-ui': 18083,
'mempool': 4080,
@@ -71,8 +73,9 @@ export const EXTERNAL_URLS: Record<string, string> = {
}
export const APP_TITLES: Record<string, string> = {
...GENERATED_APP_TITLES,
'bitcoin-knots': 'Bitcoin Knots', 'bitcoin-core': 'Bitcoin Core',
'btcpay-server': 'BTCPay Server', 'saleor': 'Saleor', 'indeedhub': 'Indeehub',
'btcpay-server': 'BTCPay Server', 'indeedhub': 'Indeehub',
'botfights': 'BotFights', 'gitea': 'Gitea', '484-kitchen': '484 Kitchen', 'arch-presentation': 'Presentation',
'homeassistant': 'Home Assistant', 'uptime-kuma': 'Uptime Kuma',
'nginx-proxy-manager': 'Nginx Proxy Manager',
@@ -82,17 +85,8 @@ export const APP_TITLES: Record<string, string> = {
/** 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',
'portainer',
'onlyoffice',
...GENERATED_NEW_TAB_APPS,
'nginx-proxy-manager',
'gitea',
'tailscale',
])
@@ -100,7 +94,7 @@ export const NEW_TAB_APPS = new Set([
export const IFRAME_BLOCKED_APPS = new Set<string>([])
/** Resolve app URL using direct port mapping (source of truth) */
export function resolveAppUrl(id: string, routeQueryPath?: string, _runtimeUrl?: string): string {
export function resolveAppUrl(id: string, routeQueryPath?: string, runtimeUrl?: string): string {
// External HTTPS apps
const ext = EXTERNAL_URLS[id]
if (ext) return ext
@@ -109,9 +103,16 @@ export function resolveAppUrl(id: string, routeQueryPath?: string, _runtimeUrl?:
// /app/bitcoin-ui/: the static UI is built for root and renders a blank
// shell when proxied under a path prefix on some nodes.
if (id === 'bitcoin-knots' || id === 'bitcoin-core' || id === 'bitcoin-ui') {
if (import.meta.env.DEV) return '/app/bitcoin-ui/'
return 'http://' + window.location.hostname + ':8334'
}
if (runtimeUrl && id !== 'netbird') {
let base = runtimeUrl.replace(/localhost/i, window.location.hostname)
if (routeQueryPath) base += routeQueryPath
return base
}
// Local apps launch by host port.
const port = APP_PORTS[id]
if (!port) return ''