From 51a4b59ce82cadc25166c700719c37bc14b0b23b Mon Sep 17 00:00:00 2001 From: archipelago Date: Sat, 8 Aug 2026 13:11:18 -0400 Subject: [PATCH] fix(settings): card the Lightning section, and stop it crying "not installed" mid-rotation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two operator reports on the same screen. The section had no card. Every other Settings section wraps itself in `glass-card px-6 py-6 mb-6` — AccountSection, AIDataAccessSection, NodeCertificateSection, BackupSection, the lot — and this one rendered as bare text on the page. Reported twice, because the wrapper lives in the new component and nothing about adding `` to SystemSection.vue's list tells you it is missing. Heading moved to h2/text-xl to match its siblings. A test now asserts the card, so a third report is not needed. And rotating told the operator Lightning did not exist. Rotation restarts LND, so `status.installed` reads false for a moment — and the template read that literally: "Lightning is not set up on this node yet, so there are no credentials to rotate. Install the Lightning app first." Seconds after rotating. On a node with a working wallet. It also replaced the progress they had every reason to be watching, on the one action that invalidates every credential their wallet holds. A container briefly absent is what rotating LOOKS like, not evidence Lightning was never there. The not-installed message is now gated on `!rotationInFlight`, which covers both `running: true` and the awaitUntil window between asking for a rotation and the node reporting one — `installed` can already be false in that gap, so gating on `running` alone would have left the same hole. Mid-rotation with no status yet says "Rotating credentials — Lightning is restarting" instead of falling through to a details block with empty fields. awaitUntil became a ref so the computed re-evaluates rather than holding a stale value until some other reactive dependency happens to change. Three tests: the card exists; a running rotation does not claim Lightning is missing; and — the half that matters just as much — a node with genuinely no Lightning still gets told there is nothing to rotate, so the fix has not simply hidden a true statement. 16/16, vue-tsc clean. Co-Authored-By: Claude Opus 5 (1M context) --- .../settings/LightningCredentialsSection.vue | 55 ++++++++++++++--- .../LightningCredentialsSection.test.ts | 61 +++++++++++++++++++ 2 files changed, 109 insertions(+), 7 deletions(-) diff --git a/neode-ui/src/views/settings/LightningCredentialsSection.vue b/neode-ui/src/views/settings/LightningCredentialsSection.vue index e2dbc320..87267742 100644 --- a/neode-ui/src/views/settings/LightningCredentialsSection.vue +++ b/neode-ui/src/views/settings/LightningCredentialsSection.vue @@ -36,12 +36,28 @@ let poll: ReturnType | null = null /// /// Bounded rather than a plain flag, so a request the node accepted but never /// acted on stops polling instead of hammering it forever. -let awaitUntil = 0 +const awaitUntil = ref(0) const AWAIT_START_MS = 120_000 const rotation = computed(() => status.value?.rotation ?? null) const isRunning = computed(() => rotation.value?.running === true) +/// Ticks while a rotation is being awaited, so `rotationInFlight` re-evaluates +/// as the await window expires instead of holding a stale value until the next +/// poll happens to touch a reactive dependency. +const now = ref(Date.now()) + +/// Is a rotation happening, INCLUDING the gap between asking for one and the +/// node reporting it? +/// +/// Rotation restarts LND, so `status.installed` goes false for a moment +/// mid-rotation. Read literally that says "Lightning is not set up on this +/// node" — which the screen then told the operator, seconds after they +/// rotated, on a node with a working Lightning wallet. The container being +/// briefly absent is what rotating LOOKS like, not evidence it was never +/// there. +const rotationInFlight = computed(() => isRunning.value || now.value < awaitUntil.value) + /** A finished rotation, successful or not. `ok` is null while running. */ const finished = computed( () => rotation.value !== null && !rotation.value.running && rotation.value.ok !== null, @@ -68,8 +84,9 @@ async function load() { /// waking the node every few seconds. function syncPolling() { const running = status.value?.rotation.running === true - if (running) awaitUntil = 0 - if (running || Date.now() < awaitUntil) startPolling() + if (running) awaitUntil.value = 0 + now.value = Date.now() + if (running || Date.now() < awaitUntil.value) startPolling() else stopPolling() } @@ -103,7 +120,8 @@ async function rotate() { try { await rpcClient.lndRotateMacaroons(password.value) closeConfirm() - awaitUntil = Date.now() + AWAIT_START_MS + awaitUntil.value = Date.now() + AWAIT_START_MS + now.value = Date.now() startPolling() await load() } catch (e) { @@ -155,8 +173,16 @@ onUnmounted(stopPolling)