Compare commits

..
28 changed files with 423 additions and 185 deletions
+21
View File
@@ -1,5 +1,26 @@
# Changelog
## v1.7.88-alpha (2026-06-12)
- AIUI now loads immediately again instead of waiting on a production availability probe and cache-busted iframe URL, restoring the lighter launch behavior from before the regression.
- Bitcoin receive now uses LND's GET-based newaddress flow with the native SegWit address type, fixing the `501 Method Not Allowed` response from the previous POST attempt.
- Validation pending on the AIUI rollback; the rest of the release train remains unchanged.
## v1.7.87-alpha (2026-06-12)
- Bitcoin receive now calls LND's on-chain address endpoint with the correct REST method, and backend failures keep the specific address-generation error instead of collapsing into the generic operation-failed message.
- App launch credential interstitials now render as true full-screen overlays, and the launcher loading indicator uses the neutral brand palette instead of a blue spinner.
- Validation passed with `git diff --check`, `npm run type-check`, and the focused frontend tests for `bitcoinReceive` and `AppIconGrid`.
## 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.
+1
View File
@@ -57,6 +57,7 @@ app:
interval: 30s
timeout: 5s
retries: 3
start_period: 10m
bitcoin_integration:
rpc_access: read-only
+1 -1
View File
@@ -80,7 +80,7 @@ checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
[[package]]
name = "archipelago"
version = "1.7.85-alpha"
version = "1.7.88-alpha"
dependencies = [
"anyhow",
"archipelago-container",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "archipelago"
version = "1.7.85-alpha"
version = "1.7.88-alpha"
edition = "2021"
description = "Archipelago Bitcoin Node OS - Native backend"
authors = ["Archipelago Team"]
+12 -7
View File
@@ -13,22 +13,27 @@ impl RpcHandler {
let resp = client
.get(format!("{LND_REST_BASE_URL}/v1/newaddress"))
.query(&[("type", "WITNESS_PUBKEY_HASH")])
.query(&[("type", "p2wkh")])
.header("Grpc-Metadata-macaroon", &macaroon_hex)
.send()
.await
.context("LND REST connection failed")?;
let status = resp.status();
let body: serde_json::Value = resp
.json()
let raw_body = resp
.text()
.await
.context("Failed to parse newaddress response")?;
.context("LND address response could not be read")?;
let body: serde_json::Value = serde_json::from_str(&raw_body).unwrap_or_else(|_| {
serde_json::json!({
"raw": raw_body,
})
});
if !status.is_success() {
let message = lnd_error_message(&body);
anyhow::bail!(
"LND could not generate a Bitcoin address ({}): {}",
"Bitcoin address generation failed ({}): {}",
status,
message
);
@@ -39,14 +44,14 @@ impl RpcHandler {
.or_else(|| body.get("message"))
.and_then(|v| v.as_str())
{
anyhow::bail!("LND could not generate a Bitcoin address: {}", error);
anyhow::bail!("Bitcoin address generation failed: {}", error);
}
let address = body
.get("address")
.and_then(|v| v.as_str())
.filter(|addr| !addr.trim().is_empty())
.ok_or_else(|| anyhow::anyhow!("LND did not return a Bitcoin address. The wallet may still be locked, uninitialized, or waiting for Bitcoin to sync."))?
.ok_or_else(|| anyhow::anyhow!("Bitcoin address generation failed: LND did not return a Bitcoin address. The wallet may still be locked, uninitialized, or waiting for Bitcoin to sync."))?
.to_string();
Ok(serde_json::json!({ "address": address }))
@@ -63,6 +63,7 @@ pub(super) fn sanitize_error_message(msg: &str) -> String {
"Failed to start",
"Container",
"Image",
"Bitcoin address",
];
for prefix in &user_facing_prefixes {
if msg.starts_with(prefix) {
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "neode-ui",
"version": "1.7.85-alpha",
"version": "1.7.88-alpha",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "neode-ui",
"version": "1.7.85-alpha",
"version": "1.7.88-alpha",
"dependencies": {
"@types/dompurify": "^3.0.5",
"@vue-leaflet/vue-leaflet": "^0.10.1",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "neode-ui",
"private": true,
"version": "1.7.85-alpha",
"version": "1.7.88-alpha",
"type": "module",
"scripts": {
"start": "./start-dev.sh",
@@ -3,16 +3,17 @@
<Transition name="app-launcher">
<div
v-if="store.isOpen"
class="fixed inset-0 z-[2400] flex items-center justify-center p-0 md:p-10"
class="fixed inset-0 z-[2400] flex items-stretch justify-stretch p-0"
@click.self="store.close()"
>
<!-- Backdrop - blur like spotlight -->
<div class="app-launcher-backdrop absolute inset-0 bg-black/60 backdrop-blur-md"></div>
<div class="app-launcher-backdrop absolute inset-0 bg-black/75 backdrop-blur-md"></div>
<!-- Panel - inset with margins, glass style like spotlight -->
<!-- Panel - full-screen overlay -->
<div
class="app-launcher-panel relative z-10 flex flex-col overflow-hidden rounded-none md:rounded-2xl shadow-2xl"
class="app-launcher-panel relative z-10 flex flex-col overflow-hidden rounded-none shadow-2xl"
:class="panelClasses"
style="border-radius: 0;"
>
<!-- Header bar - sticky on mobile -->
<div class="sticky top-0 z-10 flex items-center gap-3 border-b border-white/10 px-4 py-3 bg-black/60 backdrop-blur-md md:bg-transparent md:backdrop-blur-none">
@@ -69,7 +70,7 @@
<!-- Loading indicator -->
<Transition name="content-fade">
<div v-if="iframeLoading" class="absolute inset-0 z-10 flex items-center justify-center bg-black/40">
<svg class="animate-spin h-8 w-8 text-blue-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<svg class="animate-spin h-8 w-8 text-white/70" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
@@ -398,7 +399,7 @@ function injectScrollbarHideIfSameOrigin() {
const panelClasses = [
'glass-card',
'w-full h-full',
'md:max-w-[calc(100vw-5rem)] md:max-h-[calc(100vh-5rem)]',
'max-w-none max-h-none',
]
function onKeyDown(e: KeyboardEvent) {
@@ -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>
+2 -2
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;
}
}
@@ -17,4 +17,8 @@ describe('explainReceiveAddressFailure', () => {
it('explains lnd transport failures', () => {
expect(explainReceiveAddressFailure(new Error('LND REST connection failed'))).toContain('not responding cleanly')
})
it('keeps bitcoin address generation failures visible', () => {
expect(explainReceiveAddressFailure(new Error('Bitcoin address generation failed: LND is not ready'))).toContain('Bitcoin address generation failed')
})
})
+3 -3
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" />
+17 -15
View File
@@ -244,10 +244,10 @@
<Transition name="fade">
<div
v-if="credentialModal.show"
class="credential-modal-overlay fixed inset-0 z-[2700] flex items-center justify-center bg-black/60 backdrop-blur-md p-4 md:p-6"
class="credential-modal-overlay fixed inset-0 z-[2700] flex items-stretch justify-stretch bg-black/80 backdrop-blur-md p-0"
@click.self="closeCredentialModal"
>
<div class="sideload-modal credential-modal">
<div class="credential-modal-panel">
<div class="flex items-start justify-between gap-4 mb-5">
<div>
<h2 class="text-lg font-semibold text-white">{{ credentialModal.title }}</h2>
@@ -259,7 +259,7 @@
<div v-for="cred in credentialModal.credentials" :key="cred.label" class="rounded-lg border border-white/10 bg-white/[0.04] p-3">
<div class="flex items-center justify-between gap-3 mb-1">
<span class="text-white/60 text-xs uppercase tracking-wide">{{ cred.label }}</span>
<button type="button" class="text-xs text-blue-300 hover:text-blue-200" @click="copyModalCredential(cred.label, cred.value)">{{ credentialModal.copied === cred.label ? 'Copied' : 'Copy' }}</button>
<button type="button" class="text-xs text-orange-300 hover:text-orange-200" @click="copyModalCredential(cred.label, cred.value)">{{ credentialModal.copied === cred.label ? 'Copied' : 'Copy' }}</button>
</div>
<p class="font-mono text-sm text-white break-all">{{ cred.value }}</p>
</div>
@@ -802,15 +802,24 @@ async function submitSideload() {
}
.sideload-input::placeholder { color: rgba(255, 255, 255, 0.38); }
.sideload-input:focus { border-color: rgba(255, 255, 255, 0.38); }
.credential-modal {
.credential-modal-panel {
display: flex;
flex-direction: column;
max-height: calc(100dvh - var(--safe-area-top, env(safe-area-inset-top, 0px)) - var(--safe-area-bottom, env(safe-area-inset-bottom, 0px)) - 2rem);
border-radius: 1.25rem;
padding-bottom: 1.25rem;
box-shadow: 0 25px 80px rgba(0, 0, 0, 0.55);
width: 100%;
height: 100%;
min-height: 0;
max-width: none;
max-height: none;
overflow: hidden;
border: 0;
border-radius: 0;
background: rgba(8, 10, 18, 0.98);
padding: 1.25rem;
padding-bottom: calc(1.25rem + var(--safe-area-bottom, env(safe-area-inset-bottom, 0px)));
box-shadow: none;
}
.credential-modal-body {
flex: 1 1 auto;
min-height: 0;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
@@ -818,11 +827,4 @@ async function submitSideload() {
.credential-modal-actions {
flex-shrink: 0;
}
@media (min-width: 768px) {
.sideload-modal {
border-radius: 1.25rem;
padding: 1.5rem;
box-shadow: 0 25px 80px rgba(0, 0, 0, 0.55);
}
}
</style>
+27 -38
View File
@@ -1,18 +1,30 @@
<template>
<div class="chat-fullscreen">
<!-- Close button (desktop: top-right pill) -->
<div class="chat-mode-pill hidden md:flex">
<!-- Close button + connection indicator (desktop: top-right pill) -->
<div class="chat-mode-pill flex">
<button class="chat-close-btn" :aria-label="t('chat.closeAssistant')" @click="closeChat">
<svg class="w-4 h-4" aria-hidden="true" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
<span class="text-xs font-medium">{{ t('chat.close') }}</span>
</button>
</div>
<div
v-if="aiuiConnected"
class="w-2 h-2 rounded-full bg-green-400 ml-2 shadow-[0_0_6px_rgba(74,222,128,0.5)]"
:title="t('chat.aiuiConnected')"
/>
</div>
<!-- Loading indicator while checking availability -->
<!-- Mobile back button -->
<button class="chat-mobile-back md:hidden" :aria-label="t('common.goBack')" @click="closeChat">
<svg class="w-5 h-5" aria-hidden="true" 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" />
</svg>
</button>
<!-- Loading indicator while iframe loads -->
<Transition name="fade">
<div v-if="aiuiAvailable === null" class="chat-loading" role="status" aria-live="polite">
<div v-if="aiuiUrl && !aiuiConnected" class="chat-loading" role="status" aria-live="polite">
<div class="glass-card p-8 flex flex-col items-center gap-4">
<div class="chat-loading-spinner" aria-hidden="true" />
<p class="text-sm text-white/60">{{ t('chat.loadingAssistant') }}</p>
@@ -27,24 +39,23 @@
:src="aiuiUrl"
:title="t('chat.aiAssistant')"
class="chat-iframe chat-iframe-mobile"
allow="microphone"
style="background: transparent"
/>
<!-- Fallback when AIUI is not deployed -->
<div v-else-if="aiuiAvailable === false" class="chat-placeholder">
<div class="chat-placeholder-inner glass-card p-8">
<!-- Fallback when no AIUI URL configured -->
<div v-else class="chat-placeholder">
<div class="chat-placeholder-inner">
<div class="chat-placeholder-icon">
<svg class="w-8 h-8 text-white/40" aria-hidden="true" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
</svg>
</div>
<h2 class="text-2xl font-semibold text-white mb-2">{{ t('chat.aiAssistant') }}</h2>
<p class="text-white/80 mb-4 leading-relaxed">
<p class="text-white/60 mb-4 leading-relaxed">
{{ t('chat.notConfigured') }}
</p>
<p class="text-sm text-white/50">
<p class="text-xs text-white/30">
{{ t('chat.deployCta') }}
</p>
</div>
@@ -63,37 +74,16 @@ const { t } = useI18n()
const router = useRouter()
const aiuiFrame = ref<HTMLIFrameElement | null>(null)
const aiuiConnected = ref(false)
let broker: ContextBroker | null = null
const aiuiAvailable = ref<boolean | null>(null) // null = checking, true/false = result
const aiuiUrl = computed(() => {
const envUrl = import.meta.env.VITE_AIUI_URL
if (envUrl) return `${envUrl}?embedded=true&hideClose=true`
// In production, only return the URL if we've confirmed AIUI files exist
if (import.meta.env.PROD && aiuiAvailable.value === true) return `/aiui/?embedded=true&hideClose=true&v=${Date.now()}`
if (envUrl) return `${envUrl}?embedded=true`
if (import.meta.env.PROD) return '/aiui/?embedded=true'
return ''
})
/** Check if AIUI is actually deployed by fetching its index.html */
async function checkAiuiAvailable() {
if (import.meta.env.VITE_AIUI_URL) {
aiuiAvailable.value = true
return
}
if (!import.meta.env.PROD) {
aiuiAvailable.value = false
return
}
try {
const res = await fetch('/aiui/', { method: 'HEAD' })
// If we get HTML back (200), AIUI is deployed. If 404/403, it's not.
aiuiAvailable.value = res.ok
} catch {
aiuiAvailable.value = false
}
}
function closeChat() {
if (window.history.length > 1) {
router.back()
@@ -111,13 +101,12 @@ function onAiuiMessage(event: MessageEvent) {
} catch { return }
// Listen for ready messages from AIUI iframe
if (event.data?.type === 'ready') {
// AIUI connected - could use for future features
aiuiConnected.value = true
}
}
onMounted(async () => {
onMounted(() => {
window.addEventListener('message', onAiuiMessage)
await checkAiuiAvailable()
if (aiuiUrl.value) {
broker = new ContextBroker(aiuiFrame, aiuiUrl.value)
broker.start()
+56 -14
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(() => {
+19 -15
View File
@@ -85,8 +85,8 @@
</div>
<Transition name="fade">
<div v-if="credentialModal.show" class="credential-modal-overlay fixed inset-0 z-[2700] flex items-center justify-center bg-black/60 backdrop-blur-md p-4 md:p-6" @click.self="closeCredentialModal">
<div class="sideload-modal credential-modal">
<div v-if="credentialModal.show" class="credential-modal-overlay fixed inset-0 z-[2700] flex items-stretch justify-stretch bg-black/80 backdrop-blur-md p-0" @click.self="closeCredentialModal">
<div class="credential-modal-panel">
<div class="flex items-start justify-between gap-4 mb-5">
<div>
<h2 class="text-lg font-semibold text-white">{{ credentialModal.title }}</h2>
@@ -98,7 +98,7 @@
<div v-for="cred in credentialModal.credentials" :key="cred.label" class="rounded-lg border border-white/10 bg-white/[0.04] p-3">
<div class="flex items-center justify-between gap-3 mb-1">
<span class="text-white/60 text-xs uppercase tracking-wide">{{ cred.label }}</span>
<button type="button" class="text-xs text-blue-300 hover:text-blue-200" @click="copyModalCredential(cred.label, cred.value)">{{ credentialModal.copied === cred.label ? 'Copied' : 'Copy' }}</button>
<button type="button" class="text-xs text-orange-300 hover:text-orange-200" @click="copyModalCredential(cred.label, cred.value)">{{ credentialModal.copied === cred.label ? 'Copied' : 'Copy' }}</button>
</div>
<p class="font-mono text-sm text-white break-all">{{ cred.value }}</p>
</div>
@@ -328,24 +328,28 @@ function scrollToPage(index: number) {
background: rgba(255, 255, 255, 0.06);
}
.credential-modal-body {
flex: 1 1 auto;
min-height: 0;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
.credential-modal {
max-height: calc(100dvh - var(--safe-area-top, env(safe-area-inset-top, 0px)) - var(--safe-area-bottom, env(safe-area-inset-bottom, 0px)) - 2rem);
border-radius: 1.25rem;
padding-bottom: 1.25rem;
box-shadow: 0 25px 80px rgba(0, 0, 0, 0.55);
.credential-modal-panel {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
min-height: 0;
max-width: none;
max-height: none;
overflow: hidden;
border: 0;
border-radius: 0;
background: rgba(8, 10, 18, 0.98);
padding: 1.25rem;
padding-bottom: calc(1.25rem + var(--safe-area-bottom, env(safe-area-inset-bottom, 0px)));
box-shadow: none;
}
.credential-modal-actions {
flex-shrink: 0;
}
@media (min-width: 768px) {
.sideload-modal {
border-radius: 1.25rem;
padding: 1.5rem;
box-shadow: 0 25px 80px rgba(0, 0, 0, 0.55);
}
}
</style>
@@ -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' }}
@@ -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[]
+1 -1
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>
+82 -6
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 ---
@@ -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"
@@ -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>
+68 -4
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'))
+25 -1
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 {
+19 -19
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"
+15 -18
View File
@@ -1,30 +1,27 @@
{
"version": "1.7.85-alpha",
"version": "1.7.88-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."
"AIUI now loads immediately again instead of waiting on a production availability probe and cache-busted iframe URL, restoring the lighter launch behavior from before the regression.",
"Bitcoin receive now uses LND's GET-based newaddress flow with the native SegWit address type, fixing the `501 Method Not Allowed` response from the previous POST attempt.",
"Validation pending on the AIUI rollback; the rest of the release train remains unchanged."
],
"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.88-alpha",
"new_version": "1.7.88-alpha",
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.88-alpha/archipelago",
"sha256": "5948655918c76f7a3e53e2c61c3e5662b4035eb4692545546b17d7f2d4d782c3",
"size_bytes": 44057520
},
{
"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.88-alpha.tar.gz",
"current_version": "1.7.88-alpha",
"new_version": "1.7.88-alpha",
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.88-alpha/archipelago-frontend-1.7.88-alpha.tar.gz",
"sha256": "158be23c11d3a5d007e2a99d70eaafce2f287c5d3c240c4ab37945245f006a13",
"size_bytes": 184056182
}
]
}
+15 -18
View File
@@ -1,30 +1,27 @@
{
"version": "1.7.85-alpha",
"version": "1.7.88-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."
"AIUI now loads immediately again instead of waiting on a production availability probe and cache-busted iframe URL, restoring the lighter launch behavior from before the regression.",
"Bitcoin receive now uses LND's GET-based newaddress flow with the native SegWit address type, fixing the `501 Method Not Allowed` response from the previous POST attempt.",
"Validation pending on the AIUI rollback; the rest of the release train remains unchanged."
],
"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.88-alpha",
"new_version": "1.7.88-alpha",
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.88-alpha/archipelago",
"sha256": "5948655918c76f7a3e53e2c61c3e5662b4035eb4692545546b17d7f2d4d782c3",
"size_bytes": 44057520
},
{
"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.88-alpha.tar.gz",
"current_version": "1.7.88-alpha",
"new_version": "1.7.88-alpha",
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.88-alpha/archipelago-frontend-1.7.88-alpha.tar.gz",
"sha256": "158be23c11d3a5d007e2a99d70eaafce2f287c5d3c240c4ab37945245f006a13",
"size_bytes": 184056182
}
]
}