Compare commits

...
14 changed files with 134 additions and 122 deletions
+12
View File
@@ -1,5 +1,17 @@
# 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.
+1 -1
View File
@@ -80,7 +80,7 @@ checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
[[package]]
name = "archipelago"
version = "1.7.86-alpha"
version = "1.7.88-alpha"
dependencies = [
"anyhow",
"archipelago-container",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "archipelago"
version = "1.7.86-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.86-alpha",
"version": "1.7.88-alpha",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "neode-ui",
"version": "1.7.86-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.86-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) {
@@ -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')
})
})
+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()
+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>
+15 -18
View File
@@ -1,30 +1,27 @@
{
"version": "1.7.86-alpha",
"version": "1.7.88-alpha",
"release_date": "2026-06-12",
"changelog": [
"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`."
"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.86-alpha",
"new_version": "1.7.86-alpha",
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.86-alpha/archipelago",
"sha256": "2eb936fe188df25947a2d626af1b22be2f7ef9dcbc927542389de3b38e80f9e6",
"size_bytes": 44050232
"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.86-alpha.tar.gz",
"current_version": "1.7.86-alpha",
"new_version": "1.7.86-alpha",
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.86-alpha/archipelago-frontend-1.7.86-alpha.tar.gz",
"sha256": "9f6f146eaf709cd3e778550bb7a56877dce551cb6d631bccf08e80ed977dd17b",
"size_bytes": 184060614
"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.86-alpha",
"version": "1.7.88-alpha",
"release_date": "2026-06-12",
"changelog": [
"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`."
"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.86-alpha",
"new_version": "1.7.86-alpha",
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.86-alpha/archipelago",
"sha256": "2eb936fe188df25947a2d626af1b22be2f7ef9dcbc927542389de3b38e80f9e6",
"size_bytes": 44050232
"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.86-alpha.tar.gz",
"current_version": "1.7.86-alpha",
"new_version": "1.7.86-alpha",
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.86-alpha/archipelago-frontend-1.7.86-alpha.tar.gz",
"sha256": "9f6f146eaf709cd3e778550bb7a56877dce551cb6d631bccf08e80ed977dd17b",
"size_bytes": 184060614
"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
}
]
}