All checks were successful
Build Archipelago ISO (dev) / build-iso (push) Successful in 10m10s
Install UX SystemUpdate.vue now shows a full-screen overlay after apply: the BitcoinFaceAscii logo, a target-version label, an indeterminate progress stripe (solid orange; solid green on ready), and an elapsed-time readout. Polls /health every 1.5s and auto-reloads once the backend reports the new version. 3-min stall → "Reload now" button. Download UI also shows a spinner + "Finishing download — verifying checksum…" while the fake bar sits at 95%. FIPS reconnect — for real this time New fips.reconnect RPC does stop → start → wait 20s → re-poll → classify. Classification buckets: connected / daemon_down / no_seed_key / no_outbound_udp_or_anchor_down / peers_but_no_anchor, each with a plain-language hint surfaced verbatim by the Reconnect button. The real reason nodes like .198/.253 couldn't reach the anchor: identity::write_fips_key_from_seed was writing fips_key.pub as a bech32 npub TEXT file, but upstream fips expects 32 raw bytes. The daemon silently authenticated with garbage. Fix: PublicKey::to_bytes() → raw 32 bytes, and new fips::config::normalize_pub_file migrates legacy files by decoding the npub and rewriting in place. fips.reconnect also re-installs the config + healed keys to /etc/fips before restarting. AIUI preservation + restore apply_update was wiping /opt/archipelago/web-ui/aiui because the Vue build doesn't include it — every OTA lost the Claude sidebar. The preserve block now copies aiui/ + archipelago-companion.apk from the old web-ui into the staging dir before the swap, and prefers new-tar versions if present. To restore it on the three nodes that already lost it (.116/.198/.253), this release bundles the 85 MB aiui build into the frontend tarball. Frontend component size is now ~155 MB. Download / install timeouts Backend download client timeout 1800s → 3600s (1 h). Larger tarball + slow gitea raw throughput put us above the old cap. Frontend update.download rpc timeout 30 min → 65 min to match. package.install rpc timeout 15 min → 45 min — IndeedHub pulls 6 images and was timing out mid-install. UI nit "Rollback to Previous" → "Rollback Available". App-catalog proxy already landed in v1.7.13. Artefacts: archipelago 725e18e6…3c525e6 40462288 archipelago-frontend-1.7.14-alpha.tar.gz c35284be…ff2c16 162077052 (+aiui) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
219 lines
9.1 KiB
Vue
219 lines
9.1 KiB
Vue
<template>
|
|
<div data-controller-container tabindex="0" class="glass-card p-6 flex flex-col transition-all hover:-translate-y-1">
|
|
<div class="flex items-start gap-4 mb-4 shrink-0">
|
|
<div class="flex-shrink-0 w-12 h-12 rounded-lg bg-white/10 flex items-center justify-center">
|
|
<svg class="w-6 h-6 text-white/80" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
|
</svg>
|
|
</div>
|
|
<div class="flex-1">
|
|
<div class="flex items-start justify-between gap-4 mb-2">
|
|
<h2 class="text-xl font-semibold text-white">FIPS Mesh</h2>
|
|
<div class="flex items-center gap-2" :title="statusLabel">
|
|
<span class="w-2 h-2 rounded-full" :class="statusDotColor"></span>
|
|
<span class="text-sm font-medium" :class="statusTextColor">{{ statusLabel }}</span>
|
|
</div>
|
|
</div>
|
|
<p class="text-white/70 text-sm mb-4">Fast Nostr-keyed mesh routing</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 mb-3 shrink-0">
|
|
<div class="p-3 bg-white/5 rounded-lg">
|
|
<p class="text-xs text-white/60 mb-1">Daemon version</p>
|
|
<p class="text-sm font-medium text-white break-all">{{ status.version || '—' }}</p>
|
|
<p v-if="!status.installed" class="text-xs text-white/40 mt-1">Package not installed</p>
|
|
</div>
|
|
<div class="p-3 bg-white/5 rounded-lg">
|
|
<div class="flex items-center justify-between gap-2 mb-1">
|
|
<p class="text-xs text-white/60">FIPS npub</p>
|
|
<button
|
|
v-if="status.npub"
|
|
type="button"
|
|
class="text-xs text-white/60 hover:text-white transition-colors flex items-center gap-1"
|
|
:title="copied ? 'Copied!' : 'Copy full npub to clipboard'"
|
|
@click="copyNpub"
|
|
>
|
|
<svg v-if="!copied" class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" /></svg>
|
|
<svg v-else class="w-3.5 h-3.5 text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" /></svg>
|
|
<span :class="{ 'text-green-400': copied }">{{ copied ? 'Copied' : 'Copy' }}</span>
|
|
</button>
|
|
</div>
|
|
<p class="text-sm font-mono text-white break-all select-all">{{ npubDisplay }}</p>
|
|
<p v-if="!status.key_present && status.npub" class="text-xs text-white/40 mt-1">Upstream key (not seed-derived)</p>
|
|
<p v-else-if="!status.key_present" class="text-xs text-white/40 mt-1">Unlock seed to derive archipelago-managed key</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Anchor status: always full-width to keep desktop layout tidy -->
|
|
<div v-if="status.service_active" class="p-3 bg-white/5 rounded-lg mb-3 shrink-0">
|
|
<div class="flex items-center justify-between gap-3 flex-wrap">
|
|
<div class="flex items-center gap-2 text-xs">
|
|
<span class="w-2 h-2 rounded-full" :class="status.anchor_connected ? 'bg-cyan-400' : 'bg-orange-400'"></span>
|
|
<span class="text-white/70">Anchor (fips.v0l.io):</span>
|
|
<span :class="status.anchor_connected ? 'text-cyan-300' : 'text-orange-300'">
|
|
{{ status.anchor_connected ? 'connected' : 'not reached' }}
|
|
</span>
|
|
<span class="text-white/40">·</span>
|
|
<span class="text-white/60">{{ status.authenticated_peer_count ?? 0 }} peer{{ (status.authenticated_peer_count ?? 0) === 1 ? '' : 's' }}</span>
|
|
</div>
|
|
<button
|
|
v-if="!status.anchor_connected"
|
|
type="button"
|
|
class="text-xs px-3 py-1.5 rounded-md bg-orange-400/15 hover:bg-orange-400/25 text-orange-200 disabled:opacity-60 transition-colors"
|
|
:disabled="reconnecting"
|
|
@click="reconnectAnchor"
|
|
>
|
|
{{ reconnecting ? 'Reconnecting…' : 'Reconnect' }}
|
|
</button>
|
|
</div>
|
|
<p v-if="!status.anchor_connected" class="mt-2 text-[11px] text-white/40 leading-snug">
|
|
Without the anchor, DHT routing to unknown npubs can't bootstrap; federation and messaging fall back to Tor until it reconnects. Reconnect restarts the FIPS daemon, which usually clears a stale identity cache.
|
|
</p>
|
|
</div>
|
|
|
|
<div v-if="statusMessage" class="mb-3 p-3 rounded-lg text-xs" :class="statusIsError ? 'bg-red-400/10 text-red-300' : 'bg-green-400/10 text-green-300'">{{ statusMessage }}</div>
|
|
|
|
<div v-if="status.key_present && !status.service_active" class="flex gap-2 mt-auto pt-3 shrink-0">
|
|
<button class="flex-1 min-h-[44px] px-4 py-2 glass-button rounded-lg text-sm font-medium transition-colors" :disabled="installing" @click="installAndActivate">{{ installing ? 'Installing…' : 'Activate' }}</button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed, onMounted, ref } from 'vue'
|
|
import { rpcClient } from '@/api/rpc-client'
|
|
import { safeClipboardWrite } from '@/views/web5/utils'
|
|
|
|
interface FipsStatus {
|
|
installed: boolean
|
|
version: string | null
|
|
service_state: string
|
|
upstream_service_state: string
|
|
service_active: boolean
|
|
key_present: boolean
|
|
npub: string | null
|
|
authenticated_peer_count?: number
|
|
anchor_connected?: boolean
|
|
}
|
|
|
|
const status = ref<FipsStatus>({
|
|
installed: false,
|
|
version: null,
|
|
service_state: 'unknown',
|
|
upstream_service_state: 'unknown',
|
|
service_active: false,
|
|
key_present: false,
|
|
npub: null,
|
|
authenticated_peer_count: 0,
|
|
anchor_connected: false,
|
|
})
|
|
const installing = ref(false)
|
|
const reconnecting = ref(false)
|
|
const statusMessage = ref('')
|
|
const statusIsError = ref(false)
|
|
const copied = ref(false)
|
|
|
|
async function copyNpub() {
|
|
if (!status.value.npub) return
|
|
try {
|
|
await safeClipboardWrite(status.value.npub)
|
|
copied.value = true
|
|
setTimeout(() => { copied.value = false }, 2000)
|
|
} catch (e: unknown) {
|
|
const msg = e instanceof Error ? e.message : String(e)
|
|
flash(`Copy failed: ${msg}`, true)
|
|
}
|
|
}
|
|
|
|
const statusLabel = computed(() => {
|
|
if (!status.value.installed) return 'not installed'
|
|
// Active takes precedence: the daemon may be running from its own upstream
|
|
// key on a legacy/dev node that doesn't have a seed-derived archipelago key.
|
|
if (status.value.service_active) return 'active'
|
|
if (!status.value.key_present) return 'awaiting seed'
|
|
return status.value.service_state
|
|
})
|
|
|
|
const statusDotColor = computed(() => {
|
|
if (status.value.service_active) return 'bg-green-400'
|
|
if (!status.value.installed || !status.value.key_present) return 'bg-white/30'
|
|
return 'bg-orange-400'
|
|
})
|
|
|
|
const statusTextColor = computed(() => {
|
|
if (status.value.service_active) return 'text-green-400'
|
|
if (!status.value.installed || !status.value.key_present) return 'text-white/50'
|
|
return 'text-orange-400'
|
|
})
|
|
|
|
const npubDisplay = computed(() => {
|
|
const n = status.value.npub
|
|
if (!n) return '—'
|
|
return n.length > 20 ? `${n.slice(0, 12)}…${n.slice(-6)}` : n
|
|
})
|
|
|
|
function flash(msg: string, isError = false) {
|
|
statusMessage.value = msg
|
|
statusIsError.value = isError
|
|
setTimeout(() => { statusMessage.value = '' }, 6000)
|
|
}
|
|
|
|
async function loadStatus() {
|
|
try {
|
|
status.value = await rpcClient.call<FipsStatus>({ method: 'fips.status' })
|
|
} catch (e) {
|
|
if (import.meta.env.DEV) console.warn('fips.status failed', e)
|
|
}
|
|
}
|
|
|
|
async function installAndActivate() {
|
|
installing.value = true
|
|
try {
|
|
status.value = await rpcClient.call<FipsStatus>({ method: 'fips.install' })
|
|
flash('FIPS installed and activated')
|
|
} catch (e: unknown) {
|
|
const msg = e instanceof Error ? e.message : String(e)
|
|
flash(`Install failed: ${msg}`, true)
|
|
} finally {
|
|
installing.value = false
|
|
}
|
|
}
|
|
|
|
// Restart the FIPS daemon and wait for the anchor bootstrap window.
|
|
// The backend runs a proper recovery sequence (stop → start → wait →
|
|
// classify) and returns a structured diagnostic we can show the user
|
|
// instead of a generic "still unreachable".
|
|
async function reconnectAnchor() {
|
|
reconnecting.value = true
|
|
try {
|
|
const res = await rpcClient.call<{
|
|
recovered: boolean
|
|
likely_cause: string
|
|
hint: string
|
|
after: FipsStatus
|
|
}>({ method: 'fips.reconnect', timeout: 60_000 })
|
|
// Update the card with the post-reconnect status returned by the
|
|
// backend — avoids an extra status fetch race.
|
|
status.value = { ...status.value, ...res.after }
|
|
if (res.recovered) {
|
|
flash('Anchor reconnected.')
|
|
} else if (res.likely_cause === 'connected') {
|
|
// Already connected, not a "recovery" per se.
|
|
flash('Anchor is reachable.')
|
|
} else {
|
|
// Surface the backend's diagnostic hint verbatim — it's been
|
|
// written for the fleet reader.
|
|
flash(res.hint || 'Reconnect finished but anchor is still unreachable.', true)
|
|
}
|
|
} catch (e: unknown) {
|
|
const msg = e instanceof Error ? e.message : String(e)
|
|
flash(`Reconnect failed: ${msg}`, true)
|
|
} finally {
|
|
reconnecting.value = false
|
|
}
|
|
}
|
|
|
|
onMounted(loadStatus)
|
|
</script>
|