chore: release v1.7.86-alpha

This commit is contained in:
archipelago
2026-06-12 04:21:18 -04:00
parent e474a2b4c9
commit b11c6c17d1
21 changed files with 330 additions and 104 deletions

View File

@@ -1,5 +1,14 @@
# Changelog
## v1.7.86-alpha (2026-06-12)
- Fleet now preserves the last known node list, alerts, and selection locally while telemetry refreshes in the background, so the dashboard no longer blanks on tab switches or update scans.
- Connected nodes and identities now reuse their last loaded data instead of reloading the visible list every time the user revisits the tab.
- The Fleet matrix and detail views now show actual node names and host information instead of raw node id prefixes.
- The network map only redraws when its graph data actually changes, which stops the D3 scene from visually resetting on every refresh tick.
- Mobile federation and system-update actions now stack full width, and the ElectrumX app health check allows a long startup window so slow sync nodes do not restart mid-index.
- Validation passed with `git diff --check`, focused frontend tests, and `npm run type-check`.
## v1.7.85-alpha (2026-06-12)
- ElectrumX now runs with less cache pressure and more memory headroom, reducing the restart loop seen during sync catch-up.

View File

@@ -57,6 +57,7 @@ app:
interval: 30s
timeout: 5s
retries: 3
start_period: 10m
bitcoin_integration:
rpc_access: read-only

2
core/Cargo.lock generated
View File

