frontend: polish app launch and release experience
This commit is contained in:
@@ -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 ''
|
||||
|
||||
Reference in New Issue
Block a user