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 # 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) ## 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. - 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 (client, macaroon_hex) = self.lnd_client().await?;
let resp = client let resp = client
.get(format!("{LND_REST_BASE_URL}/v1/newaddress")) .post(format!("{LND_REST_BASE_URL}/v1/newaddress"))
.query(&[("type", "WITNESS_PUBKEY_HASH")])
.header("Grpc-Metadata-macaroon", &macaroon_hex) .header("Grpc-Metadata-macaroon", &macaroon_hex)
.json(&serde_json::json!({ "type": "WITNESS_PUBKEY_HASH" }))
.send() .send()
.await .await
.context("LND REST connection failed")?; .context("LND REST connection failed")?;
let status = resp.status(); let status = resp.status();
let body: serde_json::Value = resp let raw_body = resp
.json() .text()
.await .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() { if !status.is_success() {
let message = lnd_error_message(&body); let message = lnd_error_message(&body);
anyhow::bail!( anyhow::bail!(
"LND could not generate a Bitcoin address ({}): {}", "Bitcoin address generation failed ({}): {}",
status, status,
message message
); );
@ -39,14 +44,14 @@ impl RpcHandler {
.or_else(|| body.get("message")) .or_else(|| body.get("message"))
.and_then(|v| v.as_str()) .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 let address = body
.get("address") .get("address")
.and_then(|v| v.as_str()) .and_then(|v| v.as_str())
.filter(|addr| !addr.trim().is_empty()) .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(); .to_string();
Ok(serde_json::json!({ "address": address })) Ok(serde_json::json!({ "address": address }))

View File

@ -63,6 +63,7 @@ pub(super) fn sanitize_error_message(msg: &str) -> String {
"Failed to start", "Failed to start",
"Container", "Container",
"Image", "Image",
"Bitcoin address",
]; ];
for prefix in &user_facing_prefixes { for prefix in &user_facing_prefixes {
if msg.starts_with(prefix) { if msg.starts_with(prefix) {

View File

@ -3,16 +3,17 @@
<Transition name="app-launcher"> <Transition name="app-launcher">
<div <div
v-if="store.isOpen" 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()" @click.self="store.close()"
> >
<!-- Backdrop - blur like spotlight --> <!-- 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 <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" :class="panelClasses"
style="border-radius: 0;"
> >
<!-- Header bar - sticky on mobile --> <!-- 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"> <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 --> <!-- Loading indicator -->
<Transition name="content-fade"> <Transition name="content-fade">
<div v-if="iframeLoading" class="absolute inset-0 z-10 flex items-center justify-center bg-black/40"> <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> <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> <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> </svg>
@ -398,7 +399,7 @@ function injectScrollbarHideIfSameOrigin() {
const panelClasses = [ const panelClasses = [
'glass-card', 'glass-card',
'w-full h-full', '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) { function onKeyDown(e: KeyboardEvent) {

View File

@ -17,4 +17,8 @@ describe('explainReceiveAddressFailure', () => {
it('explains lnd transport failures', () => { it('explains lnd transport failures', () => {
expect(explainReceiveAddressFailure(new Error('LND REST connection failed'))).toContain('not responding cleanly') 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"> <Transition name="fade">
<div <div
v-if="credentialModal.show" 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" @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 class="flex items-start justify-between gap-4 mb-5">
<div> <div>
<h2 class="text-lg font-semibold text-white">{{ credentialModal.title }}</h2> <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 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"> <div class="flex items-center justify-between gap-3 mb-1">
<span class="text-white/60 text-xs uppercase tracking-wide">{{ cred.label }}</span> <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> </div>
<p class="font-mono text-sm text-white break-all">{{ cred.value }}</p> <p class="font-mono text-sm text-white break-all">{{ cred.value }}</p>
</div> </div>
@ -802,15 +802,24 @@ async function submitSideload() {
} }
.sideload-input::placeholder { color: rgba(255, 255, 255, 0.38); } .sideload-input::placeholder { color: rgba(255, 255, 255, 0.38); }
.sideload-input:focus { border-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; display: flex;
flex-direction: column; 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); width: 100%;
border-radius: 1.25rem; height: 100%;
padding-bottom: 1.25rem; min-height: 0;
box-shadow: 0 25px 80px rgba(0, 0, 0, 0.55); 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 { .credential-modal-body {
flex: 1 1 auto;
min-height: 0; min-height: 0;
overflow-y: auto; overflow-y: auto;
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;
@ -818,11 +827,4 @@ async function submitSideload() {
.credential-modal-actions { .credential-modal-actions {
flex-shrink: 0; 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> </style>

View File

@ -85,8 +85,8 @@
</div> </div>
<Transition name="fade"> <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 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="sideload-modal credential-modal"> <div class="credential-modal-panel">
<div class="flex items-start justify-between gap-4 mb-5"> <div class="flex items-start justify-between gap-4 mb-5">
<div> <div>
<h2 class="text-lg font-semibold text-white">{{ credentialModal.title }}</h2> <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 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"> <div class="flex items-center justify-between gap-3 mb-1">
<span class="text-white/60 text-xs uppercase tracking-wide">{{ cred.label }}</span> <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> </div>
<p class="font-mono text-sm text-white break-all">{{ cred.value }}</p> <p class="font-mono text-sm text-white break-all">{{ cred.value }}</p>
</div> </div>
@ -328,24 +328,28 @@ function scrollToPage(index: number) {
background: rgba(255, 255, 255, 0.06); background: rgba(255, 255, 255, 0.06);
} }
.credential-modal-body { .credential-modal-body {
flex: 1 1 auto;
min-height: 0; min-height: 0;
overflow-y: auto; overflow-y: auto;
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;
} }
.credential-modal { .credential-modal-panel {
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); display: flex;
border-radius: 1.25rem; flex-direction: column;
padding-bottom: 1.25rem; width: 100%;
box-shadow: 0 25px 80px rgba(0, 0, 0, 0.55); 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 { .credential-modal-actions {
flex-shrink: 0; 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> </style>