feat(ui): dual-ecash wallet settings, buy-peer-files, seed backup, assorted fixes

- Tabbed Wallet Settings modal (Cashu + Fedimint) and dual-balance wallet card
- Buy a peer's paid file (ecash / node Lightning / on-chain / external QR)
- Recovery-phrase reveal + backup section; onboarding seed retry resilience
- NetBird HTTPS launch, remote-control two-finger scroll + external-open
- Shared BackButton, single-v version label, mesh Bitcoin header toggles

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
archipelago
2026-06-17 19:21:42 -04:00
co-authored by Claude Opus 4.8
parent bd567cd165
commit 87769cbfbf
46 changed files with 1527 additions and 283 deletions
+51 -6
View File
@@ -3,6 +3,26 @@ import { ref, watch } from 'vue'
import { rpcClient } from '@/api/rpc-client'
import router from '@/router'
import { recordAppLaunch } from '@/utils/appUsage'
import { requestExternalOpen } from '@/api/remote-relay'
/**
* Open a URL in a new browser tab — but if a companion (phone) is currently
* driving this kiosk, hand the URL to the phone instead so it opens in the
* phone's browser rather than the (often headless / unattended) kiosk display.
* Falls back to a local `window.open` when no companion is active.
*/
function openExternal(launchUrl: string) {
// Resolve to an absolute URL so the phone can open it (window.open also
// handles absolute URLs fine).
let absolute = launchUrl
try {
absolute = new URL(launchUrl, window.location.origin).href
} catch {
/* keep as-is */
}
if (requestExternalOpen(absolute)) return
window.open(launchUrl, '_blank', 'noopener,noreferrer')
}
/** Ports of apps that set X-Frame-Options (can't iframe, must open in new tab) */
const NEW_TAB_PORTS = new Set([
@@ -29,8 +49,16 @@ const NEW_TAB_APP_IDS = new Set([
'nginx-proxy-manager',
'uptime-kuma',
'gitea',
// netbird's dashboard needs a secure context (window.crypto.subtle for OIDC
// PKCE), so it's served over HTTPS and must open in a real tab — a
// self-signed-HTTPS iframe is blocked by the browser (you can't accept the
// cert warning inside an iframe).
'netbird',
])
// Apps served over HTTPS (self-signed) rather than plain HTTP.
const HTTPS_APP_IDS = new Set(['netbird'])
function mustOpenInNewTab(url: string): boolean {
try {
const u = new URL(url)
@@ -127,12 +155,16 @@ const APP_ID_TO_PORT: Record<string, string> = {
'nginx-proxy-manager': '8081',
'uptime-kuma': '3002',
gitea: '3001',
// Without this, directAppUrl('netbird') returns null and netbird falls
// through to the iframe (and never gets its https URL) — issue #15.
netbird: '8087',
}
function directAppUrl(appId: string): string | null {
const port = APP_ID_TO_PORT[appId]
if (!port || typeof window === 'undefined') return null
return `http://${window.location.hostname}:${port}`
const scheme = HTTPS_APP_IDS.has(appId) ? 'https' : 'http'
return `${scheme}://${window.location.hostname}:${port}`
}
@@ -192,7 +224,7 @@ export const useAppLauncherStore = defineStore('appLauncher', () => {
const mobile = isMobileViewport()
const launchUrl = NEW_TAB_APP_IDS.has(appId) ? directAppUrl(appId) : null
if (launchUrl && !mobile) {
window.open(launchUrl, '_blank', 'noopener,noreferrer')
openExternal(launchUrl)
return
}
@@ -212,12 +244,25 @@ export const useAppLauncherStore = defineStore('appLauncher', () => {
/** Legacy: open app in iframe overlay (kept for backward compat) */
function open(payload: { url: string; title: string; openInNewTab?: boolean }) {
const titleHintId = inferAppIdFromTitle(payload.title)
const launchUrl = normalizeLaunchUrl(payload.url, titleHintId)
let launchUrl = normalizeLaunchUrl(payload.url, titleHintId)
const resolvedId = resolveAppIdFromUrl(launchUrl) || titleHintId
// Apps served over HTTPS (e.g. netbird, which needs a secure context for
// its OIDC dashboard) must be launched over https — a stale http URL hits
// the TLS port and 400s. Upgrade the scheme defensively in every path.
if (resolvedId && HTTPS_APP_IDS.has(resolvedId)) {
try {
const u = new URL(launchUrl, window.location.origin)
if (u.protocol === 'http:') {
u.protocol = 'https:'
launchUrl = u.href
}
} catch { /* leave as-is */ }
}
if (!isMobileViewport() && payload.openInNewTab) {
if (resolvedId) recordAppLaunch(resolvedId)
window.open(launchUrl, '_blank', 'noopener,noreferrer')
openExternal(launchUrl)
return
}
@@ -226,7 +271,7 @@ export const useAppLauncherStore = defineStore('appLauncher', () => {
// native launchers and keep the user inside Archipelago.
if (!isMobileViewport() && resolvedId && NEW_TAB_APP_IDS.has(resolvedId)) {
recordAppLaunch(resolvedId)
window.open(launchUrl, '_blank', 'noopener,noreferrer')
openExternal(launchUrl)
return
}
@@ -238,7 +283,7 @@ export const useAppLauncherStore = defineStore('appLauncher', () => {
// Unknown apps that block iframes — open directly in new tab
if (!isMobileViewport() && mustOpenInNewTab(launchUrl)) {
window.open(launchUrl, '_blank', 'noopener,noreferrer')
openExternal(launchUrl)
return
}
+3
View File
@@ -16,6 +16,9 @@ export interface MeshStatus {
messages_sent: number
messages_received: number
detected_devices?: string[]
/** Bitcoin block-header send/receive prefs (issue #28). */
announce_block_headers?: boolean
receive_block_headers?: boolean
}
export interface MeshPeer {