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
@@ -3,97 +3,59 @@ import { ref, watch } from 'vue'
|
||||
import { rpcClient } from '@/api/rpc-client'
|
||||
import router from '@/router'
|
||||
|
||||
/** Hostnames of external sites that block iframes via X-Frame-Options or CSP.
|
||||
* These always open in a new tab. Other external sites load directly in the iframe. */
|
||||
/** Legacy: these used to open in new tabs. Now all apps go through AppSession. */
|
||||
const IFRAME_BLOCKED_HOSTS: string[] = []
|
||||
|
||||
/** External site proxy paths — disabled. External URLs load directly in the iframe
|
||||
* via their standard https:// URL. The /ext/ subpath approach broke SPAs. */
|
||||
const EXTERNAL_PROXY_PATH: Record<string, string> = {}
|
||||
/** Ports of apps that set X-Frame-Options (can't iframe, must open in new tab) */
|
||||
const NEW_TAB_PORTS = new Set([
|
||||
'23000', // BTCPay — X-Frame-Options: DENY
|
||||
'3000', // Grafana — X-Frame-Options: deny
|
||||
'2342', // PhotoPrism — X-Frame-Options: DENY
|
||||
'8123', // Home Assistant — X-Frame-Options: SAMEORIGIN
|
||||
'8082', // Vaultwarden — X-Frame-Options: SAMEORIGIN
|
||||
'8085', // Nextcloud — X-Frame-Options: SAMEORIGIN
|
||||
'3001', // Uptime Kuma — X-Frame-Options: SAMEORIGIN
|
||||
'9001', // Penpot — not reachable
|
||||
// IndeedHub (7777) uses proxy path for NIP-07 nostr-provider.js — NOT new tab
|
||||
])
|
||||
|
||||
function mustOpenInNewTab(url: string): boolean {
|
||||
try {
|
||||
const u = new URL(url)
|
||||
// External sites that block iframes
|
||||
if (IFRAME_BLOCKED_HOSTS.some(h => u.hostname === h || u.hostname.endsWith(`.${h}`))) {
|
||||
return true
|
||||
}
|
||||
// Local apps with X-Frame-Options or CSP frame-ancestors blocking iframes
|
||||
if (
|
||||
u.port === '23000' || // BTCPay — X-Frame-Options: DENY
|
||||
u.port === '3000' || // Grafana — X-Frame-Options: deny
|
||||
u.port === '8082' || // Vaultwarden — X-Frame-Options: SAMEORIGIN + CSP frame-ancestors
|
||||
u.port === '2342' || // PhotoPrism — X-Frame-Options: DENY + CSP frame-ancestors: 'none'
|
||||
u.port === '8085' || // Nextcloud — X-Frame-Options: SAMEORIGIN
|
||||
u.port === '3001' || // Uptime Kuma — X-Frame-Options: SAMEORIGIN
|
||||
u.port === '8123' // Home Assistant — X-Frame-Options: SAMEORIGIN
|
||||
) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
return NEW_TAB_PORTS.has(u.port)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/** Port → proxy path for apps (nginx strips X-Frame-Options + avoids mixed content) */
|
||||
const PORT_TO_PROXY: Record<string, string> = {
|
||||
'81': '/app/nginx-proxy-manager/',
|
||||
'3000': '/app/grafana/',
|
||||
'3001': '/app/uptime-kuma/',
|
||||
'8080': '/app/endurain/',
|
||||
'8081': '/app/lnd/',
|
||||
'8082': '/app/vaultwarden/',
|
||||
'8083': '/app/filebrowser/',
|
||||
'8085': '/app/nextcloud/',
|
||||
'8096': '/app/jellyfin/',
|
||||
'8123': '/app/homeassistant/',
|
||||
'8240': '/app/tailscale/',
|
||||
'8334': '/app/bitcoin-ui/',
|
||||
'8888': '/app/searxng/',
|
||||
'9000': '/app/portainer/',
|
||||
'9001': '/app/penpot/',
|
||||
'9980': '/app/onlyoffice/',
|
||||
'11434': '/app/ollama/',
|
||||
'2283': '/app/immich/',
|
||||
'23000': '/app/btcpay/',
|
||||
'2342': '/app/photoprism/',
|
||||
'4080': '/app/mempool/',
|
||||
'8175': '/app/fedimint/',
|
||||
'8176': '/app/fedimint-gateway/',
|
||||
'3100': '/app/dwn/',
|
||||
'18081': '/app/nostr-rs-relay/',
|
||||
'7777': '/app/indeedhub/',
|
||||
/** Port → app ID for resolving URLs to AppSession routes */
|
||||
const PORT_TO_APP_ID: Record<string, string> = {
|
||||
'81': 'nginx-proxy-manager',
|
||||
'3000': 'grafana',
|
||||
'3001': 'uptime-kuma',
|
||||
'8080': 'endurain',
|
||||
'8081': 'lnd',
|
||||
'8082': 'vaultwarden',
|
||||
'8083': 'filebrowser',
|
||||
'8085': 'nextcloud',
|
||||
'8096': 'jellyfin',
|
||||
'8123': 'homeassistant',
|
||||
'8240': 'tailscale',
|
||||
'8334': 'bitcoin-knots',
|
||||
'8888': 'searxng',
|
||||
'9000': 'portainer',
|
||||
'9001': 'penpot',
|
||||
'9980': 'onlyoffice',
|
||||
'11434': 'ollama',
|
||||
'2283': 'immich',
|
||||
'23000': 'btcpay-server',
|
||||
'2342': 'photoprism',
|
||||
'4080': 'mempool',
|
||||
'8175': 'fedimint',
|
||||
'8176': 'fedimint-gateway',
|
||||
'3100': 'dwn',
|
||||
'18081': 'nostr-rs-relay',
|
||||
'7777': 'indeedhub',
|
||||
'50002': 'electrumx',
|
||||
}
|
||||
|
||||
/** Rewrite to same-origin proxy ONLY when needed for HTTPS mixed-content.
|
||||
* On HTTP, direct port URLs are used — they avoid subpath routing issues
|
||||
* (apps' root-relative asset paths like /static/main.js break under /app/xxx/).
|
||||
* On HTTPS, must proxy to avoid mixed-content blocks; nginx also strips X-Frame-Options.
|
||||
*/
|
||||
function toEmbeddableUrl(url: string): string {
|
||||
try {
|
||||
const u = new URL(url)
|
||||
const origin = window.location.origin
|
||||
|
||||
// External sites proxied through nginx path-based locations
|
||||
const extPath = EXTERNAL_PROXY_PATH[u.hostname]
|
||||
if (extPath) {
|
||||
return `${origin}${extPath}`
|
||||
}
|
||||
|
||||
const proxyPath = PORT_TO_PROXY[u.port]
|
||||
const sameHost = u.hostname === window.location.hostname
|
||||
const needsProxy = window.location.protocol === 'https:' && u.protocol === 'http:'
|
||||
if (proxyPath && sameHost && needsProxy) {
|
||||
return `${origin}${proxyPath}`
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
const APPROVED_ORIGINS_KEY = 'neode_nostr_approved_origins'
|
||||
|
||||
@@ -121,6 +83,8 @@ export interface NostrConsentRequest {
|
||||
reject: () => void
|
||||
}
|
||||
|
||||
const DISPLAY_MODE_KEY = 'archipelago_app_display_mode'
|
||||
|
||||
export const useAppLauncherStore = defineStore('appLauncher', () => {
|
||||
const isOpen = ref(false)
|
||||
const url = ref('')
|
||||
@@ -129,9 +93,22 @@ export const useAppLauncherStore = defineStore('appLauncher', () => {
|
||||
const showConsent = ref(false)
|
||||
let previousActiveElement: HTMLElement | null = null
|
||||
|
||||
/** Open app in full-page session view (preferred — no iframe subpath issues) */
|
||||
/** Active app in panel mode (store-based, no route change) */
|
||||
const panelAppId = ref<string | null>(null)
|
||||
|
||||
/** Open app in session view — panel mode uses store, overlay/fullscreen uses route */
|
||||
function openSession(appId: string) {
|
||||
router.push({ name: 'app-session', params: { appId } })
|
||||
const mode = localStorage.getItem(DISPLAY_MODE_KEY) || 'panel'
|
||||
if (mode === 'panel') {
|
||||
panelAppId.value = appId
|
||||
} else {
|
||||
panelAppId.value = null
|
||||
router.push({ name: 'app-session', params: { appId } })
|
||||
}
|
||||
}
|
||||
|
||||
function closePanel() {
|
||||
panelAppId.value = null
|
||||
}
|
||||
|
||||
/** Legacy: open app in iframe overlay (kept for backward compat) */
|
||||
@@ -142,13 +119,13 @@ export const useAppLauncherStore = defineStore('appLauncher', () => {
|
||||
openSession(resolvedId)
|
||||
return
|
||||
}
|
||||
// Apps that block iframes — open directly in new tab
|
||||
if (payload.openInNewTab || mustOpenInNewTab(payload.url)) {
|
||||
window.open(payload.url, '_blank', 'noopener,noreferrer')
|
||||
return
|
||||
}
|
||||
const embeddableUrl = toEmbeddableUrl(payload.url)
|
||||
previousActiveElement = (document.activeElement as HTMLElement) || null
|
||||
url.value = embeddableUrl
|
||||
url.value = payload.url
|
||||
title.value = payload.title
|
||||
isOpen.value = true
|
||||
}
|
||||
@@ -158,11 +135,8 @@ export const useAppLauncherStore = defineStore('appLauncher', () => {
|
||||
try {
|
||||
const u = new URL(urlStr)
|
||||
// Check port-based apps
|
||||
for (const [port, proxyPath] of Object.entries(PORT_TO_PROXY)) {
|
||||
if (u.port === port) {
|
||||
return proxyPath.replace('/app/', '').replace(/\/$/, '')
|
||||
}
|
||||
}
|
||||
const appId = PORT_TO_APP_ID[u.port]
|
||||
if (appId) return appId
|
||||
// Check external URLs
|
||||
const EXTERNAL_APP_HOSTS: Record<string, string> = {
|
||||
'botfights.net': 'botfights',
|
||||
@@ -326,6 +300,8 @@ export const useAppLauncherStore = defineStore('appLauncher', () => {
|
||||
open,
|
||||
openSession,
|
||||
close,
|
||||
closePanel,
|
||||
panelAppId,
|
||||
showConsent,
|
||||
consentRequest,
|
||||
approveConsent,
|
||||
|
||||
Reference in New Issue
Block a user