archy/neode-ui/src/views/server/FipsNetworkCard.vue

209 lines
8.7 KiB
Vue
Raw Normal View History

<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>
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>
2026-04-20 11:44:59 -04:00
<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>
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>
2026-04-20 11:44:59 -04:00
<!-- 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)
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>
2026-04-20 11:44:59 -04:00
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
}
}
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>
2026-04-20 11:44:59 -04:00
// 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>