v1.8.9's move to Router.SendPaymentV2 shipped without fee_limit_sat,
and the v2 route treats an ABSENT fee limit as zero allowed fees.
Every real route carries a routing fee (the 2-hop route here: 1.5
sats), so the pathfinder rejected them all and the wallet answered
"No route to the recipient" on EVERY send — all day, on healthy
channels with plenty of liquidity both ways.
The router debug log makes it unambiguous:
wallet payment (v1.8.9 backend): fee_limit=0 mSAT -> no route
same payment by hand (lncli --fee_limit=100): fee_limit=100000 mSAT -> settles in 0.65s
My earlier "pipeline verified" claim was wrong — the manual lncli
verification set a fee limit by hand and masked this exact bug. The
400k that succeeded this morning went through the pre-update backend
on the pre-update LND.
Payments now carry lncli's own default budget — the payment amount
(100%), preferring the payer-supplied amount for zero-value invoices
and the invoice's own amount otherwise, with a nominal floor so the
limit can never be zero. Unit-pinned so it cannot regress.
LND normally reconnects channel peers after a restart, but not reliably:
after long or repeated downtime (an app update, a node reboot,
reconciler churn) the peer link can stay down for hours while BOTH
endpoints keep the channel flagged disabled in the routing graph. The
node looks perfectly healthy, the wallet shows balance, and every
payment in either direction fails "no route to the recipient" —
observed live on framework-pt (2026-09-01): its only channel sat
disabled on both policy sides for ~17 hours after the LND 0.21.2
update, while shorty had 583k spendable and the user was told, by a
mis-mapped modal, that they had 'no payment channel'.
The channel graph is desired state — every open channel should have a
live peer connection. A daemon-side watchdog now enforces it:
- every 2 minutes, list channels + peers over LND REST
- for each channel whose remote peer is not connected, look the peer's
advertised addresses up in the public graph and dial one
- per-peer retries throttled to 10 minutes so an unreachable peer is
not hammered; 'already connected' counts as done; a peer with no
advertised address is logged once per pass (cannot be dialed)
- no-ops quietly on nodes without LND (missing macaroon) and while a
wallet is locked (503 body has no channels)
Unit tests pin the selection against the live REST shapes
(remote_pubkey in /v1/channels vs pub_key in /v1/peers).
v1.8.10 CHANGELOG + What's New entries staged so the next release run
is clean first time.
"LND thinks I do not have a channel" while the wallet showed plenty of
liquidity (framework-pt, 2026-09-01): the send gate sums outbound over
FULLY-OPEN channels only, which is correct — a just-opened channel
sits in LND's pending list until it has ~3 confirmations, and an
open channel can have all its balance on the far side — but the modal
then claimed the node had NO channel at all, in every one of those
states, and pointed the user at opening another one.
The gate already fetched the full channel list; it now records WHY
liquidity is zero and the modal says the truth per state:
- pending channels -> "your new channel is waiting for on-chain
confirmations, it unlocks automatically, nothing is needed from you"
(and no "Open a channel" button — that would send the user to fix
a problem they don't have, possibly opening a second channel)
- open channels, zero on the needed side -> "balance is on the far
side — you can receive but there's nothing to send right now"
- payment refused with a routing/liquidity error -> says so, instead
of claiming no channels
- only a genuinely channel-less node keeps the open-one guidance
Eleven unit tests pin the state machine, including the regression
case (pending-only -> 'pending', not 'none') and fail-open on RPC
errors.
Root-cause table, fix inventory, regression-test inventory and the
deploy/live-verification checklist for today's fleet incident — written
as the working record while the fixes land, so the deploy + verify steps
can be checked off against real nodes rather than memory. CHANGELOG
carries the user-facing notes for the release these fixes ship in.
Settings gains the SSH-over-mesh card (danger-zone confirmation for the
any-peer scope, sshd preflights, fipssh copy hint). The signed-catalog
merge filters components via the shared serviceNames module; Discover
grids get the standard icon container; install no longer yanks the user
to My Apps; v1.8.8 release notes.
The RELEASE_NOTES_BACKLOG gate for cutting the next release, closed out:
- Eight sections backfilled to the curated standard, from the Settings
What's New blocks, the old-lineage release commits, and the hotfix
diffs: v1.7.44 (was four raw commit-hash lines), v1.7.47/48/64/65
(thin), and v1.7.50/51/107 (real tagged releases whose sections were
missing entirely — v1.7.107 restored verbatim from the curated copy
at 35e9c624 that later went missing).
- Mechanical inventory across all 92 sections in range: every section
now has ≥3 curated bullets, zero raw-hash entries.
- What's New modal regenerated for the three restored versions
(sync-whats-new --check passes, 92 versions present).
- Manifest-notes-only confirmed by construction: the manifest reads its
changelog from CHANGELOG.md and check-release-manifest.sh rejects raw
or thin entries before publishing.
Evidence trail for the backfills is recorded in
docs/RELEASE_NOTES_BACKLOG.md.
Curated release notes for the pending v1.8.5-alpha: Cuprate (with the
two review catches), kdump/rasdaemon + the host-fixup OTA channel, the
uninstall-abort fix, federation inline-picture routing, honest disk
usage, the three lying-screens fixes (#143/#127/#129), durable mesh
notifications + router recovery (#57/#103), and upstream-release tracking
with the first-sweep safe bumps.
What's New modal synced via scripts/sync-whats-new.py (--check passes;
89 versions, all present). Per docs/RELEASE_NOTES_BACKLOG.md the
v1.7.44-alpha -> current section audit remains the open item before the
tag.
create-release aborted at [4/8] with "web/dist/neode-ui does not contain
v1.8.4-alpha — the frontend build no-opped or its output is stale". The
build had not no-opped: it was fresh, and simply had no 1.8.4 string to
embed.
sync-whats-new.py only matches '## vX.Y.Z (YYYY-MM-DD)'. The entry read
'## v1.8.4-alpha (draft — date set at cut)', so the version was invisible
to it: the gate's whats-new-sync stage reported "87 versions, all present"
while the release being cut had no What's New block. That modal is the
only place a version string appears in the frontend, so the bundle carried
none and the freshness check — correctly — refused it, while naming the
wrong cause. Step [5/8] only greps for '^## v1.8.4-alpha (' so it passed
the draft too.
Three changes: date the v1.8.4-alpha entry, insert the modal block it was
owed, and make the sync tool refuse any version header without a real date
instead of skipping it. Skipping is what let a wrong "all present" through.
Verified: the draft header now fails the check with an explicit message,
the dated one passes (88 versions, up from 87), and a rebuilt bundle
contains 1.8.4-alpha where it did not before.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Some apps carry a complete account system and are broken by an upstream
challenge: git clients speak basic-auth (not browser cookies), and a
BTCPay checkout link handed to a customer must open for that customer.
Both were behind the gate's login page — the "non-browser clients need an
access token" gap disclosed in five consecutive releases.
- New manifest port policy `auth: open`: the daemon still fronts the port
exactly like `gated` (loopback pin, external binds, frame-header fixes,
app-down retry page, Tor upstream) but serves it without the login
challenge. Requires auth_rationale, same burden of proof as `none`.
Gitea 3001 and BTCPay 23000 declare it.
- Runtime operator override per app (security.set-app-gate → app-configs/
<id>.json "gateEnabled"), surfaced as Settings → app → Access control.
Wins over the manifest in both directions and applies on the next
request — no restart, and it works today on catalog-covered apps whose
signed manifest still says `gated`.
- The gate resolves policy per-request from the live port map, so a
toggle takes effect without waiting for the 60s rebind sweep. "Off"
never releases the port: gated apps are loopback-pinned, so releasing
would strand them, not open them.
- security.app-gate-status now reports gate_enabled + any override.
- New guard test pins the `auth: open` set (both entries reviewed); the
`auth: none` count moves 25 → 26, absorbing pre-existing drift from the
phoenixd onboarding (loopback JSON API with its own generated password).
- Docs: the manifest spec's ports row documented only host/container/
protocol — bind, auth, auth_rationale and session_passthrough were
undocumented. Added a full "Ports & the app gate" section plus a
developer-guide entry telling app authors to enforce their own auth
regardless, since the operator can flip the gate either way.
Verified live on archi-dev-box from an external address: gated → 401 gate
page; override off → Gitea 200 own page, BTCPay 302 to its own login,
git-over-HTTP info/refs 200; override on → 401 again; clear → default.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- alby-hub + phoenixd icons re-set with the standard 12% inner margin
(they shipped edge-to-edge; every other icon carries whitespace).
- scripts/normalize-app-icon.py: wraps any third-party SVG mark onto the
house canvas — the system applies the tile plate (archy-app-icon)
automatically but deliberately no runtime inset, so the margin must be
baked; the guide now says exactly that.
- MarketplaceAppDetails: the icon now carries archy-app-icon like the
store tiles — the treatment no longer stops at the detail page.
- v1.8.2 changelog: third curated bullet (the ceremony gate requires 3).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
An app whose frame never loads while its backend reports Running (the
embed-refusal signature: frame-busting JS, top-level-origin apps,
SameSite=Strict logins — everything the gate's header stripping cannot
fix) is remembered in localStorage; every later launch opens a tab
straight from the click (user gesture, so no popup blocker), and
opensInTab() gives it the tab-launch icon. A successful iframe load
clears the memory and entries expire after 7 days, so nodes that gain
embedding (gate improvements) get re-probed instead of being remembered
broken forever. Dev guide updated; v1.8.2 changelog + What's New curated.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The animated federation map froze the framework-pt 4K TV: the launcher
held every machine to the HD 5500-era choppy-audio flags (single raster
thread, GpuRasterization banned) while the map wrote SVG attrs at 60fps.
- Launcher: two flag tiers. legacy = the proven conservative set; modern
(Intel gen8+, 'NNth Gen' models, AMD Ryzen) = default raster threads +
GPU rasterization. Classified from /proc/cpuinfo (11 model strings
covered by tests in-session); KIOSK_GRAPHICS=performance|quality in
kiosk-display.conf overrides; headless unchanged. Reaches deployed
kiosks via the include_str! self-heal, same as the vsync fix.
- system.kiosk-display.get/set: carries a 'graphics' field alongside
'preset'; setting one no longer clobbers the other.
- Settings → Display: Graphics picker (Auto / Compatibility / Quality).
- NetworkMap3D: kiosks default to the 2D projection (remembered toggle
still works) and tick at half rate with carried-over deltas — same
spin speed, half the paint cost.
- Changelog: curated Unreleased notes for all of the above + the gate
frame-embedding fix.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>