feat: bitcoin-ui CSS fix, HTTPS proxy support, deploy script improvements
Bitcoin UI: - Replace cdn.tailwindcss.com with locally bundled tailwind.css (CSP blocks external scripts) - Make all asset paths relative for nginx proxy compatibility - Add bitcoin-ui build/deploy to deploy-to-target.sh (was missing entirely) - Use --network host (bitcoin-ui proxies Bitcoin RPC at 127.0.0.1:8332) HTTPS mixed content fix: - Add HTTPS_PROXY_PATHS in AppSession.vue — when parent page is HTTPS, iframe loads through nginx proxy instead of direct HTTP port - Prevents browser blocking HTTP iframes inside HTTPS pages - All Tailscale servers use HTTPS, this was breaking all app iframes Deploy & first-boot improvements: - first-boot-containers.sh auto-detects disk size for pruning vs txindex - first-boot-containers.sh checks fallback source path for UI containers - Added mempool-electrs to APP_PORTS mapping - ElectrumX container creation in first-boot - Podman doctor/fix/uptime skills added Also includes: session persistence, identity management, LND transactions, ElectrumX status UI, nostr-provider improvements, Web5 enhancements Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
4e54b8bd4d
commit
367b483a72
@@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<div class="app-session-root">
|
||||
<Teleport to="body" :disabled="displayMode === 'panel'">
|
||||
<Teleport to="body" :disabled="isInlinePanel">
|
||||
<div
|
||||
:class="backdropClasses"
|
||||
@click.self="goBack"
|
||||
@click.self="handleBackdropClick"
|
||||
>
|
||||
<div
|
||||
ref="sessionRef"
|
||||
@@ -178,12 +178,24 @@
|
||||
import { ref, computed, onMounted, onBeforeUnmount, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { rpcClient } from '@/api/rpc-client'
|
||||
import { useAppLauncherStore } from '@/stores/appLauncher'
|
||||
import NostrIdentityPicker from '@/components/NostrIdentityPicker.vue'
|
||||
|
||||
type DisplayMode = 'panel' | 'overlay' | 'fullscreen'
|
||||
|
||||
const DISPLAY_MODE_KEY = 'archipelago_app_display_mode'
|
||||
|
||||
const props = defineProps<{
|
||||
appIdProp?: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
close: []
|
||||
}>()
|
||||
|
||||
/** True when rendered inline via store (panel mode), false when route-based */
|
||||
const isInlinePanel = computed(() => !!props.appIdProp)
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
@@ -214,6 +226,25 @@ function setMode(mode: DisplayMode) {
|
||||
displayMode.value = mode
|
||||
localStorage.setItem(DISPLAY_MODE_KEY, mode)
|
||||
showModeMenu.value = false
|
||||
|
||||
// Switch from inline panel → route-based overlay/fullscreen
|
||||
if (isInlinePanel.value && mode !== 'panel') {
|
||||
const id = appId.value
|
||||
emit('close')
|
||||
router.push({ name: 'app-session', params: { appId: id } })
|
||||
return
|
||||
}
|
||||
|
||||
// Switch from route-based → inline panel
|
||||
if (!isInlinePanel.value && mode === 'panel') {
|
||||
const id = appId.value
|
||||
const launcher = useAppLauncherStore()
|
||||
router.push({ name: 'apps' }).then(() => {
|
||||
launcher.panelAppId = id
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Enter fullscreen if selected
|
||||
if (mode === 'fullscreen' && sessionRef.value && !document.fullscreenElement) {
|
||||
sessionRef.value.requestFullscreen().catch(() => {})
|
||||
@@ -222,49 +253,92 @@ function setMode(mode: DisplayMode) {
|
||||
|
||||
// Reactive classes based on display mode
|
||||
const backdropClasses = computed(() => {
|
||||
if (displayMode.value === 'overlay' || displayMode.value === 'fullscreen') {
|
||||
return 'app-session-backdrop-overlay'
|
||||
}
|
||||
return 'app-session-backdrop-panel'
|
||||
if (isInlinePanel.value) return 'app-session-backdrop-inline'
|
||||
return 'app-session-backdrop-overlay'
|
||||
})
|
||||
|
||||
const panelClasses = computed(() => {
|
||||
const base = 'app-session-panel glass-card'
|
||||
if (displayMode.value === 'overlay') return `${base} app-session-overlay`
|
||||
if (isInlinePanel.value) return `${base} app-session-inline`
|
||||
if (displayMode.value === 'fullscreen') return `${base} app-session-fullscreen`
|
||||
return `${base} app-session-inpanel`
|
||||
return `${base} app-session-overlay`
|
||||
})
|
||||
|
||||
const appId = computed(() => route.params.appId as string)
|
||||
const appId = computed(() => props.appIdProp || (route.params.appId as string))
|
||||
|
||||
const APP_URLS: Record<string, string> = {
|
||||
// Container apps — use nginx proxy paths (strips X-Frame-Options)
|
||||
/** Container apps: direct port access (avoids root-relative asset breakage under /app/xxx/ proxy) */
|
||||
const APP_PORTS: Record<string, number> = {
|
||||
'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': 9980,
|
||||
'penpot': 9001,
|
||||
'nextcloud': 8085,
|
||||
'vaultwarden': 8082,
|
||||
'jellyfin': 8096,
|
||||
'photoprism': 2342,
|
||||
'immich': 2283,
|
||||
'immich_server': 2283,
|
||||
'filebrowser': 8083,
|
||||
'nginx-proxy-manager': 81,
|
||||
'portainer': 9000,
|
||||
'uptime-kuma': 3001,
|
||||
'tailscale': 8240,
|
||||
'fedimint': 8175,
|
||||
'fedimint-gateway': 8176,
|
||||
'nostr-rs-relay': 18081,
|
||||
'indeedhub': 7777,
|
||||
'dwn': 3100,
|
||||
'endurain': 8080,
|
||||
}
|
||||
|
||||
/** Apps that need nginx proxy for iframe embedding.
|
||||
* IndeedHub loads via direct port 7777 — deploy script removes X-Frame-Options
|
||||
* from the container's internal nginx so iframe works on all servers. */
|
||||
const PROXY_APPS: Record<string, string> = {}
|
||||
|
||||
/** 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). */
|
||||
const HTTPS_PROXY_PATHS: Record<string, string> = {
|
||||
'bitcoin-knots': '/app/bitcoin-ui/',
|
||||
'electrs': '/app/electrs/',
|
||||
'btcpay-server': '/app/btcpay/',
|
||||
'bitcoin-ui': '/app/bitcoin-ui/',
|
||||
'lnd': '/app/lnd/',
|
||||
'electrumx': '/app/electrs/',
|
||||
'electrs': '/app/electrs/',
|
||||
'mempool-electrs': '/app/electrs/',
|
||||
'mempool': '/app/mempool/',
|
||||
'homeassistant': '/app/homeassistant/',
|
||||
'grafana': '/app/grafana/',
|
||||
'mempool-web': '/app/mempool/',
|
||||
'archy-mempool-web': '/app/mempool/',
|
||||
'fedimint': '/app/fedimint/',
|
||||
'fedimint-gateway': '/app/fedimint-gateway/',
|
||||
'jellyfin': '/app/jellyfin/',
|
||||
'searxng': '/app/searxng/',
|
||||
'filebrowser': '/app/filebrowser/',
|
||||
'ollama': '/app/ollama/',
|
||||
'onlyoffice': '/app/onlyoffice/',
|
||||
'penpot': '/app/penpot/',
|
||||
'nextcloud': '/app/nextcloud/',
|
||||
'vaultwarden': '/app/vaultwarden/',
|
||||
'jellyfin': '/app/jellyfin/',
|
||||
'photoprism': '/app/photoprism/',
|
||||
'immich': '/app/immich/',
|
||||
'filebrowser': '/app/filebrowser/',
|
||||
'nginx-proxy-manager': '/app/nginx-proxy-manager/',
|
||||
'portainer': '/app/portainer/',
|
||||
'uptime-kuma': '/app/uptime-kuma/',
|
||||
'immich_server': '/app/immich/',
|
||||
'tailscale': '/app/tailscale/',
|
||||
'fedimint': '/app/fedimint/',
|
||||
'nostr-rs-relay': '/app/nostr-rs-relay/',
|
||||
'endurain': '/app/endurain/',
|
||||
'indeedhub': '/app/indeedhub/',
|
||||
'dwn': '/app/dwn/',
|
||||
'endurain': '/app/endurain/',
|
||||
}
|
||||
|
||||
/** External HTTPS apps — always loaded directly */
|
||||
const EXTERNAL_URLS: Record<string, string> = {
|
||||
'botfights': 'https://botfights.net',
|
||||
'nwnn': 'https://nwnn.l484.com',
|
||||
'484-kitchen': 'https://484.kitchen',
|
||||
@@ -286,15 +360,47 @@ const APP_TITLES: Record<string, string> = {
|
||||
|
||||
const appTitle = computed(() => APP_TITLES[appId.value] || appId.value.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase()))
|
||||
|
||||
/** Apps that set X-Frame-Options and MUST open in a new tab (can't iframe) */
|
||||
const NEW_TAB_APPS = new Set([
|
||||
'btcpay-server', // X-Frame-Options: DENY
|
||||
'grafana', // X-Frame-Options: deny
|
||||
'photoprism', // X-Frame-Options: DENY
|
||||
'homeassistant', // X-Frame-Options: SAMEORIGIN
|
||||
'vaultwarden', // X-Frame-Options: SAMEORIGIN
|
||||
'nextcloud', // X-Frame-Options: SAMEORIGIN
|
||||
'uptime-kuma', // X-Frame-Options: SAMEORIGIN
|
||||
'penpot', // Not reachable / blocks iframe
|
||||
])
|
||||
|
||||
const mustOpenNewTab = computed(() => NEW_TAB_APPS.has(appId.value))
|
||||
|
||||
const appUrl = computed(() => {
|
||||
const url = APP_URLS[appId.value]
|
||||
if (!url) return ''
|
||||
// Proxy paths — same origin
|
||||
if (url.startsWith('/')) return `${window.location.origin}${url}`
|
||||
// External HTTPS sites — direct
|
||||
if (url.startsWith('https://')) return url
|
||||
// Fallback: localhost port URLs (shouldn't reach here normally)
|
||||
return url.replace('localhost', window.location.hostname)
|
||||
const id = appId.value
|
||||
|
||||
// External HTTPS apps — iframe overlay
|
||||
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}`
|
||||
|
||||
// HTTPS: use nginx proxy to avoid mixed content (browser blocks HTTP iframes in HTTPS pages)
|
||||
if (window.location.protocol === 'https:') {
|
||||
const httpsProxy = HTTPS_PROXY_PATHS[id]
|
||||
if (httpsProxy) return `${window.location.origin}${httpsProxy}`
|
||||
}
|
||||
|
||||
// HTTP: direct port access (faster, no proxy overhead)
|
||||
const port = APP_PORTS[id]
|
||||
if (!port) return ''
|
||||
let base = `http://${window.location.hostname}:${port}`
|
||||
|
||||
// Append sub-path from query param (e.g. ?path=/tx/abc123)
|
||||
const subpath = route.query.path as string | undefined
|
||||
if (subpath) base += subpath
|
||||
|
||||
return base
|
||||
})
|
||||
|
||||
// --- Identity ---
|
||||
@@ -325,6 +431,9 @@ function onIdentitySelected(identity: SelectedIdentity) {
|
||||
showIdentityPicker.value = false
|
||||
storeIdentity(identity)
|
||||
sendIdentity(identity)
|
||||
// NIP-98 auto-login disabled — apps like IndeedHub have their own login flow
|
||||
// that properly sets up internal account state. We provide window.nostr via
|
||||
// nostr-provider.js so the app's built-in "Sign In" button works.
|
||||
}
|
||||
|
||||
async function sendIdentity(identity: SelectedIdentity) {
|
||||
@@ -339,6 +448,8 @@ async function sendIdentity(identity: SelectedIdentity) {
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// NIP-98 auto-login removed — apps handle their own login via window.nostr (NIP-07)
|
||||
|
||||
// --- Lifecycle ---
|
||||
|
||||
function onLoad() {
|
||||
@@ -393,7 +504,7 @@ function startLoadTimeout() {
|
||||
|
||||
function openNewTabAndBack() {
|
||||
if (appUrl.value) window.open(appUrl.value, '_blank', 'noopener,noreferrer')
|
||||
goBack()
|
||||
closeSession()
|
||||
}
|
||||
|
||||
function openNewTab() {
|
||||
@@ -408,14 +519,14 @@ function iframeGoForward() {
|
||||
try { iframeRef.value?.contentWindow?.history.forward() } catch {}
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
if (document.fullscreenElement) document.exitFullscreen().catch(() => {})
|
||||
router.back()
|
||||
function handleBackdropClick() {
|
||||
closeSession()
|
||||
}
|
||||
|
||||
function closeSession() {
|
||||
if (document.fullscreenElement) document.exitFullscreen().catch(() => {})
|
||||
router.push({ name: 'apps' })
|
||||
if (isInlinePanel.value) emit('close')
|
||||
else router.push({ name: 'apps' })
|
||||
}
|
||||
|
||||
function onKeyDown(e: KeyboardEvent) {
|
||||
@@ -463,12 +574,18 @@ async function handleNostrRequest(event: MessageEvent) {
|
||||
const { id, method, params } = event.data
|
||||
const source = event.source as Window | null
|
||||
if (!source) return
|
||||
const identityId = getStoredIdentity()?.id || null
|
||||
const storedIdentity = getStoredIdentity()
|
||||
const identityId = storedIdentity?.id || null
|
||||
console.log(`[NIP-07] ${method} identityId=${identityId} storedPubkey=${storedIdentity?.nostr_pubkey?.slice(0, 12) || 'none'}`)
|
||||
|
||||
try {
|
||||
let result: unknown
|
||||
if (method === 'getPublicKey') {
|
||||
if (identityId) {
|
||||
// Use stored nostr_pubkey directly if available (avoids RPC call that may 401)
|
||||
if (storedIdentity?.nostr_pubkey) {
|
||||
result = storedIdentity.nostr_pubkey
|
||||
console.log('[NIP-07] getPublicKey from stored identity:', (result as string).slice(0, 12))
|
||||
} else if (identityId) {
|
||||
const res = await rpcClient.call<{ nostr_pubkey: string }>({ method: 'identity.get', params: { id: identityId } })
|
||||
result = res.nostr_pubkey
|
||||
} else {
|
||||
@@ -476,11 +593,13 @@ async function handleNostrRequest(event: MessageEvent) {
|
||||
result = res.nostr_pubkey
|
||||
}
|
||||
} else if (method === 'signEvent') {
|
||||
console.log(`[NIP-07] signEvent kind=${params.event?.kind} using identity=${identityId || 'node-default'}`)
|
||||
if (identityId) {
|
||||
result = await rpcClient.call<unknown>({ method: 'identity.nostr-sign', params: { id: identityId, event: params.event } })
|
||||
} else {
|
||||
result = await rpcClient.call<unknown>({ method: 'node.nostr-sign', params: { event: params.event } })
|
||||
}
|
||||
console.log('[NIP-07] signEvent OK')
|
||||
} else if (method === 'getRelays') { result = {} }
|
||||
else if (method === 'nip04.encrypt') { result = (await rpcClient.call<{ ciphertext: string }>({ method: 'identity.nostr-encrypt-nip04', params: { id: identityId || undefined, pubkey: params.pubkey, plaintext: params.plaintext } })).ciphertext }
|
||||
else if (method === 'nip04.decrypt') { result = (await rpcClient.call<{ plaintext: string }>({ method: 'identity.nostr-decrypt-nip04', params: { id: identityId || undefined, pubkey: params.pubkey, ciphertext: params.ciphertext } })).plaintext }
|
||||
@@ -489,23 +608,30 @@ async function handleNostrRequest(event: MessageEvent) {
|
||||
else { throw new Error(`Unsupported NIP-07 method: ${method}`) }
|
||||
source.postMessage({ type: 'nostr-response', id, result }, '*')
|
||||
} catch (err) {
|
||||
console.error(`[NIP-07] ${method} FAILED:`, err instanceof Error ? err.message : err)
|
||||
source.postMessage({ type: 'nostr-response', id, error: err instanceof Error ? err.message : 'Unknown error' }, '*')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// Apps that block iframes (X-Frame-Options) — open in new tab, close session
|
||||
if (mustOpenNewTab.value && appUrl.value) {
|
||||
window.open(appUrl.value, '_blank', 'noopener,noreferrer')
|
||||
if (isInlinePanel.value) emit('close')
|
||||
else router.back()
|
||||
return
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', onKeyDown, true)
|
||||
window.addEventListener('message', onMessage)
|
||||
document.addEventListener('click', onClickOutside)
|
||||
document.addEventListener('fullscreenchange', onFullscreenChange)
|
||||
// Known blocked apps — show fallback immediately
|
||||
if (IFRAME_BLOCKED_APPS.has(appId.value)) {
|
||||
loading.value = false
|
||||
iframeBlocked.value = true
|
||||
} else {
|
||||
startLoadTimeout()
|
||||
}
|
||||
// Enter fullscreen if that's the stored mode
|
||||
if (displayMode.value === 'fullscreen') {
|
||||
requestAnimationFrame(() => {
|
||||
sessionRef.value?.requestFullscreen().catch(() => {})
|
||||
@@ -528,19 +654,18 @@ onBeforeUnmount(() => {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
/* Panel mode — edge-to-edge dark overlay with centered glass panel */
|
||||
.app-session-backdrop-panel {
|
||||
/* Inline panel mode — fills content area, no blur, original layout */
|
||||
.app-session-backdrop-inline {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
backdrop-filter: blur(8px);
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.app-session-inpanel {
|
||||
.app-session-inline {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
@@ -550,10 +675,10 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.app-session-backdrop-panel {
|
||||
.app-session-backdrop-inline {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
.app-session-inpanel {
|
||||
.app-session-inline {
|
||||
border-radius: 1rem;
|
||||
max-width: calc(100% - 1rem);
|
||||
max-height: calc(100vh - 6rem);
|
||||
|
||||
Reference in New Issue
Block a user