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
+117 -9
View File
@@ -1,5 +1,6 @@
<template>
<div class="pb-6">
<BackButton label="Back to Settings" desktop-margin="mb-6" @click="$router.push('/dashboard/settings')" />
<div class="mb-6">
<h1 class="text-3xl font-bold text-white mb-2">{{ t('systemUpdate.title') }}</h1>
<p class="text-white/70">{{ t('systemUpdate.subtitle') }}</p>
@@ -20,7 +21,7 @@
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div class="p-4 bg-white/5 rounded-lg">
<p class="text-xs text-white/60 mb-1">{{ t('common.version') }}</p>
<p class="text-xl font-bold text-white">v{{ currentVersion }}</p>
<p class="text-xl font-bold text-white">{{ $ver(currentVersion) }}</p>
</div>
<div class="p-4 bg-white/5 rounded-lg">
<p class="text-xs text-white/60 mb-1">{{ t('systemUpdate.lastChecked') }}</p>
@@ -186,6 +187,62 @@
</div>
</div>
<!-- Update source (origin vs DHT swarm) -->
<div class="glass-card p-6 mb-6">
<h2 class="text-lg font-semibold text-white mb-1">Update &amp; app source</h2>
<p class="text-sm text-white/60 mb-4">
Where this node fetches updates and apps. <span class="text-white/80">Stable origin</span> is the
known-good default. <span class="text-white/80">DHT swarm</span> pulls content-addressed blobs from
peers first and falls back to the origin automatically use it to live-test the swarm; flip back any time.
</p>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
<button
type="button"
:disabled="sourceSaving"
class="text-left p-4 rounded-lg border transition-colors disabled:opacity-60"
:class="updateSource === 'origin' ? 'border-green-400/40 bg-green-500/10' : 'border-white/10 bg-white/5 hover:bg-white/10'"
@click="setSource('origin')"
>
<div class="flex items-center gap-2 mb-1">
<div class="w-2.5 h-2.5 rounded-full" :class="updateSource === 'origin' ? 'bg-green-400' : 'bg-white/30'"></div>
<span class="text-sm font-medium text-white">Stable origin</span>
<span class="text-[10px] font-mono px-1.5 py-0.5 rounded bg-white/10 text-white/60">DEFAULT</span>
</div>
<p class="text-xs text-white/50">HTTP from the mirrors below. Safe, predictable, always available.</p>
</button>
<button
type="button"
:disabled="sourceSaving"
class="text-left p-4 rounded-lg border transition-colors disabled:opacity-60"
:class="updateSource === 'swarm' ? 'border-blue-400/40 bg-blue-500/10' : 'border-white/10 bg-white/5 hover:bg-white/10'"
@click="setSource('swarm')"
>
<div class="flex items-center gap-2 mb-1">
<div class="w-2.5 h-2.5 rounded-full" :class="updateSource === 'swarm' ? 'bg-blue-400' : 'bg-white/30'"></div>
<span class="text-sm font-medium text-white">DHT swarm</span>
<span class="text-[10px] font-mono px-1.5 py-0.5 rounded bg-blue-500/20 text-blue-300">EXPERIMENTAL</span>
</div>
<p class="text-xs text-white/50">Peer-to-peer first, origin fallback. For testing the distributed network.</p>
</button>
</div>
<p v-if="updateSource === 'swarm' && !swarmAvailable" class="mt-3 text-xs text-orange-300/90 flex items-start gap-1.5">
<svg class="w-3.5 h-3.5 shrink-0 mt-px" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
<span>This build doesn't include the swarm engine, so DHT mode currently behaves exactly like origin. A swarm-enabled build is required for true peer fetching.</span>
</p>
<!-- Provide to the swarm (seed/serve to peers) -->
<div class="mt-5 pt-5 border-t border-white/10 flex items-start justify-between gap-4">
<div class="min-w-0">
<p class="text-sm font-medium text-white mb-0.5">Provide to the swarm</p>
<p class="text-xs text-white/50">
Let other nodes fetch updates and apps from this one (seeding). On by default so the network has providers.
Turn off if this node shouldn't serve content to peers it won't change how this node fetches.
</p>
</div>
<ToggleSwitch :model-value="provideDht" :disabled="sourceSaving" @update:model-value="setProvide" />
</div>
</div>
<!-- Mirrors -->
<div class="glass-card p-6 mb-6">
<div class="flex items-start justify-between gap-4 mb-2">
@@ -285,12 +342,6 @@
>
{{ t('systemUpdate.rollback') }}
</button>
<RouterLink
to="/dashboard/settings"
class="glass-button w-full rounded-lg px-5 py-2 text-sm font-medium text-center sm:w-auto"
>
{{ t('systemUpdate.backToSettings') }}
</RouterLink>
</div>
</div>
@@ -451,9 +502,10 @@
<script setup lang="ts">
import { ref, computed, onMounted, reactive } from 'vue'
import { useI18n } from 'vue-i18n'
import { RouterLink } from 'vue-router'
import { rpcClient } from '@/api/rpc-client'
import BitcoinFaceAscii from '@/views/discover/BitcoinFaceAscii.vue'
import BackButton from '@/components/BackButton.vue'
import ToggleSwitch from '@/components/ToggleSwitch.vue'
interface UpdateDetail {
version: string
@@ -491,6 +543,16 @@ const statusIsError = ref(false)
const downloadPercent = ref(0)
const downloadPercentFormatted = computed(() => downloadPercent.value.toFixed(2))
// Update/app fetch source: "origin" (HTTP mirrors, default/safe) vs "swarm"
// (DHT peer-assist with origin fallback). Lets us live-test the DHT on a node
// and flip back instantly. swarmAvailable reflects whether this binary was
// built with the iroh swarm engine at all.
const updateSource = ref<'origin' | 'swarm'>('origin')
const provideDht = ref(true)
const swarmAvailable = ref(false)
const swarmEnabled = ref(false)
const sourceSaving = ref(false)
// Mirrors — servers this node tries for the manifest, in priority
// order. First entry is the primary. Add/remove/set-primary are wired
// to update.*-mirror RPCs; downloads automatically go to the mirror
@@ -564,6 +626,52 @@ async function loadMirrors() {
}
}
interface SourceState { source: 'origin' | 'swarm'; provide_dht: boolean; swarm_available: boolean; swarm_enabled: boolean }
async function loadSource() {
try {
const res = await rpcClient.call<SourceState>({ method: 'update.get-source' })
updateSource.value = res.source
swarmAvailable.value = res.swarm_available
swarmEnabled.value = res.swarm_enabled
if (typeof res.provide_dht === 'boolean') provideDht.value = res.provide_dht
} catch (e) {
if (import.meta.env.DEV) console.warn('update.get-source failed', e)
}
}
async function setSource(source: 'origin' | 'swarm') {
if (sourceSaving.value || updateSource.value === source) return
sourceSaving.value = true
try {
const res = await rpcClient.call<SourceState>({ method: 'update.set-source', params: { source } })
updateSource.value = res.source
showStatus(source === 'swarm'
? 'Update source set to DHT swarm origin still used as fallback.'
: 'Update source set to stable origin.')
} catch (e) {
showStatus(`Failed to change source: ${e instanceof Error ? e.message : e}`, true)
} finally {
sourceSaving.value = false
}
}
async function setProvide(provide: boolean) {
if (sourceSaving.value || provideDht.value === provide) return
sourceSaving.value = true
try {
const res = await rpcClient.call<SourceState>({ method: 'update.set-source', params: { provide } })
if (typeof res.provide_dht === 'boolean') provideDht.value = res.provide_dht
showStatus(provide
? 'This node will now seed updates/apps to peers.'
: 'This node will no longer provide to the swarm.')
} catch (e) {
showStatus(`Failed to change setting: ${e instanceof Error ? e.message : e}`, true)
} finally {
sourceSaving.value = false
}
}
async function submitMirror() {
const url = mirrorDraft.url.trim()
if (!url) return
@@ -1013,7 +1121,7 @@ async function setSchedule(value: ScheduleValue) {
}
onMounted(async () => {
await Promise.all([loadStatus(), loadSchedule(), loadMirrors(), checkForUpdates()])
await Promise.all([loadStatus(), loadSchedule(), loadMirrors(), loadSource(), checkForUpdates()])
// If a download was already running when the user navigated here
// (or refreshed), pick up the progress bar where it is and keep
// polling until the backend reports done. No RPC call to start the