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:
co-authored by
Claude Opus 4.8
parent
bd567cd165
commit
87769cbfbf
@@ -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 & 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
|
||||
|
||||
Reference in New Issue
Block a user