release(v1.7.3-alpha): sidebar version sync + FIPS reconnect + profile pic render
Sidebar version
detect_build_version() no longer reads /opt/archipelago/build-info.txt
first. That file was written by the ISO installer at flash time and
never rewritten by OTA or sideload, so after any binary swap the
sidebar kept advertising whatever the ISO shipped with. Now just
returns env!("CARGO_PKG_VERSION") unconditionally — always matches the
running binary.
FIPS card
The two-column grid in FipsNetworkCard.vue placed version/npub boxes
side-by-side on mobile but the anchor-status panel forced col-span-2,
creating an unbalanced empty column at every desktop width. Anchor
status moves to its own full-width row below the grid. When the
anchor is not reached, a "Reconnect" button appears next to the
status line; it calls fips.restart (45s timeout), waits 5s for the
daemon to come back, then reloads fips.status. Surfaces whether the
restart actually recovered the anchor in a status flash.
Profile picture render
Uploaded profile pictures are stored with an onion-rooted URL so
external Nostr clients can fetch them. The local browser isn't
Tor-routed though, so the <img src> silently 404'd and the UI fell
back to showing initials. Added a displayableUrl() helper on
Web5Identities.vue that rewrites http://<onion>/blob/<cid>[?...] to
same-origin /blob/<cid> for rendering, while the stored URL keeps
its onion prefix so publishing to Nostr still works for external
viewers. Pass-through for data: URLs and already-relative paths.
Identity row title
The identity list header now renders profile.display_name (when set)
and keeps identity.name as a muted parenthetical. Before, only the
internal name was shown and a user who'd customised their Nostr
display_name saw a mismatch between their own UI and what peers
rendered.
Artefacts:
archipelago 99184b95…22dc1b 40350664
archipelago-frontend-1.7.3-alpha.tar.gz 7b933cf4…74a8bc 76987031
Changelog layman-style per the saved feedback.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2
core/Cargo.lock
generated
2
core/Cargo.lock
generated
@@ -80,7 +80,7 @@ checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
|
||||
|
||||
[[package]]
|
||||
name = "archipelago"
|
||||
version = "1.7.2-alpha"
|
||||
version = "1.7.3-alpha"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"archipelago-container",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "archipelago"
|
||||
version = "1.7.2-alpha"
|
||||
version = "1.7.3-alpha"
|
||||
edition = "2021"
|
||||
description = "Archipelago Bitcoin Node OS - Native backend"
|
||||
authors = ["Archipelago Team"]
|
||||
|
||||
@@ -265,16 +265,13 @@ impl DataModel {
|
||||
/// falling back to Cargo.toml version. This allows sequential CI build
|
||||
/// numbers to be reflected in the UI without recompiling the binary.
|
||||
fn detect_build_version() -> String {
|
||||
if let Ok(content) = std::fs::read_to_string("/opt/archipelago/build-info.txt") {
|
||||
for line in content.lines() {
|
||||
if let Some(v) = line.strip_prefix("version=") {
|
||||
let v = v.trim();
|
||||
if !v.is_empty() {
|
||||
return v.to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Always use the binary's compiled-in version. The ISO installer
|
||||
// writes /opt/archipelago/build-info.txt at install time, but that
|
||||
// file is never rewritten by OTA or sideload, so trusting it made
|
||||
// the sidebar permanently advertise whatever the ISO shipped with
|
||||
// even after the running binary had moved on. CARGO_PKG_VERSION is
|
||||
// baked into the binary at compile time, so it always matches what
|
||||
// is actually running.
|
||||
env!("CARGO_PKG_VERSION").to_string()
|
||||
}
|
||||
|
||||
|
||||
@@ -18,27 +18,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 mb-3 flex-1 min-h-0">
|
||||
<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 v-if="status.service_active" class="p-3 bg-white/5 rounded-lg sm:col-span-2">
|
||||
<div class="flex items-center justify-between gap-3 text-xs">
|
||||
<div class="flex items-center gap-2">
|
||||
<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>
|
||||
</div>
|
||||
<div class="text-white/60">{{ status.authenticated_peer_count ?? 0 }} peer{{ (status.authenticated_peer_count ?? 0) === 1 ? '' : 's' }}</div>
|
||||
</div>
|
||||
<p v-if="!status.anchor_connected" class="mt-1 text-[11px] text-white/40">
|
||||
Without the anchor, DHT routing to unknown npubs can't bootstrap; federation + messaging will fall back to Tor until it reconnects.
|
||||
</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>
|
||||
@@ -60,6 +45,33 @@
|
||||
</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">
|
||||
@@ -97,6 +109,7 @@ const status = ref<FipsStatus>({
|
||||
anchor_connected: false,
|
||||
})
|
||||
const installing = ref(false)
|
||||
const reconnecting = ref(false)
|
||||
const statusMessage = ref('')
|
||||
const statusIsError = ref(false)
|
||||
const copied = ref(false)
|
||||
@@ -167,5 +180,29 @@ async function installAndActivate() {
|
||||
}
|
||||
}
|
||||
|
||||
// Restart the FIPS daemon to kick it back onto the public anchor. Stale
|
||||
// identity-cache entries are the usual cause of "not reached"; systemctl
|
||||
// restart clears them and re-runs the bootstrap handshake.
|
||||
async function reconnectAnchor() {
|
||||
reconnecting.value = true
|
||||
try {
|
||||
await rpcClient.call({ method: 'fips.restart', timeout: 45_000 })
|
||||
// Give the daemon a few seconds to come back and re-populate its
|
||||
// identity cache before we re-query status.
|
||||
await new Promise((resolve) => setTimeout(resolve, 5000))
|
||||
await loadStatus()
|
||||
if (status.value.anchor_connected) {
|
||||
flash('Anchor reconnected')
|
||||
} else {
|
||||
flash('FIPS restarted — anchor still reporting unreachable. Check network / firewall.', 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>
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
>
|
||||
<!-- Avatar -->
|
||||
<button @click="openProfileEditor(identity)" class="relative flex-shrink-0 w-10 h-10 rounded-full overflow-hidden group" title="Edit profile">
|
||||
<img v-if="identity.profile?.picture" :src="identity.profile.picture" class="w-full h-full object-cover" @error="($event.target as HTMLImageElement).style.display = 'none'" />
|
||||
<img v-if="identity.profile?.picture" :src="displayableUrl(identity.profile.picture)" class="w-full h-full object-cover" @error="($event.target as HTMLImageElement).style.display = 'none'" />
|
||||
<div v-if="!identity.profile?.picture" class="w-full h-full flex items-center justify-center" :class="{
|
||||
'bg-blue-500/20': identity.purpose === 'personal',
|
||||
'bg-orange-500/20': identity.purpose === 'business',
|
||||
@@ -88,7 +88,8 @@
|
||||
<!-- Info -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-white font-medium text-sm">{{ identity.name }}</span>
|
||||
<span class="text-white font-medium text-sm">{{ identity.profile?.display_name || identity.name }}</span>
|
||||
<span v-if="identity.profile?.display_name && identity.profile.display_name !== identity.name" class="text-white/40 text-xs truncate max-w-[160px]" :title="`Internal name: ${identity.name}`">({{ identity.name }})</span>
|
||||
<span v-if="identity.is_default" class="text-yellow-400 text-xs" title="Default identity">★</span>
|
||||
<span class="text-xs px-2 py-0.5 rounded-full capitalize" :class="{
|
||||
'bg-blue-500/20 text-blue-300': identity.purpose === 'personal',
|
||||
@@ -301,7 +302,7 @@
|
||||
<div class="glass-card p-6 w-full max-w-2xl mx-4 max-h-[90vh] overflow-y-auto" role="dialog" aria-modal="true" aria-labelledby="profile-editor-title">
|
||||
<div class="flex items-center gap-3 mb-5">
|
||||
<div class="relative w-16 h-16 rounded-full overflow-hidden bg-white/10 shrink-0">
|
||||
<img v-if="profileForm.picture" :src="profileForm.picture" class="w-full h-full object-cover" @error="($event.target as HTMLImageElement).style.display = 'none'" />
|
||||
<img v-if="profileForm.picture" :src="displayableUrl(profileForm.picture)" class="w-full h-full object-cover" @error="($event.target as HTMLImageElement).style.display = 'none'" />
|
||||
<div v-else class="w-full h-full flex items-center justify-center">
|
||||
<span class="text-2xl font-bold text-white/40">{{ profileEditorIdentity.name.charAt(0).toUpperCase() }}</span>
|
||||
</div>
|
||||
@@ -408,6 +409,20 @@ const profilePublishing = ref(false)
|
||||
const avatarUploading = ref(false)
|
||||
const bannerUploading = ref(false)
|
||||
|
||||
// The backend returns onion-based public URLs for uploaded profile
|
||||
// pictures (so they're fetchable by external Nostr clients), but the
|
||||
// local browser session isn't Tor-routed and can't resolve .onion hosts.
|
||||
// Rewrite onion-rooted `/blob/<cid>` URLs (with or without capability
|
||||
// query) to same-origin `/blob/<cid>` so they render in this UI. Data
|
||||
// URLs and plain external URLs pass through untouched.
|
||||
function displayableUrl(url: string | null | undefined): string {
|
||||
if (!url) return ''
|
||||
if (url.startsWith('data:') || url.startsWith('/')) return url
|
||||
const onionMatch = url.match(/^https?:\/\/[a-z2-7]{16,56}\.onion(\/blob\/[0-9a-f]{64})(\?.*)?$/i)
|
||||
if (onionMatch && onionMatch[1]) return onionMatch[1]
|
||||
return url
|
||||
}
|
||||
|
||||
// Upload to the node's blob store and drop the returned public URL into
|
||||
// the profile field. The /api/blob endpoint marks these blobs public, so
|
||||
// the URL served back (`public_url`, onion-rooted when Tor is up) is
|
||||
|
||||
Reference in New Issue
Block a user