fix: patch bitcoin receive and full-screen launch overlays

This commit is contained in:
archipelago 2026-06-12 04:42:23 -04:00
parent b11c6c17d1
commit 8d4b309753
7 changed files with 67 additions and 44 deletions

View File

@ -1,5 +1,11 @@
# Changelog
## 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.

View File

@ -12,23 +12,28 @@ impl RpcHandler {
let (client, macaroon_hex) = self.lnd_client().await?;
let resp = client
.get(format!("{LND_REST_BASE_URL}/v1/newaddress"))
.query(&[("type", "WITNESS_PUBKEY_HASH")])
.post(format!("{LND_REST_BASE_URL}/v1/newaddress"))
.header("Grpc-Metadata-macaroon", &macaroon_hex)
.json(&serde_json::json!({ "type": "WITNESS_PUBKEY_HASH" }))
.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 }))

View File

@ -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) {

View File

@ -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) {

View File

@ -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')
})
})

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>

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>