@@ -80,7 +80,7 @@ checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
[[package]]
name = "archipelago"
version = "1.7.85-alpha"
version = "1.7.86-alpha"
dependencies = [
"anyhow",
"archipelago-container",

View File

@@ -1,6 +1,6 @@
[package]
name = "archipelago"
version = "1.7.85-alpha"
version = "1.7.86-alpha"
edition = "2021"
description = "Archipelago Bitcoin Node OS - Native backend"
authors = ["Archipelago Team"]

View File

@@ -1,12 +1,12 @@
{
"name": "neode-ui",
"version": "1.7.85-alpha",
"version": "1.7.86-alpha",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "neode-ui",
"version": "1.7.85-alpha",
"version": "1.7.86-alpha",
"dependencies": {
"@types/dompurify": "^3.0.5",
"@vue-leaflet/vue-leaflet": "^0.10.1",

View File

@@ -1,7 +1,7 @@
{
"name": "neode-ui",
"private": true,
"version": "1.7.85-alpha",
"version": "1.7.86-alpha",
"type": "module",
"scripts": {
"start": "./start-dev.sh",

View File

@@ -5,7 +5,7 @@
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch } from 'vue'
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
import * as d3 from 'd3'
interface MapNode {
@@ -36,6 +36,11 @@ type SimLink = d3.SimulationLinkDatum<SimNode> & { source: string | SimNode; tar
let simulation: d3.Simulation<SimNode, SimLink> | null = null
let resizeObserver: ResizeObserver | null = null
const graphSignature = computed(() => JSON.stringify({
nodes: props.nodes.map(n => [n.did, n.label, n.trust_level, n.online, n.app_count, n.is_self]),
links: props.links.map(l => [l.source, l.target]),
}))
function trustColor(level: string): string {
switch (level) {
case 'trusted': return '#4ade80'
@@ -50,6 +55,7 @@ function nodeRadius(n: MapNode): number {
}
function render() {
simulation?.stop()
const svg = d3.select(svgRef.value!)
svg.selectAll('*').remove()
@@ -160,7 +166,7 @@ onUnmounted(() => {
resizeObserver?.disconnect()
})
watch(() => [props.nodes, props.links], () => render(), { deep: true })
watch(graphSignature, () => render())
</script>
<style scoped>

View File

@@ -556,8 +556,8 @@ input[type="radio"]:active + * {
context) stay above the tab bar instead of sliding underneath it. */
@media (max-width: 767px) {
.chat-iframe-mobile {
height: calc(100vh - var(--mobile-tab-bar-height, 72px) - var(--safe-area-top, env(safe-area-inset-top, 0px)) - 16px) !important;
height: calc(100dvh - var(--mobile-tab-bar-height, 72px) - var(--safe-area-top, env(safe-area-inset-top, 0px)) - 16px) !important;
height: calc(100vh - var(--mobile-tab-bar-height, 72px) - var(--safe-area-top, env(safe-area-inset-top, 0px))) !important;
height: calc(100dvh - var(--mobile-tab-bar-height, 72px) - var(--safe-area-top, env(safe-area-inset-top, 0px))) !important;
flex: none;
}
}

View File

@@ -19,11 +19,11 @@
<!-- Registry list -->
<div class="glass-card p-6 mb-6">
<div class="flex items-start justify-between gap-4 mb-2">
<div class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between mb-2">
<h2 class="text-lg font-semibold text-white">Registries</h2>
<button
type="button"
class="text-xs px-3 py-1.5 rounded-md bg-white/5 hover:bg-white/10 text-white/70 hover:text-white transition-colors"
class="text-xs px-3 py-1.5 rounded-md bg-white/5 hover:bg-white/10 text-white/70 hover:text-white transition-colors w-full sm:w-auto"
@click="openAddRegistry"
>+ Add registry</button>
</div>
@@ -118,7 +118,7 @@
<!-- Back link -->
<RouterLink
to="/dashboard/settings"
class="glass-button rounded-lg px-5 py-2 text-sm font-medium inline-flex items-center gap-2"
class="glass-button w-full rounded-lg px-5 py-2 text-sm font-medium inline-flex items-center justify-center gap-2 sm:w-auto"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />

View File

@@ -69,12 +69,12 @@
</div>
<!-- Actions -->
<div class="flex gap-3">
<div class="flex flex-col gap-3 sm:flex-row">
<!-- Git path: one-shot pull+rebuild+restart -->
<button
v-if="updateMethod === 'git' && !applying"
@click="requestGitApply"
class="glass-button rounded-lg px-6 py-2 text-sm font-medium bg-orange-500/20 border-orange-400/30"
class="glass-button w-full rounded-lg px-6 py-2 text-sm font-medium bg-orange-500/20 border-orange-400/30 sm:w-auto"
>
{{ t('systemUpdate.pullAndRebuild') }}
</button>
@@ -82,14 +82,14 @@
<button
v-if="updateMethod !== 'git' && !downloading && !applying && !downloaded"
@click="downloadUpdate"
class="glass-button rounded-lg px-6 py-2 text-sm font-medium"
class="glass-button w-full rounded-lg px-6 py-2 text-sm font-medium sm:w-auto"
>
{{ t('systemUpdate.downloadUpdate') }}
</button>
<button
v-if="updateMethod !== 'git' && downloaded && !applying"
@click="requestApply"
class="glass-button rounded-lg px-6 py-2 text-sm font-medium bg-orange-500/20 border-orange-400/30"
class="glass-button w-full rounded-lg px-6 py-2 text-sm font-medium bg-orange-500/20 border-orange-400/30 sm:w-auto"
>
{{ t('systemUpdate.applyUpdate') }}
</button>
@@ -145,8 +145,16 @@
<!-- Applying -->
<div v-if="applying" class="glass-card p-6 mb-6">
<h2 class="text-lg font-semibold text-white mb-4">{{ t('systemUpdate.applying') }}</h2>
<div class="flex items-center gap-3">
<div class="w-5 h-5 border-2 border-orange-400 border-t-transparent rounded-full animate-spin"></div>
<div class="flex flex-col items-start gap-3 sm:flex-row sm:items-center">
<svg
class="w-5 h-5 shrink-0 animate-spin text-orange-400"
viewBox="0 0 24 24"
fill="none"
aria-hidden="true"
>
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="3" stroke-opacity="0.2"></circle>
<path fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
</svg>
<p class="text-sm text-white/70">{{ t('systemUpdate.applyWarning') }}</p>
</div>
</div>
@@ -262,22 +270,25 @@
<!-- Actions row -->
<div class="glass-card p-6">
<h2 class="text-lg font-semibold text-white mb-4">{{ t('systemUpdate.actions') }}</h2>
<div class="flex flex-wrap gap-3">
<div class="flex flex-col gap-3 sm:flex-row sm:flex-wrap">
<button
@click="checkForUpdates"
:disabled="loading"
class="glass-button rounded-lg px-5 py-2 text-sm font-medium disabled:opacity-40"
class="glass-button w-full rounded-lg px-5 py-2 text-sm font-medium disabled:opacity-40 sm:w-auto"
>
{{ loading ? t('systemUpdate.checking') : t('systemUpdate.checkForUpdates') }}
</button>
<button
v-if="rollbackAvailable"
@click="requestRollback"
class="glass-button rounded-lg px-5 py-2 text-sm font-medium bg-red-500/10 border-red-400/20"
class="glass-button w-full rounded-lg px-5 py-2 text-sm font-medium bg-red-500/10 border-red-400/20 sm:w-auto"
>
{{ t('systemUpdate.rollback') }}
</button>
<RouterLink to="/dashboard/settings" class="glass-button rounded-lg px-5 py-2 text-sm font-medium text-center">
<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>
@@ -650,12 +661,28 @@ const installStartedAt = ref<number>(0)
const installElapsedSec = ref(0)
let installPollTimer: ReturnType<typeof setInterval> | null = null
let installElapsedTimer: ReturnType<typeof setInterval> | null = null
let installReadyTimer: ReturnType<typeof setTimeout> | null = null
const installElapsedLabel = computed(() => {
const s = installElapsedSec.value
if (s < 60) return `Elapsed: ${s}s`
return `Elapsed: ${Math.floor(s / 60)}m${s % 60 < 10 ? '0' : ''}${s % 60}s`
})
function clearInstallTimers() {
if (installPollTimer) {
clearInterval(installPollTimer)
installPollTimer = null
}
if (installElapsedTimer) {
clearInterval(installElapsedTimer)
installElapsedTimer = null
}
if (installReadyTimer) {
clearTimeout(installReadyTimer)
installReadyTimer = null
}
}
function startInstallOverlay(targetVersion: string) {
clearInstallTimers()
installing.value = true
installStage.value = 'applying'
installTargetVersion.value = targetVersion
@@ -672,7 +699,7 @@ function startInstallOverlay(targetVersion: string) {
// Start polling /health after a short delay — the backend restarts 2s
// after replying to update.apply, so an immediate poll would see the
// old backend and conclude nothing happened.
setTimeout(() => {
installReadyTimer = setTimeout(() => {
installStage.value = 'restarting'
installPollTimer = setInterval(pollHealth, 1500)
}, 2500)
@@ -687,22 +714,37 @@ async function pollHealth() {
installStage.value = 'ready'
if (installPollTimer) { clearInterval(installPollTimer); installPollTimer = null }
// Brief pause so the user sees the "Ready" state before the reload.
setTimeout(() => { window.location.reload() }, 1200)
installReadyTimer = setTimeout(() => { window.location.reload() }, 1200)
} else {
// Backend is up but still reporting the old version — frontend
// and backend are mid-swap. Signal to the user.
installStage.value = 'reconnecting'
void confirmBackendUpdateSettled()
}
} catch {
// Fetch fails while the server is mid-restart. Stay in 'restarting'.
}
}
async function confirmBackendUpdateSettled() {
try {
const res = await rpcClient.call<{
current_version: string
update_in_progress: boolean
}>({ method: 'update.status' })
if (!res.update_in_progress && installStage.value !== 'ready' && installStage.value !== 'stalled') {
installStage.value = 'ready'
if (installPollTimer) { clearInterval(installPollTimer); installPollTimer = null }
installReadyTimer = setTimeout(() => { window.location.reload() }, 800)
}
} catch {
// Keep waiting on /health.
}
}
function reloadNow() { window.location.reload() }
// Cleanup if the component is torn down mid-install (unlikely but safe).
import { onBeforeUnmount } from 'vue'
onBeforeUnmount(() => {
if (installPollTimer) clearInterval(installPollTimer)
if (installElapsedTimer) clearInterval(installElapsedTimer)
clearInstallTimers()
})
const lastCheckDisplay = computed(() => {

View File

@@ -16,7 +16,7 @@
</div>
<button
@click="$emit('generate-invite', 'trusted')"
class="w-fit px-3 py-1.5 glass-button glass-button-sm rounded text-xs font-medium text-white/90 hover:text-white transition-colors disabled:opacity-50"
class="w-full sm:w-fit px-3 py-1.5 glass-button glass-button-sm rounded text-xs font-medium text-white/90 hover:text-white transition-colors disabled:opacity-50"
:disabled="generatingInvite"
>
{{ generatingInvite && inviteType === 'trusted' ? 'Generating...' : 'Generate Code' }}
@@ -36,7 +36,7 @@
</div>
<button
@click="$emit('generate-invite', 'observer')"
class="w-fit px-3 py-1.5 glass-button glass-button-sm rounded text-xs font-medium text-white/90 hover:text-white transition-colors disabled:opacity-50"
class="w-full sm:w-fit px-3 py-1.5 glass-button glass-button-sm rounded text-xs font-medium text-white/90 hover:text-white transition-colors disabled:opacity-50"
:disabled="generatingInvite"
>
{{ generatingInvite && inviteType === 'observer' ? 'Generating...' : 'Generate Code' }}
@@ -56,7 +56,7 @@
</div>
<button
@click="$emit('show-join')"
class="w-fit px-3 py-1.5 glass-button glass-button-sm rounded text-xs font-medium text-white/90 hover:text-white transition-colors"
class="w-full sm:w-fit px-3 py-1.5 glass-button glass-button-sm rounded text-xs font-medium text-white/90 hover:text-white transition-colors"
>
Enter Code
</button>
@@ -75,7 +75,7 @@
</div>
<button
@click="$emit('sync')"
class="w-fit px-3 py-1.5 glass-button glass-button-sm rounded text-xs font-medium text-white/90 hover:text-white transition-colors disabled:opacity-50"
class="w-full sm:w-fit px-3 py-1.5 glass-button glass-button-sm rounded text-xs font-medium text-white/90 hover:text-white transition-colors disabled:opacity-50"
:disabled="syncing"
>
{{ syncing ? 'Syncing...' : 'Sync Now' }}

View File

@@ -13,9 +13,10 @@
<th
v-for="node in sortedNodes"
:key="node.node_id"
class="fleet-matrix-header-cell font-mono"
class="fleet-matrix-header-cell"
:title="fleetNodeSubtitle(node)"
>
{{ node.node_id.slice(0, 6) }}
{{ fleetNodeDisplayName(node) }}
</th>
</tr>
</thead>
@@ -39,7 +40,7 @@
</template>
<script setup lang="ts">
import { type FleetNode, getContainerState } from './useFleetData'
import { type FleetNode, getContainerState, fleetNodeDisplayName, fleetNodeSubtitle } from './useFleetData'
defineProps<{
nodes: FleetNode[]

View File

@@ -11,7 +11,7 @@
<div class="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
<div class="monitoring-stat-card">
<p class="text-xs text-white/50 uppercase tracking-wide">Hostname</p>
<p class="text-lg font-bold text-white truncate">{{ node.hostname || 'Unknown' }}</p>
<p class="text-lg font-bold text-white truncate">{{ node.hostname || fleetNodeDisplayName(node) }}</p>
</div>
<div class="monitoring-stat-card">
<p class="text-xs text-white/50 uppercase tracking-wide">Address</p>

View File

@@ -187,21 +187,59 @@ export function normalizeNodeHistoryResponse(data: {
return []
}
type FleetCache = {
nodes: FleetNode[]
fleetAlerts: FleetAlert[]
lastRefreshed: string
selectedNodeId: string | null
sortBy: SortOption
}
const FLEET_CACHE_KEY = 'archipelago.fleet.cache.v1'
function readFleetCache(): Partial<FleetCache> {
if (typeof window === 'undefined') return {}
try {
const raw = window.sessionStorage.getItem(FLEET_CACHE_KEY)
if (!raw) return {}
const parsed = JSON.parse(raw) as Partial<FleetCache>
return {
nodes: Array.isArray(parsed.nodes) ? parsed.nodes.map(normalizeFleetNode) : [],
fleetAlerts: Array.isArray(parsed.fleetAlerts) ? parsed.fleetAlerts : [],
lastRefreshed: typeof parsed.lastRefreshed === 'string' ? parsed.lastRefreshed : '',
selectedNodeId: typeof parsed.selectedNodeId === 'string' ? parsed.selectedNodeId : null,
sortBy: parsed.sortBy === 'last-seen' || parsed.sortBy === 'name' ? parsed.sortBy : 'status',
}
} catch {
return {}
}
}
function writeFleetCache(state: FleetCache) {
if (typeof window === 'undefined') return
try {
window.sessionStorage.setItem(FLEET_CACHE_KEY, JSON.stringify(state))
} catch {
// Cache is opportunistic only.
}
}
// --- Composable ---
export function useFleetData() {
const loading = ref(true)
const cached = readFleetCache()
const loading = ref(!(cached.nodes?.length ?? 0))
const errorMessage = ref('')
const nodes = ref<FleetNode[]>([])
const fleetAlerts = ref<FleetAlert[]>([])
const nodes = ref<FleetNode[]>(cached.nodes ?? [])
const fleetAlerts = ref<FleetAlert[]>(cached.fleetAlerts ?? [])
const refreshing = ref(false)
const alertsLoading = ref(false)
const selectedNodeId = ref<string | null>(null)
const selectedNodeId = ref<string | null>(cached.selectedNodeId ?? null)
const nodeHistory = ref<NodeHistoryEntry[]>([])
const nodeHistoryLoading = ref(false)
const autoRefresh = ref(true)
const lastRefreshed = ref('')
const sortBy = ref<SortOption>('status')
const lastRefreshed = ref(cached.lastRefreshed ?? '')
const sortBy = ref<SortOption>(cached.sortBy ?? 'status')
const chartWidth = ref(300)
let pollTimer: ReturnType<typeof setInterval> | null = null
@@ -284,6 +322,13 @@ export function useFleetData() {
if (data?.nodes) {
nodes.value = data.nodes.map(normalizeFleetNode)
lastRefreshed.value = new Date().toISOString()
writeFleetCache({
nodes: nodes.value,
fleetAlerts: fleetAlerts.value,
lastRefreshed: lastRefreshed.value,
selectedNodeId: selectedNodeId.value,
sortBy: sortBy.value,
})
}
} catch (err) {
if (loading.value) {
@@ -300,6 +345,13 @@ export function useFleetData() {
})
if (data?.alerts) {
fleetAlerts.value = data.alerts
writeFleetCache({
nodes: nodes.value,
fleetAlerts: fleetAlerts.value,
lastRefreshed: lastRefreshed.value,
selectedNodeId: selectedNodeId.value,
sortBy: sortBy.value,
})
}
} catch {
// Non-critical, retry on next poll
@@ -342,6 +394,13 @@ export function useFleetData() {
} else {
selectedNodeId.value = nodeId
}
writeFleetCache({
nodes: nodes.value,
fleetAlerts: fleetAlerts.value,
lastRefreshed: lastRefreshed.value,
selectedNodeId: selectedNodeId.value,
sortBy: sortBy.value,
})
}
function toggleAutoRefresh() {
@@ -400,6 +459,23 @@ export function useFleetData() {
} else {
nodeHistory.value = []
}
writeFleetCache({
nodes: nodes.value,
fleetAlerts: fleetAlerts.value,
lastRefreshed: lastRefreshed.value,
selectedNodeId: selectedNodeId.value,
sortBy: sortBy.value,
})
})
watch(sortBy, () => {
writeFleetCache({
nodes: nodes.value,
fleetAlerts: fleetAlerts.value,
lastRefreshed: lastRefreshed.value,
selectedNodeId: selectedNodeId.value,
sortBy: sortBy.value,
})
})
// --- Lifecycle ---

View File

@@ -5,7 +5,7 @@ import { RouterLink } from 'vue-router'
<template>
<!-- App Registries Section -->
<div class="glass-card px-6 py-6 mb-6">
<div class="flex items-center justify-between">
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div>
<h2 class="text-xl font-semibold text-white/96">App registries</h2>
<p class="text-sm text-white/60 mt-1">
@@ -14,7 +14,7 @@ import { RouterLink } from 'vue-router'
</div>
<RouterLink
to="/dashboard/settings/registries"
class="glass-button px-4 py-2 rounded-lg text-sm flex items-center gap-2"
class="glass-button px-4 py-2 rounded-lg text-sm flex w-full items-center justify-center gap-2 sm:w-auto"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"

View File

@@ -7,12 +7,15 @@ const { t } = useI18n()
<template>
<!-- System Updates Section -->
<div class="glass-card px-6 py-6 mb-6">
<div class="flex items-center justify-between">
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div>
<h2 class="text-xl font-semibold text-white/96">{{ t('settings.systemUpdates') }}</h2>
<p class="text-sm text-white/60 mt-1">{{ t('settings.systemUpdatesDesc') }}</p>
</div>
<RouterLink to="/dashboard/settings/update" class="glass-button px-4 py-2 rounded-lg text-sm flex items-center gap-2">
<RouterLink
to="/dashboard/settings/update"
class="glass-button px-4 py-2 rounded-lg text-sm flex w-full items-center justify-center gap-2 sm:w-auto"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
</svg>

View File

@@ -324,14 +324,49 @@ const messageToast = useMessageToast()
const web5Badge = useWeb5BadgeStore()
const appStore = useAppStore()
const CONNECTED_NODES_CACHE_KEY = 'archipelago.web5.connected-nodes.v1'
type ConnectedNodesCache = {
peers: Peer[]
observers: Peer[]
peerReachable: Record<string, boolean>
connectionRequests: ConnectionRequest[]
}
function readConnectedNodesCache(): Partial<ConnectedNodesCache> {
if (typeof window === 'undefined') return {}
try {
const raw = window.sessionStorage.getItem(CONNECTED_NODES_CACHE_KEY)
if (!raw) return {}
const parsed = JSON.parse(raw) as Partial<ConnectedNodesCache>
return {
peers: Array.isArray(parsed.peers) ? parsed.peers : [],
observers: Array.isArray(parsed.observers) ? parsed.observers : [],
peerReachable: parsed.peerReachable && typeof parsed.peerReachable === 'object' ? parsed.peerReachable : {},
connectionRequests: Array.isArray(parsed.connectionRequests) ? parsed.connectionRequests : [],
}
} catch {
return {}
}
}
function writeConnectedNodesCache(state: ConnectedNodesCache) {
if (typeof window === 'undefined') return
try {
window.sessionStorage.setItem(CONNECTED_NODES_CACHE_KEY, JSON.stringify(state))
} catch {
// Cache is best-effort.
}
}
const nodesContainerRef = ref<HTMLElement | null>(null)
const nodesContainerTab = ref<'trusted' | 'observers' | 'messages' | 'requests'>('trusted')
const { receivedMessages, loadingMessages, unreadCount, loadReceivedMessages, markAsRead } = messageToast
const peers = ref<Peer[]>([])
const observers = ref<Peer[]>([])
const cached = readConnectedNodesCache()
const peers = ref<Peer[]>(cached.peers ?? [])
const observers = ref<Peer[]>(cached.observers ?? [])
const loadingPeers = ref(false)
const peerReachableLocal = ref<Record<string, boolean>>({})
const peerReachableLocal = ref<Record<string, boolean>>(cached.peerReachable ?? {})
const peerReachable = computed(() => ({ ...appStore.peerHealth, ...peerReachableLocal.value }))
const discovering = ref(false)
@@ -351,7 +386,7 @@ const sendMessageError = ref('')
const sendMessageSuccess = ref('')
// Connection requests
const connectionRequests = ref<ConnectionRequest[]>([])
const connectionRequests = ref<ConnectionRequest[]>(cached.connectionRequests ?? [])
const loadingRequests = ref(false)
const processingRequestId = ref<string | null>(null)
@@ -388,6 +423,7 @@ function switchToRequestsTab() {
}
async function loadPeers() {
const hadPeers = peers.value.length > 0 || observers.value.length > 0
loadingPeers.value = true
try {
const res = await rpcClient.listPeers()
@@ -427,8 +463,18 @@ async function loadPeers() {
peerReachableLocal.value[p.onion] = false
}
}
writeConnectedNodesCache({
peers: peers.value,
observers: observers.value,
peerReachable: peerReachableLocal.value,
connectionRequests: connectionRequests.value,
})
} catch (e) {
if (import.meta.env.DEV) console.error('Failed to load peers:', e)
if (!hadPeers) {
peers.value = []
observers.value = []
}
} finally {
loadingPeers.value = false
}
@@ -483,6 +529,12 @@ async function loadConnectionRequests() {
const res = await rpcClient.call<{ requests: ConnectionRequest[] }>({ method: 'network.list-requests' })
connectionRequests.value = res.requests || []
web5Badge.pendingRequestCount = connectionRequests.value.length
writeConnectedNodesCache({
peers: peers.value,
observers: observers.value,
peerReachable: peerReachableLocal.value,
connectionRequests: connectionRequests.value,
})
} catch {
if (!hadRequests) connectionRequests.value = []
} finally {
@@ -496,6 +548,12 @@ async function acceptRequest(requestId: string) {
await rpcClient.call({ method: 'network.accept-request', params: { request_id: requestId } })
connectionRequests.value = connectionRequests.value.filter(r => r.id !== requestId)
web5Badge.pendingRequestCount = connectionRequests.value.length
writeConnectedNodesCache({
peers: peers.value,
observers: observers.value,
peerReachable: peerReachableLocal.value,
connectionRequests: connectionRequests.value,
})
await loadPeers()
emit('toast', t('web5.connectionAccepted'))
} catch {
@@ -511,6 +569,12 @@ async function rejectRequest(requestId: string) {
await rpcClient.call({ method: 'network.reject-request', params: { request_id: requestId } })
connectionRequests.value = connectionRequests.value.filter(r => r.id !== requestId)
web5Badge.pendingRequestCount = connectionRequests.value.length
writeConnectedNodesCache({
peers: peers.value,
observers: observers.value,
peerReachable: peerReachableLocal.value,
connectionRequests: connectionRequests.value,
})
emit('toast', t('web5.requestRejected'))
} catch {
emit('toast', t('web5.failedToRejectRequest'))

View File

@@ -389,6 +389,29 @@ import type { ManagedIdentity, IdentityProfile } from './types'
const { t } = useI18n()
const IDENTITIES_CACHE_KEY = 'archipelago.web5.identities.v1'
function readIdentitiesCache(): ManagedIdentity[] {
if (typeof window === 'undefined') return []
try {
const raw = window.sessionStorage.getItem(IDENTITIES_CACHE_KEY)
if (!raw) return []
const parsed = JSON.parse(raw) as ManagedIdentity[]
return Array.isArray(parsed) ? parsed : []
} catch {
return []
}
}
function writeIdentitiesCache(identities: ManagedIdentity[]) {
if (typeof window === 'undefined') return
try {
window.sessionStorage.setItem(IDENTITIES_CACHE_KEY, JSON.stringify(identities))
} catch {
// Cache is opportunistic only.
}
}
defineProps<{
showStagger: boolean
}>()
@@ -397,7 +420,7 @@ const emit = defineEmits<{
toast: [text: string]
}>()
const managedIdentities = ref<ManagedIdentity[]>([])
const managedIdentities = ref<ManagedIdentity[]>(readIdentitiesCache())
const identitiesLoading = ref(false)
const showCreateIdentityModal = ref(false)
const newIdentityName = ref('Personal')
@@ -508,6 +531,7 @@ async function loadIdentities() {
try {
const res = await rpcClient.call<{ identities: ManagedIdentity[] }>({ method: 'identity.list' })
managedIdentities.value = res.identities || []
writeIdentitiesCache(managedIdentities.value)
} catch {
if (!hadIdentities) managedIdentities.value = []
} finally {

View File

@@ -33,12 +33,12 @@
</div>
</div>
<div v-if="userDid" class="flex gap-2 mt-auto">
<button
@click="$emit('copyDid')"
class="flex-1 px-3 py-1.5 glass-button glass-button-sm rounded text-xs font-medium text-white/90 hover:text-white transition-colors"
>
{{ didCopied ? t('common.copiedBang') : t('web5.copyDid') }}
</button>
<button
@click="$emit('copyDid')"
class="flex-1 px-3 py-1.5 glass-button glass-button-sm rounded text-xs font-medium text-white/90 hover:text-white transition-colors"
>
{{ didCopied ? t('common.copiedBang') : t('web5.copyDid') }}
</button>
<button
@click="$emit('showDidDocument')"
class="flex-1 px-3 py-1.5 glass-button glass-button-sm rounded text-xs font-medium text-white/90 hover:text-white transition-colors"
@@ -69,19 +69,19 @@
</div>
</div>
<div v-if="dhtDid" class="flex gap-2 mt-auto">
<button
@click="$emit('copyDhtDid')"
class="flex-1 px-3 py-1.5 glass-button glass-button-sm rounded text-xs font-medium text-white/90 hover:text-white transition-colors"
>
{{ dhtDidCopied ? 'Copied!' : 'Copy' }}
</button>
<button
@click="$emit('refreshDhtDid')"
:disabled="publishingDht"
class="flex-1 px-3 py-1.5 glass-button glass-button-sm rounded text-xs font-medium text-white/90 hover:text-white transition-colors disabled:opacity-50"
>
{{ publishingDht ? 'Refreshing...' : 'Refresh DHT' }}
</button>
<button
@click="$emit('copyDhtDid')"
class="flex-1 px-3 py-1.5 glass-button glass-button-sm rounded text-xs font-medium text-white/90 hover:text-white transition-colors"
>
{{ dhtDidCopied ? 'Copied!' : 'Copy' }}
</button>
<button
@click="$emit('refreshDhtDid')"
:disabled="publishingDht"
class="flex-1 px-3 py-1.5 glass-button glass-button-sm rounded text-xs font-medium text-white/90 hover:text-white transition-colors disabled:opacity-50"
>
{{ publishingDht ? 'Refreshing...' : 'Refresh DHT' }}
</button>
</div>
<button
v-else-if="userDid"

View File

@@ -1,30 +1,30 @@
{
"version": "1.7.85-alpha",
"version": "1.7.86-alpha",
"release_date": "2026-06-12",
"changelog": [
"ElectrumX now runs with less cache pressure and more memory headroom, reducing the restart loop seen during sync catch-up.",
"Portainer is pinned to `2.19.4` instead of `latest`, avoiding schema-drift restarts from surprise image updates.",
"LND receive-address creation now asks for a native SegWit address and returns clearer wallet/readiness failures when an address is not available.",
"Fleet telemetry now carries server name, hostname, and server URL, and the Fleet dashboard shows those names instead of hashed node ids.",
"Trusted federation peers are still auto-added transitively, but the local node no longer imports itself back into the fleet list.",
"Validation passed locally for the touched frontend helpers, `git diff --check`, and Rust formatting."
"Fleet now preserves the last known node list, alerts, and selection locally while telemetry refreshes in the background, so the dashboard no longer blanks on tab switches or update scans.",
"Connected nodes and identities now reuse their last loaded data instead of reloading the visible list every time the user revisits the tab.",
"The Fleet matrix and detail views now show actual node names and host information instead of raw node id prefixes.",
"The network map only redraws when its graph data actually changes, which stops the D3 scene from visually resetting on every refresh tick.",
"Mobile federation and system-update actions now stack full width, and the ElectrumX app health check allows a long startup window so slow sync nodes do not restart mid-index.",
"Validation passed with `git diff --check`, focused frontend tests, and `npm run type-check`."
],
"components": [
{
"name": "archipelago",
"current_version": "1.7.85-alpha",
"new_version": "1.7.85-alpha",
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.85-alpha/archipelago",
"sha256": "06a6fe6e8f2e50bcda6c152c2de1a874edc84b2e65377f6e06d195c4eebc9cde",
"size_bytes": 44049488
"current_version": "1.7.86-alpha",
"new_version": "1.7.86-alpha",
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.86-alpha/archipelago",
"sha256": "2eb936fe188df25947a2d626af1b22be2f7ef9dcbc927542389de3b38e80f9e6",
"size_bytes": 44050232
},
{
"name": "archipelago-frontend-1.7.85-alpha.tar.gz",
"current_version": "1.7.85-alpha",
"new_version": "1.7.85-alpha",
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.85-alpha/archipelago-frontend-1.7.85-alpha.tar.gz",
"sha256": "c809fb27772773925d89b711236a81834465229d9f544bd65cf5816776cfda76",
"size_bytes": 184057997
"name": "archipelago-frontend-1.7.86-alpha.tar.gz",
"current_version": "1.7.86-alpha",
"new_version": "1.7.86-alpha",
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.86-alpha/archipelago-frontend-1.7.86-alpha.tar.gz",
"sha256": "9f6f146eaf709cd3e778550bb7a56877dce551cb6d631bccf08e80ed977dd17b",
"size_bytes": 184060614
}
]
}

View File

@@ -1,30 +1,30 @@
{
"version": "1.7.85-alpha",
"version": "1.7.86-alpha",
"release_date": "2026-06-12",
"changelog": [
"ElectrumX now runs with less cache pressure and more memory headroom, reducing the restart loop seen during sync catch-up.",
"Portainer is pinned to `2.19.4` instead of `latest`, avoiding schema-drift restarts from surprise image updates.",
"LND receive-address creation now asks for a native SegWit address and returns clearer wallet/readiness failures when an address is not available.",
"Fleet telemetry now carries server name, hostname, and server URL, and the Fleet dashboard shows those names instead of hashed node ids.",
"Trusted federation peers are still auto-added transitively, but the local node no longer imports itself back into the fleet list.",
"Validation passed locally for the touched frontend helpers, `git diff --check`, and Rust formatting."
"Fleet now preserves the last known node list, alerts, and selection locally while telemetry refreshes in the background, so the dashboard no longer blanks on tab switches or update scans.",
"Connected nodes and identities now reuse their last loaded data instead of reloading the visible list every time the user revisits the tab.",
"The Fleet matrix and detail views now show actual node names and host information instead of raw node id prefixes.",
"The network map only redraws when its graph data actually changes, which stops the D3 scene from visually resetting on every refresh tick.",
"Mobile federation and system-update actions now stack full width, and the ElectrumX app health check allows a long startup window so slow sync nodes do not restart mid-index.",
"Validation passed with `git diff --check`, focused frontend tests, and `npm run type-check`."
],
"components": [
{
"name": "archipelago",
"current_version": "1.7.85-alpha",
"new_version": "1.7.85-alpha",
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.85-alpha/archipelago",
"sha256": "06a6fe6e8f2e50bcda6c152c2de1a874edc84b2e65377f6e06d195c4eebc9cde",
"size_bytes": 44049488
"current_version": "1.7.86-alpha",
"new_version": "1.7.86-alpha",
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.86-alpha/archipelago",
"sha256": "2eb936fe188df25947a2d626af1b22be2f7ef9dcbc927542389de3b38e80f9e6",
"size_bytes": 44050232
},
{
"name": "archipelago-frontend-1.7.85-alpha.tar.gz",
"current_version": "1.7.85-alpha",
"new_version": "1.7.85-alpha",
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.85-alpha/archipelago-frontend-1.7.85-alpha.tar.gz",
"sha256": "c809fb27772773925d89b711236a81834465229d9f544bd65cf5816776cfda76",
"size_bytes": 184057997
"name": "archipelago-frontend-1.7.86-alpha.tar.gz",
"current_version": "1.7.86-alpha",
"new_version": "1.7.86-alpha",
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.86-alpha/archipelago-frontend-1.7.86-alpha.tar.gz",
"sha256": "9f6f146eaf709cd3e778550bb7a56877dce551cb6d631bccf08e80ed977dd17b",
"size_bytes": 184060614
}
]
}