chore: release v1.7.86-alpha
This commit is contained in:
parent
e474a2b4c9
commit
146e641b57
@ -1,5 +1,14 @@
|
|||||||
# Changelog
|
# 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)
|
## 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.
|
- ElectrumX now runs with less cache pressure and more memory headroom, reducing the restart loop seen during sync catch-up.
|
||||||
|
|||||||
@ -57,6 +57,7 @@ app:
|
|||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 3
|
retries: 3
|
||||||
|
start_period: 10m
|
||||||
|
|
||||||
bitcoin_integration:
|
bitcoin_integration:
|
||||||
rpc_access: read-only
|
rpc_access: read-only
|
||||||
|
|||||||
2
core/Cargo.lock
generated
2
core/Cargo.lock
generated
@ -80,7 +80,7 @@ checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "archipelago"
|
name = "archipelago"
|
||||||
version = "1.7.85-alpha"
|
version = "1.7.86-alpha"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"archipelago-container",
|
"archipelago-container",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "archipelago"
|
name = "archipelago"
|
||||||
version = "1.7.85-alpha"
|
version = "1.7.86-alpha"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "Archipelago Bitcoin Node OS - Native backend"
|
description = "Archipelago Bitcoin Node OS - Native backend"
|
||||||
authors = ["Archipelago Team"]
|
authors = ["Archipelago Team"]
|
||||||
|
|||||||
4
neode-ui/package-lock.json
generated
4
neode-ui/package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "neode-ui",
|
"name": "neode-ui",
|
||||||
"version": "1.7.85-alpha",
|
"version": "1.7.86-alpha",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "neode-ui",
|
"name": "neode-ui",
|
||||||
"version": "1.7.85-alpha",
|
"version": "1.7.86-alpha",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/dompurify": "^3.0.5",
|
"@types/dompurify": "^3.0.5",
|
||||||
"@vue-leaflet/vue-leaflet": "^0.10.1",
|
"@vue-leaflet/vue-leaflet": "^0.10.1",
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "neode-ui",
|
"name": "neode-ui",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.7.85-alpha",
|
"version": "1.7.86-alpha",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "./start-dev.sh",
|
"start": "./start-dev.sh",
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, onUnmounted, watch } from 'vue'
|
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
|
||||||
import * as d3 from 'd3'
|
import * as d3 from 'd3'
|
||||||
|
|
||||||
interface MapNode {
|
interface MapNode {
|
||||||
@ -36,6 +36,11 @@ type SimLink = d3.SimulationLinkDatum<SimNode> & { source: string | SimNode; tar
|
|||||||
let simulation: d3.Simulation<SimNode, SimLink> | null = null
|
let simulation: d3.Simulation<SimNode, SimLink> | null = null
|
||||||
let resizeObserver: ResizeObserver | 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 {
|
function trustColor(level: string): string {
|
||||||
switch (level) {
|
switch (level) {
|
||||||
case 'trusted': return '#4ade80'
|
case 'trusted': return '#4ade80'
|
||||||
@ -50,6 +55,7 @@ function nodeRadius(n: MapNode): number {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function render() {
|
function render() {
|
||||||
|
simulation?.stop()
|
||||||
const svg = d3.select(svgRef.value!)
|
const svg = d3.select(svgRef.value!)
|
||||||
svg.selectAll('*').remove()
|
svg.selectAll('*').remove()
|
||||||
|
|
||||||
@ -160,7 +166,7 @@ onUnmounted(() => {
|
|||||||
resizeObserver?.disconnect()
|
resizeObserver?.disconnect()
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(() => [props.nodes, props.links], () => render(), { deep: true })
|
watch(graphSignature, () => render())
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@ -556,8 +556,8 @@ input[type="radio"]:active + * {
|
|||||||
context) stay above the tab bar instead of sliding underneath it. */
|
context) stay above the tab bar instead of sliding underneath it. */
|
||||||
@media (max-width: 767px) {
|
@media (max-width: 767px) {
|
||||||
.chat-iframe-mobile {
|
.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(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)) - 16px) !important;
|
height: calc(100dvh - var(--mobile-tab-bar-height, 72px) - var(--safe-area-top, env(safe-area-inset-top, 0px))) !important;
|
||||||
flex: none;
|
flex: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,11 +19,11 @@
|
|||||||
|
|
||||||
<!-- Registry list -->
|
<!-- Registry list -->
|
||||||
<div class="glass-card p-6 mb-6">
|
<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>
|
<h2 class="text-lg font-semibold text-white">Registries</h2>
|
||||||
<button
|
<button
|
||||||
type="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"
|
@click="openAddRegistry"
|
||||||
>+ Add registry</button>
|
>+ Add registry</button>
|
||||||
</div>
|
</div>
|
||||||
@ -118,7 +118,7 @@
|
|||||||
<!-- Back link -->
|
<!-- Back link -->
|
||||||
<RouterLink
|
<RouterLink
|
||||||
to="/dashboard/settings"
|
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">
|
<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" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
||||||
|
|||||||
@ -69,12 +69,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Actions -->
|
<!-- Actions -->
|
||||||
<div class="flex gap-3">
|
<div class="flex flex-col gap-3 sm:flex-row">
|
||||||
<!-- Git path: one-shot pull+rebuild+restart -->
|
<!-- Git path: one-shot pull+rebuild+restart -->
|
||||||
<button
|
<button
|
||||||
v-if="updateMethod === 'git' && !applying"
|
v-if="updateMethod === 'git' && !applying"
|
||||||
@click="requestGitApply"
|
@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') }}
|
{{ t('systemUpdate.pullAndRebuild') }}
|
||||||
</button>
|
</button>
|
||||||
@ -82,14 +82,14 @@
|
|||||||
<button
|
<button
|
||||||
v-if="updateMethod !== 'git' && !downloading && !applying && !downloaded"
|
v-if="updateMethod !== 'git' && !downloading && !applying && !downloaded"
|
||||||
@click="downloadUpdate"
|
@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') }}
|
{{ t('systemUpdate.downloadUpdate') }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="updateMethod !== 'git' && downloaded && !applying"
|
v-if="updateMethod !== 'git' && downloaded && !applying"
|
||||||
@click="requestApply"
|
@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') }}
|
{{ t('systemUpdate.applyUpdate') }}
|
||||||
</button>
|
</button>
|
||||||
@ -145,8 +145,16 @@
|
|||||||
<!-- Applying -->
|
<!-- Applying -->
|
||||||
<div v-if="applying" class="glass-card p-6 mb-6">
|
<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>
|
<h2 class="text-lg font-semibold text-white mb-4">{{ t('systemUpdate.applying') }}</h2>
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex flex-col items-start gap-3 sm:flex-row sm:items-center">
|
||||||
<div class="w-5 h-5 border-2 border-orange-400 border-t-transparent rounded-full animate-spin"></div>
|
<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>
|
<p class="text-sm text-white/70">{{ t('systemUpdate.applyWarning') }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -262,22 +270,25 @@
|
|||||||
<!-- Actions row -->
|
<!-- Actions row -->
|
||||||
<div class="glass-card p-6">
|
<div class="glass-card p-6">
|
||||||
<h2 class="text-lg font-semibold text-white mb-4">{{ t('systemUpdate.actions') }}</h2>
|
<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
|
<button
|
||||||
@click="checkForUpdates"
|
@click="checkForUpdates"
|
||||||
:disabled="loading"
|
: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') }}
|
{{ loading ? t('systemUpdate.checking') : t('systemUpdate.checkForUpdates') }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="rollbackAvailable"
|
v-if="rollbackAvailable"
|
||||||
@click="requestRollback"
|
@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') }}
|
{{ t('systemUpdate.rollback') }}
|
||||||
</button>
|
</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') }}
|
{{ t('systemUpdate.backToSettings') }}
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
</div>
|
</div>
|
||||||
@ -650,12 +661,28 @@ const installStartedAt = ref<number>(0)
|
|||||||
const installElapsedSec = ref(0)
|
const installElapsedSec = ref(0)
|
||||||
let installPollTimer: ReturnType<typeof setInterval> | null = null
|
let installPollTimer: ReturnType<typeof setInterval> | null = null
|
||||||
let installElapsedTimer: ReturnType<typeof setInterval> | null = null
|
let installElapsedTimer: ReturnType<typeof setInterval> | null = null
|
||||||
|
let installReadyTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
const installElapsedLabel = computed(() => {
|
const installElapsedLabel = computed(() => {
|
||||||
const s = installElapsedSec.value
|
const s = installElapsedSec.value
|
||||||
if (s < 60) return `Elapsed: ${s}s`
|
if (s < 60) return `Elapsed: ${s}s`
|
||||||
return `Elapsed: ${Math.floor(s / 60)}m${s % 60 < 10 ? '0' : ''}${s % 60}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) {
|
function startInstallOverlay(targetVersion: string) {
|
||||||
|
clearInstallTimers()
|
||||||
installing.value = true
|
installing.value = true
|
||||||
installStage.value = 'applying'
|
installStage.value = 'applying'
|
||||||
installTargetVersion.value = targetVersion
|
installTargetVersion.value = targetVersion
|
||||||
@ -672,7 +699,7 @@ function startInstallOverlay(targetVersion: string) {
|
|||||||
// Start polling /health after a short delay — the backend restarts 2s
|
// Start polling /health after a short delay — the backend restarts 2s
|
||||||
// after replying to update.apply, so an immediate poll would see the
|
// after replying to update.apply, so an immediate poll would see the
|
||||||
// old backend and conclude nothing happened.
|
// old backend and conclude nothing happened.
|
||||||
setTimeout(() => {
|
installReadyTimer = setTimeout(() => {
|
||||||
installStage.value = 'restarting'
|
installStage.value = 'restarting'
|
||||||
installPollTimer = setInterval(pollHealth, 1500)
|
installPollTimer = setInterval(pollHealth, 1500)
|
||||||
}, 2500)
|
}, 2500)
|
||||||
@ -687,22 +714,37 @@ async function pollHealth() {
|
|||||||
installStage.value = 'ready'
|
installStage.value = 'ready'
|
||||||
if (installPollTimer) { clearInterval(installPollTimer); installPollTimer = null }
|
if (installPollTimer) { clearInterval(installPollTimer); installPollTimer = null }
|
||||||
// Brief pause so the user sees the "Ready" state before the reload.
|
// Brief pause so the user sees the "Ready" state before the reload.
|
||||||
setTimeout(() => { window.location.reload() }, 1200)
|
installReadyTimer = setTimeout(() => { window.location.reload() }, 1200)
|
||||||
} else {
|
} else {
|
||||||
// Backend is up but still reporting the old version — frontend
|
// Backend is up but still reporting the old version — frontend
|
||||||
// and backend are mid-swap. Signal to the user.
|
// and backend are mid-swap. Signal to the user.
|
||||||
installStage.value = 'reconnecting'
|
installStage.value = 'reconnecting'
|
||||||
|
void confirmBackendUpdateSettled()
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Fetch fails while the server is mid-restart. Stay in 'restarting'.
|
// 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() }
|
function reloadNow() { window.location.reload() }
|
||||||
// Cleanup if the component is torn down mid-install (unlikely but safe).
|
// Cleanup if the component is torn down mid-install (unlikely but safe).
|
||||||
import { onBeforeUnmount } from 'vue'
|
import { onBeforeUnmount } from 'vue'
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
if (installPollTimer) clearInterval(installPollTimer)
|
clearInstallTimers()
|
||||||
if (installElapsedTimer) clearInterval(installElapsedTimer)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const lastCheckDisplay = computed(() => {
|
const lastCheckDisplay = computed(() => {
|
||||||
|
|||||||
@ -16,7 +16,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@click="$emit('generate-invite', 'trusted')"
|
@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"
|
:disabled="generatingInvite"
|
||||||
>
|
>
|
||||||
{{ generatingInvite && inviteType === 'trusted' ? 'Generating...' : 'Generate Code' }}
|
{{ generatingInvite && inviteType === 'trusted' ? 'Generating...' : 'Generate Code' }}
|
||||||
@ -36,7 +36,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@click="$emit('generate-invite', 'observer')"
|
@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"
|
:disabled="generatingInvite"
|
||||||
>
|
>
|
||||||
{{ generatingInvite && inviteType === 'observer' ? 'Generating...' : 'Generate Code' }}
|
{{ generatingInvite && inviteType === 'observer' ? 'Generating...' : 'Generate Code' }}
|
||||||
@ -56,7 +56,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@click="$emit('show-join')"
|
@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
|
Enter Code
|
||||||
</button>
|
</button>
|
||||||
@ -75,7 +75,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@click="$emit('sync')"
|
@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"
|
:disabled="syncing"
|
||||||
>
|
>
|
||||||
{{ syncing ? 'Syncing...' : 'Sync Now' }}
|
{{ syncing ? 'Syncing...' : 'Sync Now' }}
|
||||||
|
|||||||
@ -13,9 +13,10 @@
|
|||||||
<th
|
<th
|
||||||
v-for="node in sortedNodes"
|
v-for="node in sortedNodes"
|
||||||
:key="node.node_id"
|
: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>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@ -39,7 +40,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { type FleetNode, getContainerState } from './useFleetData'
|
import { type FleetNode, getContainerState, fleetNodeDisplayName, fleetNodeSubtitle } from './useFleetData'
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
nodes: FleetNode[]
|
nodes: FleetNode[]
|
||||||
|
|||||||
@ -11,7 +11,7 @@
|
|||||||
<div class="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
<div class="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
||||||
<div class="monitoring-stat-card">
|
<div class="monitoring-stat-card">
|
||||||
<p class="text-xs text-white/50 uppercase tracking-wide">Hostname</p>
|
<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>
|
||||||
<div class="monitoring-stat-card">
|
<div class="monitoring-stat-card">
|
||||||
<p class="text-xs text-white/50 uppercase tracking-wide">Address</p>
|
<p class="text-xs text-white/50 uppercase tracking-wide">Address</p>
|
||||||
|
|||||||
@ -187,21 +187,59 @@ export function normalizeNodeHistoryResponse(data: {
|
|||||||
return []
|
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 ---
|
// --- Composable ---
|
||||||
|
|
||||||
export function useFleetData() {
|
export function useFleetData() {
|
||||||
const loading = ref(true)
|
const cached = readFleetCache()
|
||||||
|
const loading = ref(!(cached.nodes?.length ?? 0))
|
||||||
const errorMessage = ref('')
|
const errorMessage = ref('')
|
||||||
const nodes = ref<FleetNode[]>([])
|
const nodes = ref<FleetNode[]>(cached.nodes ?? [])
|
||||||
const fleetAlerts = ref<FleetAlert[]>([])
|
const fleetAlerts = ref<FleetAlert[]>(cached.fleetAlerts ?? [])
|
||||||
const refreshing = ref(false)
|
const refreshing = ref(false)
|
||||||
const alertsLoading = 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 nodeHistory = ref<NodeHistoryEntry[]>([])
|
||||||
const nodeHistoryLoading = ref(false)
|
const nodeHistoryLoading = ref(false)
|
||||||
const autoRefresh = ref(true)
|
const autoRefresh = ref(true)
|
||||||
const lastRefreshed = ref('')
|
const lastRefreshed = ref(cached.lastRefreshed ?? '')
|
||||||
const sortBy = ref<SortOption>('status')
|
const sortBy = ref<SortOption>(cached.sortBy ?? 'status')
|
||||||
const chartWidth = ref(300)
|
const chartWidth = ref(300)
|
||||||
let pollTimer: ReturnType<typeof setInterval> | null = null
|
let pollTimer: ReturnType<typeof setInterval> | null = null
|
||||||
|
|
||||||
@ -284,6 +322,13 @@ export function useFleetData() {
|
|||||||
if (data?.nodes) {
|
if (data?.nodes) {
|
||||||
nodes.value = data.nodes.map(normalizeFleetNode)
|
nodes.value = data.nodes.map(normalizeFleetNode)
|
||||||
lastRefreshed.value = new Date().toISOString()
|
lastRefreshed.value = new Date().toISOString()
|
||||||
|
writeFleetCache({
|
||||||
|
nodes: nodes.value,
|
||||||
|
fleetAlerts: fleetAlerts.value,
|
||||||
|
lastRefreshed: lastRefreshed.value,
|
||||||
|
selectedNodeId: selectedNodeId.value,
|
||||||
|
sortBy: sortBy.value,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (loading.value) {
|
if (loading.value) {
|
||||||
@ -300,6 +345,13 @@ export function useFleetData() {
|
|||||||
})
|
})
|
||||||
if (data?.alerts) {
|
if (data?.alerts) {
|
||||||
fleetAlerts.value = data.alerts
|
fleetAlerts.value = data.alerts
|
||||||
|
writeFleetCache({
|
||||||
|
nodes: nodes.value,
|
||||||
|
fleetAlerts: fleetAlerts.value,
|
||||||
|
lastRefreshed: lastRefreshed.value,
|
||||||
|
selectedNodeId: selectedNodeId.value,
|
||||||
|
sortBy: sortBy.value,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Non-critical, retry on next poll
|
// Non-critical, retry on next poll
|
||||||
@ -342,6 +394,13 @@ export function useFleetData() {
|
|||||||
} else {
|
} else {
|
||||||
selectedNodeId.value = nodeId
|
selectedNodeId.value = nodeId
|
||||||
}
|
}
|
||||||
|
writeFleetCache({
|
||||||
|
nodes: nodes.value,
|
||||||
|
fleetAlerts: fleetAlerts.value,
|
||||||
|
lastRefreshed: lastRefreshed.value,
|
||||||
|
selectedNodeId: selectedNodeId.value,
|
||||||
|
sortBy: sortBy.value,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleAutoRefresh() {
|
function toggleAutoRefresh() {
|
||||||
@ -400,6 +459,23 @@ export function useFleetData() {
|
|||||||
} else {
|
} else {
|
||||||
nodeHistory.value = []
|
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 ---
|
// --- Lifecycle ---
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { RouterLink } from 'vue-router'
|
|||||||
<template>
|
<template>
|
||||||
<!-- App Registries Section -->
|
<!-- App Registries Section -->
|
||||||
<div class="glass-card px-6 py-6 mb-6">
|
<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>
|
<div>
|
||||||
<h2 class="text-xl font-semibold text-white/96">App registries</h2>
|
<h2 class="text-xl font-semibold text-white/96">App registries</h2>
|
||||||
<p class="text-sm text-white/60 mt-1">
|
<p class="text-sm text-white/60 mt-1">
|
||||||
@ -14,7 +14,7 @@ import { RouterLink } from 'vue-router'
|
|||||||
</div>
|
</div>
|
||||||
<RouterLink
|
<RouterLink
|
||||||
to="/dashboard/settings/registries"
|
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">
|
<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"
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
|||||||
@ -7,12 +7,15 @@ const { t } = useI18n()
|
|||||||
<template>
|
<template>
|
||||||
<!-- System Updates Section -->
|
<!-- System Updates Section -->
|
||||||
<div class="glass-card px-6 py-6 mb-6">
|
<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>
|
<div>
|
||||||
<h2 class="text-xl font-semibold text-white/96">{{ t('settings.systemUpdates') }}</h2>
|
<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>
|
<p class="text-sm text-white/60 mt-1">{{ t('settings.systemUpdatesDesc') }}</p>
|
||||||
</div>
|
</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">
|
<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" />
|
<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>
|
</svg>
|
||||||
|
|||||||
@ -324,14 +324,49 @@ const messageToast = useMessageToast()
|
|||||||
const web5Badge = useWeb5BadgeStore()
|
const web5Badge = useWeb5BadgeStore()
|
||||||
const appStore = useAppStore()
|
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 nodesContainerRef = ref<HTMLElement | null>(null)
|
||||||
const nodesContainerTab = ref<'trusted' | 'observers' | 'messages' | 'requests'>('trusted')
|
const nodesContainerTab = ref<'trusted' | 'observers' | 'messages' | 'requests'>('trusted')
|
||||||
const { receivedMessages, loadingMessages, unreadCount, loadReceivedMessages, markAsRead } = messageToast
|
const { receivedMessages, loadingMessages, unreadCount, loadReceivedMessages, markAsRead } = messageToast
|
||||||
|
|
||||||
const peers = ref<Peer[]>([])
|
const cached = readConnectedNodesCache()
|
||||||
const observers = ref<Peer[]>([])
|
const peers = ref<Peer[]>(cached.peers ?? [])
|
||||||
|
const observers = ref<Peer[]>(cached.observers ?? [])
|
||||||
const loadingPeers = ref(false)
|
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 peerReachable = computed(() => ({ ...appStore.peerHealth, ...peerReachableLocal.value }))
|
||||||
const discovering = ref(false)
|
const discovering = ref(false)
|
||||||
|
|
||||||
@ -351,7 +386,7 @@ const sendMessageError = ref('')
|
|||||||
const sendMessageSuccess = ref('')
|
const sendMessageSuccess = ref('')
|
||||||
|
|
||||||
// Connection requests
|
// Connection requests
|
||||||
const connectionRequests = ref<ConnectionRequest[]>([])
|
const connectionRequests = ref<ConnectionRequest[]>(cached.connectionRequests ?? [])
|
||||||
const loadingRequests = ref(false)
|
const loadingRequests = ref(false)
|
||||||
const processingRequestId = ref<string | null>(null)
|
const processingRequestId = ref<string | null>(null)
|
||||||
|
|
||||||
@ -388,6 +423,7 @@ function switchToRequestsTab() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadPeers() {
|
async function loadPeers() {
|
||||||
|
const hadPeers = peers.value.length > 0 || observers.value.length > 0
|
||||||
loadingPeers.value = true
|
loadingPeers.value = true
|
||||||
try {
|
try {
|
||||||
const res = await rpcClient.listPeers()
|
const res = await rpcClient.listPeers()
|
||||||
@ -427,8 +463,18 @@ async function loadPeers() {
|
|||||||
peerReachableLocal.value[p.onion] = false
|
peerReachableLocal.value[p.onion] = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
writeConnectedNodesCache({
|
||||||
|
peers: peers.value,
|
||||||
|
observers: observers.value,
|
||||||
|
peerReachable: peerReachableLocal.value,
|
||||||
|
connectionRequests: connectionRequests.value,
|
||||||
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (import.meta.env.DEV) console.error('Failed to load peers:', e)
|
if (import.meta.env.DEV) console.error('Failed to load peers:', e)
|
||||||
|
if (!hadPeers) {
|
||||||
|
peers.value = []
|
||||||
|
observers.value = []
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
loadingPeers.value = false
|
loadingPeers.value = false
|
||||||
}
|
}
|
||||||
@ -483,6 +529,12 @@ async function loadConnectionRequests() {
|
|||||||
const res = await rpcClient.call<{ requests: ConnectionRequest[] }>({ method: 'network.list-requests' })
|
const res = await rpcClient.call<{ requests: ConnectionRequest[] }>({ method: 'network.list-requests' })
|
||||||
connectionRequests.value = res.requests || []
|
connectionRequests.value = res.requests || []
|
||||||
web5Badge.pendingRequestCount = connectionRequests.value.length
|
web5Badge.pendingRequestCount = connectionRequests.value.length
|
||||||
|
writeConnectedNodesCache({
|
||||||
|
peers: peers.value,
|
||||||
|
observers: observers.value,
|
||||||
|
peerReachable: peerReachableLocal.value,
|
||||||
|
connectionRequests: connectionRequests.value,
|
||||||
|
})
|
||||||
} catch {
|
} catch {
|
||||||
if (!hadRequests) connectionRequests.value = []
|
if (!hadRequests) connectionRequests.value = []
|
||||||
} finally {
|
} finally {
|
||||||
@ -496,6 +548,12 @@ async function acceptRequest(requestId: string) {
|
|||||||
await rpcClient.call({ method: 'network.accept-request', params: { request_id: requestId } })
|
await rpcClient.call({ method: 'network.accept-request', params: { request_id: requestId } })
|
||||||
connectionRequests.value = connectionRequests.value.filter(r => r.id !== requestId)
|
connectionRequests.value = connectionRequests.value.filter(r => r.id !== requestId)
|
||||||
web5Badge.pendingRequestCount = connectionRequests.value.length
|
web5Badge.pendingRequestCount = connectionRequests.value.length
|
||||||
|
writeConnectedNodesCache({
|
||||||
|
peers: peers.value,
|
||||||
|
observers: observers.value,
|
||||||
|
peerReachable: peerReachableLocal.value,
|
||||||
|
connectionRequests: connectionRequests.value,
|
||||||
|
})
|
||||||
await loadPeers()
|
await loadPeers()
|
||||||
emit('toast', t('web5.connectionAccepted'))
|
emit('toast', t('web5.connectionAccepted'))
|
||||||
} catch {
|
} catch {
|
||||||
@ -511,6 +569,12 @@ async function rejectRequest(requestId: string) {
|
|||||||
await rpcClient.call({ method: 'network.reject-request', params: { request_id: requestId } })
|
await rpcClient.call({ method: 'network.reject-request', params: { request_id: requestId } })
|
||||||
connectionRequests.value = connectionRequests.value.filter(r => r.id !== requestId)
|
connectionRequests.value = connectionRequests.value.filter(r => r.id !== requestId)
|
||||||
web5Badge.pendingRequestCount = connectionRequests.value.length
|
web5Badge.pendingRequestCount = connectionRequests.value.length
|
||||||
|
writeConnectedNodesCache({
|
||||||
|
peers: peers.value,
|
||||||
|
observers: observers.value,
|
||||||
|
peerReachable: peerReachableLocal.value,
|
||||||
|
connectionRequests: connectionRequests.value,
|
||||||
|
})
|
||||||
emit('toast', t('web5.requestRejected'))
|
emit('toast', t('web5.requestRejected'))
|
||||||
} catch {
|
} catch {
|
||||||
emit('toast', t('web5.failedToRejectRequest'))
|
emit('toast', t('web5.failedToRejectRequest'))
|
||||||
|
|||||||
@ -389,6 +389,29 @@ import type { ManagedIdentity, IdentityProfile } from './types'
|
|||||||
|
|
||||||
const { t } = useI18n()
|
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<{
|
defineProps<{
|
||||||
showStagger: boolean
|
showStagger: boolean
|
||||||
}>()
|
}>()
|
||||||
@ -397,7 +420,7 @@ const emit = defineEmits<{
|
|||||||
toast: [text: string]
|
toast: [text: string]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const managedIdentities = ref<ManagedIdentity[]>([])
|
const managedIdentities = ref<ManagedIdentity[]>(readIdentitiesCache())
|
||||||
const identitiesLoading = ref(false)
|
const identitiesLoading = ref(false)
|
||||||
const showCreateIdentityModal = ref(false)
|
const showCreateIdentityModal = ref(false)
|
||||||
const newIdentityName = ref('Personal')
|
const newIdentityName = ref('Personal')
|
||||||
@ -508,6 +531,7 @@ async function loadIdentities() {
|
|||||||
try {
|
try {
|
||||||
const res = await rpcClient.call<{ identities: ManagedIdentity[] }>({ method: 'identity.list' })
|
const res = await rpcClient.call<{ identities: ManagedIdentity[] }>({ method: 'identity.list' })
|
||||||
managedIdentities.value = res.identities || []
|
managedIdentities.value = res.identities || []
|
||||||
|
writeIdentitiesCache(managedIdentities.value)
|
||||||
} catch {
|
} catch {
|
||||||
if (!hadIdentities) managedIdentities.value = []
|
if (!hadIdentities) managedIdentities.value = []
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@ -33,12 +33,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="userDid" class="flex gap-2 mt-auto">
|
<div v-if="userDid" class="flex gap-2 mt-auto">
|
||||||
<button
|
<button
|
||||||
@click="$emit('copyDid')"
|
@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"
|
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') }}
|
{{ didCopied ? t('common.copiedBang') : t('web5.copyDid') }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@click="$emit('showDidDocument')"
|
@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"
|
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>
|
</div>
|
||||||
<div v-if="dhtDid" class="flex gap-2 mt-auto">
|
<div v-if="dhtDid" class="flex gap-2 mt-auto">
|
||||||
<button
|
<button
|
||||||
@click="$emit('copyDhtDid')"
|
@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"
|
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' }}
|
{{ dhtDidCopied ? 'Copied!' : 'Copy' }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@click="$emit('refreshDhtDid')"
|
@click="$emit('refreshDhtDid')"
|
||||||
:disabled="publishingDht"
|
: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"
|
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' }}
|
{{ publishingDht ? 'Refreshing...' : 'Refresh DHT' }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
v-else-if="userDid"
|
v-else-if="userDid"
|
||||||
|
|||||||
@ -1,30 +1,30 @@
|
|||||||
{
|
{
|
||||||
"version": "1.7.85-alpha",
|
"version": "1.7.86-alpha",
|
||||||
"release_date": "2026-06-12",
|
"release_date": "2026-06-12",
|
||||||
"changelog": [
|
"changelog": [
|
||||||
"ElectrumX now runs with less cache pressure and more memory headroom, reducing the restart loop seen during sync catch-up.",
|
"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.",
|
||||||
"Portainer is pinned to `2.19.4` instead of `latest`, avoiding schema-drift restarts from surprise image updates.",
|
"Connected nodes and identities now reuse their last loaded data instead of reloading the visible list every time the user revisits the tab.",
|
||||||
"LND receive-address creation now asks for a native SegWit address and returns clearer wallet/readiness failures when an address is not available.",
|
"The Fleet matrix and detail views now show actual node names and host information instead of raw node id prefixes.",
|
||||||
"Fleet telemetry now carries server name, hostname, and server URL, and the Fleet dashboard shows those names instead of hashed node ids.",
|
"The network map only redraws when its graph data actually changes, which stops the D3 scene from visually resetting on every refresh tick.",
|
||||||
"Trusted federation peers are still auto-added transitively, but the local node no longer imports itself back into the fleet list.",
|
"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 locally for the touched frontend helpers, `git diff --check`, and Rust formatting."
|
"Validation passed with `git diff --check`, focused frontend tests, and `npm run type-check`."
|
||||||
],
|
],
|
||||||
"components": [
|
"components": [
|
||||||
{
|
{
|
||||||
"name": "archipelago",
|
"name": "archipelago",
|
||||||
"current_version": "1.7.85-alpha",
|
"current_version": "1.7.86-alpha",
|
||||||
"new_version": "1.7.85-alpha",
|
"new_version": "1.7.86-alpha",
|
||||||
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.85-alpha/archipelago",
|
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.86-alpha/archipelago",
|
||||||
"sha256": "06a6fe6e8f2e50bcda6c152c2de1a874edc84b2e65377f6e06d195c4eebc9cde",
|
"sha256": "2eb936fe188df25947a2d626af1b22be2f7ef9dcbc927542389de3b38e80f9e6",
|
||||||
"size_bytes": 44049488
|
"size_bytes": 44050232
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "archipelago-frontend-1.7.85-alpha.tar.gz",
|
"name": "archipelago-frontend-1.7.86-alpha.tar.gz",
|
||||||
"current_version": "1.7.85-alpha",
|
"current_version": "1.7.86-alpha",
|
||||||
"new_version": "1.7.85-alpha",
|
"new_version": "1.7.86-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",
|
"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": "c809fb27772773925d89b711236a81834465229d9f544bd65cf5816776cfda76",
|
"sha256": "9f6f146eaf709cd3e778550bb7a56877dce551cb6d631bccf08e80ed977dd17b",
|
||||||
"size_bytes": 184057997
|
"size_bytes": 184060614
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,30 +1,30 @@
|
|||||||
{
|
{
|
||||||
"version": "1.7.85-alpha",
|
"version": "1.7.86-alpha",
|
||||||
"release_date": "2026-06-12",
|
"release_date": "2026-06-12",
|
||||||
"changelog": [
|
"changelog": [
|
||||||
"ElectrumX now runs with less cache pressure and more memory headroom, reducing the restart loop seen during sync catch-up.",
|
"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.",
|
||||||
"Portainer is pinned to `2.19.4` instead of `latest`, avoiding schema-drift restarts from surprise image updates.",
|
"Connected nodes and identities now reuse their last loaded data instead of reloading the visible list every time the user revisits the tab.",
|
||||||
"LND receive-address creation now asks for a native SegWit address and returns clearer wallet/readiness failures when an address is not available.",
|
"The Fleet matrix and detail views now show actual node names and host information instead of raw node id prefixes.",
|
||||||
"Fleet telemetry now carries server name, hostname, and server URL, and the Fleet dashboard shows those names instead of hashed node ids.",
|
"The network map only redraws when its graph data actually changes, which stops the D3 scene from visually resetting on every refresh tick.",
|
||||||
"Trusted federation peers are still auto-added transitively, but the local node no longer imports itself back into the fleet list.",
|
"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 locally for the touched frontend helpers, `git diff --check`, and Rust formatting."
|
"Validation passed with `git diff --check`, focused frontend tests, and `npm run type-check`."
|
||||||
],
|
],
|
||||||
"components": [
|
"components": [
|
||||||
{
|
{
|
||||||
"name": "archipelago",
|
"name": "archipelago",
|
||||||
"current_version": "1.7.85-alpha",
|
"current_version": "1.7.86-alpha",
|
||||||
"new_version": "1.7.85-alpha",
|
"new_version": "1.7.86-alpha",
|
||||||
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.85-alpha/archipelago",
|
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.86-alpha/archipelago",
|
||||||
"sha256": "06a6fe6e8f2e50bcda6c152c2de1a874edc84b2e65377f6e06d195c4eebc9cde",
|
"sha256": "2eb936fe188df25947a2d626af1b22be2f7ef9dcbc927542389de3b38e80f9e6",
|
||||||
"size_bytes": 44049488
|
"size_bytes": 44050232
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "archipelago-frontend-1.7.85-alpha.tar.gz",
|
"name": "archipelago-frontend-1.7.86-alpha.tar.gz",
|
||||||
"current_version": "1.7.85-alpha",
|
"current_version": "1.7.86-alpha",
|
||||||
"new_version": "1.7.85-alpha",
|
"new_version": "1.7.86-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",
|
"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": "c809fb27772773925d89b711236a81834465229d9f544bd65cf5816776cfda76",
|
"sha256": "9f6f146eaf709cd3e778550bb7a56877dce551cb6d631bccf08e80ed977dd17b",
|
||||||
"size_bytes": 184057997
|
"size_bytes": 184060614
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user