Branch stays alive for continued UX work; fast-forwarded to main after this
merge so it continues from the current tip.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The gate answered a dead app with the bare string "app is not responding",
which on the app's own port reads as the node itself being broken — reported
against Gitea on a fleet node (the actual fault was a ghost container holding
Gitea's LevelDB lock, crash-looping the managed container). Serve the same
styled page as the login challenge instead: names the app, says it may be
restarting, and retries via a Refresh header (page CSP allows no script).
Status stays 502 so machine clients still see an upstream failure.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The pasta exclusion paired with the transient-scope machinery: daemon-started
pasta/conmon inherit the starter's cgroup, so scopes kept app networking
alive across daemon restarts. A quadlet unit provides the same independence
(user.slice parentage) with supervision on top — Restart=always and
RestartSec=10, which also spaces restarts past pasta's port teardown (the
race observed live on strfry, 2026-08-10). Renderer needed nothing:
Network=pasta, ports and RestartSec were already in place.
Lifted in all four places: migrate_to_quadlet_if_needed, install_fresh, and
the missing-container unit-start path; the scoped start/restart helpers are
now unit-aware — quadlet-managed pasta routes through systemctl --user
(bare podman would fight systemd over an --rm container), legacy pasta
keeps its scope, so mixed fleets stay coherent during rollout.
Container suite 221/221.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The 2026-08-08 unwire set the bar: a DURABLE record of 'this app is
installed' must drive reaping, never runtime inference. installed-apps.json
is that record (written on install, cleared on uninstall, backfilled from
live containers, held through container absence). The reconciler's
companion loop now reaps against it — and only when the registry file
actually exists and parses: 'I could not look' and 'nothing is installed'
both surface as an empty set from the lossy loader, so a new
load_installed_apps_if_recorded keeps the distinction alive. The
runtime-derived set is deliberately not a fallback; it is the input class
that caused the incident. ORPHAN_GRACE still applies on top.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The signed catalog has homed grafana at lfg2025/grafana:10.2.0 in the fleet
registry all along; the manifest still pulled Docker Hub's grafana/grafana,
so the repo-mismatch guard (correctly) discarded the catalog image on every
tick and warned every ~75s on every node. Registry verified to carry the
image; aligning the repo path makes the already-signed catalog entry
effective — no re-signing needed, no behavioural change beyond pull source.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Turning discovery on prompts for a name; it rides the public announcement
(clean_display_name both directions: single line, control-stripped, 32-char
cap — it round-trips through untrusted relays). Blank lists as npub only;
off/on keeps the stored name; sending an empty name clears it. Discovery
lists show the name with the npub beneath. Own-npub display switches to
middle-ellipsis so the comparable tail stays visible.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
'Re-keying requires the authenticated system.factory-reset' is precise and
useless to an operator standing at the seed screen. Map it to what they can
do: sign in, or factory-reset from Settings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The presence event deliberately never contains the onion — the npub is
what's actually visible on the relays, and the UI previously showed neither
it nor any way to find it. Same container, same styling; the copy button
copies the npub.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Presence gets a NIP-40 expiration (48h) and a 12h re-publish heartbeat that
honours the runtime toggle (UI-enabled nodes previously never re-published
at boot). discover() drops pre-TTL events client-side for relays that
ignore NIP-40. Switching discovery off publishes an empty tombstone, and
factory-reset tombstones BEFORE wiping identity — after the wipe the key is
gone and the stale event could never be replaced by anyone.
nostr.discovery-status now also returns the node's own npub (load-only).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The catch block dropped `loading` before branching, so every failed poll
unmounted the lock icon and status text for the 4s retry gap — the screen
flashed in and out on a live install test (zaza-optiplex, 2026-08-09).
`loading` now stays true across isServerStartingError retries; only success
or a genuine failure dismisses the waiting state.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- The fit now scales screen X and Y independently (capped at 1.75x
anisotropy), so both 2D and 3D stretch to the container's aspect ratio —
a portrait phone uses its full height instead of shrinking the orbit to
the narrow width, and wide desktop panels spread horizontally.
- Legend/key centres at the top on mobile, mirroring the bottom-centre
2D/3D toggle.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Compact containers (<480px) shrink the fit margins so the scene fills the
phone screen instead of floating in padding.
- 3D mode steepens its tilt on portrait (-0.95 vs -0.5) so the orbit reads
as a tall ellipse with depth, not a squashed horizontal band; projection
params are resolved per-aspect via modeParams() and re-resolve on resize.
- The 2D/3D toggle moves to bottom-centre on mobile (thumb reach, frees the
top edge); the hint line tucks above it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Root causes of the flaky intro / janky revisits, all fixed structurally:
- Graph updates now RECONCILE against the live scene by identity (peer DID /
request id) instead of wiping the SVG: in-place updates for label/online/
trust changes, animated arrivals for new nodes, animated departures for
removed ones, and short-way-around slot glides when the ring layout shifts.
The old wipe-and-rebuild orphaned every in-flight intro tween whenever the
5s poll or the async self-DID fetch landed mid-intro — that was the
'sometimes parts don't show'.
- The intro is gated on readiness: first real container measurement (post-
layout, via ResizeObserver) AND the self node being present, with a 1.2s
fallback. Graph changes during the intro queue and apply on completion.
- Layout is deterministic per node: peers sort by DID for slot assignment and
jitter/phase hash from the DID, so backend response order can't shuffle
the map between polls.
- The long cinematic intro plays once per browser session; revisits get a
shorter, lighter entrance that doesn't fight the route transition.
- selfDid seeds from the cached localStorage value (re-cached on fetch) so
the centre node and links exist from the first frame.
- Requests' waiting-room orbit pushed clearly outside the peer rings (+1.0)
with loose per-request hover jitter — outside until accepted, then the
join animation glides them in.
- Full unmount hygiene: every tween killed by target (incl. the repeating
sonar pulse), fallback timer cleared, window listeners removed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Inbound pending requests render as blinking yellow point-globes on an
orbit outside the peers, dotted-linked to self, with their own dashed
guide ring and a Request legend chip.
- Tapping one opens a black-glass popover (name, request message,
Accept/Reject) centred over the scene; tap-away or ✕ dismisses.
- Reject: the node swells and pops out of existence, taking its link.
- Accept: green burst ring, the point cloud/link morph to the trusted
colour and the globe glides inward onto the peer orbit, then the data
refresh replaces it with the real peer node.
- Wired to the same approve/reject RPCs as the pending panel; new
motionTokens.color.pending (#facc15) token.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Small glass 2D/3D toggle top-right of the map: tweens tilt/perspective/fit
(0.9s inOut) so the scene folds flat into the original radial 2D layout and
back. Default: portrait/mobile opens 2D, desktop 3D; last choice saved to
localStorage.
- Fix: setPointerCapture retargeted pointerup to the container, suppressing
click synthesis on children — node taps and the toggle never fired. Drag
now tracks via window listeners, no capture.
- Fix: stale post-drag distance made the click-suppressor swallow toggle taps
indefinitely ('stuck' toggle). Distance resets on every pointerdown and the
suppressor is one-shot.
- Map no longer slides under the floating mobile back pill: the
mobile-scroll-pad-back panel keeps its full 64px clearance when filled.
- Mobile DID copy/rotate card moved out of the header to below the view tabs
(new DidCardMobile.vue) and hidden on the Network Map tab.
- Node labels: dark stroke halo removed; 10px on mobile.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Portrait containers now render near top-down and near-orthographic (tilt
-1.35, persp 16) so the map reads like the previous 2D radial layout —
rings become circles with no perspective squash. Desktop keeps the 3D
depth camera. Globe point-spheres unchanged on both.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The map view turns the root into a flex column; default stretch alignment
was pulling the mode-switcher full-width on desktop. md:self-start restores
the content-fit pill (mobile stays full-width by design).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Nodes are now spheres made of points: fibonacci point-cloud globes with
depth-shaded dots, limb outline, and a slow local spin (drag adds parallax).
Self node is black — dark dots over a soft light backing disc with the
brand-orange sonar pulse marking 'you'. Spheres and dots sized down.
- Scene auto-fits and centres to the container on every device: the outermost
orbit is sampled through the real camera projection to get true bounds,
then scaled/centred between the overlays. Portrait screens tilt the camera
towards top-down so the orbit uses the full height (mobile readability).
- No idle orbiting: nodes hold position with a gentle side-to-side sway;
drag inertia now settles to a stop. Intro dolly pushes in (0.82→1) instead
of pulling back, so nothing clips during the intro.
- Labels get a dark paint-order halo and bump to 12.5px on mobile.
- Point clouds update at half frame rate to keep mobile/companion smooth.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Add gsap 3.15 + design-system-aware motion module (src/utils/motion.ts):
shared colour/duration/ease tokens mirrored from style.css, gsap.defaults,
live prefers-reduced-motion check.
- Replace the d3 force NetworkMap with NetworkMap3D.vue: peers on projected
3D orbital rings around the self node, cinematic intro (camera dolly +
staggered fly-in + ring draw), idle rotation with drag-to-orbit inertia,
depth-sorted painter's order, trust-colour palette, online/offline states,
sonar pulse on self, tap-a-node opens the detail modal.
- Map view now fills the dashboard panel to the bottom edge on desktop,
mobile and companion: .dashboard-scroll-panel:has(.node-map-stage) turns
the panel into a column (tab-bar/safe-area/audio-player aware padding)
instead of leaving the old dead bottom margin.
- Reduced motion: intro/idle skipped, scene renders static.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The signed release catalog was pinned in 6c41852c; the curation source still
said :latest, which ec8d88a6 established can stall the fleet.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The release tarball bakes demo/aiui in for fresh installs; the previous dist
predated the phase-13 security waves (S1-S7, W1.4) and would have shipped a
pre-hardening bundle.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Quadlet renders --rm, so every stop DELETES the container: a scan during
the stop->start window legitimately sees nothing where an installed app
lives. The state layer treated that as the app not existing — after three
absent scans it removed the entry entirely, so dashboard tiles vanished
mid-restart, gate waits read 'absent' (grafana, at load ~2, ruling out
contention), and under daemon-restart churn the FIRST scan could publish
a completely empty My Apps map (observed twice on 2026-08-09).
Two guards, both keyed to the durable installed-apps.json registry:
- Eviction: an id the registry says is installed, and the user has not
uninstalled, is held as Stopped (health/exit cleared) instead of being
removed. The next scan that sees the container restores live state;
desired-state recovery still recreates genuinely lost containers; a
deliberate uninstall clears the registry first and still disappears.
- Empty scans: the first-scan exemption no longer applies when the
registry is non-empty — better to keep saying "scanning" than "empty".
Verified: scan-merge/absence/registry test filters 34/34.
Also carries the v1.7.127-alpha changelog (user-benefit entries for the
Tor self-heal, trusted invites, the three-layer mempool fix, vanishing
apps, the Bitcoin version pin, and the smaller UI fixes).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
An OTA updates the binary and web assets — not /etc/nginx, and not
/opt/archipelago/scripts. Auditing the 2026-08-09 fixes' delivery paths
found two that would silently reach nobody already installed:
- scripts/tor-helper.sh (reset-failed + truthful restart result) shipped
only via ISO builds and manual deploys. Now embedded via include_str!
like the doctor script, staged and installed at boot when the on-disk
copy differs.
- The /app/mempool/ nginx Upgrade/Connection headers existed only in repo
snippet sources consumed at image build time. A boot repair now
idempotently patches any mempool location block missing them — in the
live vhost (archipelago-http, the one sites-enabled actually links to),
the legacy file, and the installed snippet — and reloads nginx once.
Without this, every fleet node's mempool UI keeps loading-but-never-
connecting after the OTA that supposedly fixed it.
Both are non-fatal boot repairs in the existing bootstrap chain, no-ops
when everything is already current.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The sticky tab header painted bg-black/70 across the modal — reported as
a black container where the design is transparent glass. The backdrop
blur alone keeps pinned tabs legible over rows scrolling beneath; the
tint drops to the standard bg-white/5.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The gate accepted a client's websocket upgrade (.with_upgrades() on the
serving side) but proxy_to_app forwarded every request with a plain
hyper::Client, which completes the app's 101 handshake and then drops the
upgraded connection. Result: any ws-driven app behind a gated port loads
its page fine and then dies with close code 1006 on every connect —
mempool's entire UI is such an app, and the operator's browser console
was the only place the failure was visible. This was the THIRD layer of
the same outage (electrumx sync, then the nginx proxy path, now the app
tile's gated-port path — each fix exposed the next).
On an Upgrade request the gate now takes the client's OnUpgrade handle
from the request extensions, hands the handshake to the app with the same
sanitized headers (cookie/authorization stripping applies unchanged), and
on a 101 bridges the two upgraded connections with copy_bidirectional.
Non-101 upstream answers pass through so auth failures stay visible.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The dashboard's /app/mempool/ proxy forwarded plain HTTP only: no
Upgrade/Connection headers, so the browser's /api/v1/ws handshake reached
mempool-api as a bare GET and Express 404'd it. The mempool page loaded
fine and every REST probe was green while the user saw a UI that never
connects — the backend was fully healthy and completely unreachable in
the only way that matters to the page.
This hid behind the electrumx initial-sync outage: once sync finished and
the API answered, "mempool works" was declared from REST checks while the
websocket path stayed dead. Reported by the operator from the browser,
which is the only place it was visible.
Adds Upgrade/$http_upgrade + Connection "upgrade" to the /app/mempool/
location in both shipped sources (image-recipe snippet + scripts copy).
Live-verified on the dev node: ws through the proxy now answers 101; the
control probe without upgrade headers reproduces the 404 signature.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The full project build (vue-tsc -b, which also checks test files) caught
two default-ServerInfo constructors the scoped typecheck missed. Rather
than adding the field to both, the type now tells the truth for a
mixed-version fleet: daemons older than 2026-08-09 omit the field, so
consumers must treat absent as "unknown" and only === true as connected —
which is exactly how Home.vue already reads it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Home's Network card computed "Connected" from server-info's tor-address —
a string read from the hidden-service hostname file, which persists on
disk however dead the daemon is. Three fleet nodes ran with Tor down for
days while their dashboards said Connected; that indicator is why nobody
noticed. Server.vue's Tor label had the same flaw one step removed,
inferring "running" from services having onion_address values while the
correct signal (torDaemonRunning, backed by a real socket probe) sat
computed and unused twelve lines above.
Both now read liveness signals only: Home reads the new tor-running field
(server-side connect to 127.0.0.1:9050), Server.vue uses
torDaemonRunning. The address remains displayed as what it is — an
address — never as proof of life.
Verified: vue-tsc clean; rpc-client suite 79/79.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three of five reachable fleet nodes had Tor completely dead — Home said
"Connected" on all of them — and shipping the generator fix alone would
have repaired none of them. Two additions close that loop.
heal_on_boot (wired into the bootstrap repair chain): regenerate torrc
from current config with the fixed generator, and apply-and-restart ONLY
if the live file drifted or Tor is not answering on 9050. A healthy node
is left untouched. Without this, regenerate_torrc runs only from the Tor
RPC handlers and package install, so a node carrying a poisoned torrc
keeps it until someone happens to toggle a Tor setting — and worse, the
still-running OLD binary re-poisons on any such toggle: observed live on
the dev node at 07:20, when the running daemon rewrote torrc with the
unbindable gateway line hours after it had been hand-fixed. The heal
makes the fix self-applying on every restart, i.e. the OTA itself.
ServerInfo gains tor-running, populated by a real connect to
127.0.0.1:9050 each state refresh. tor-address is read from the
hidden-service hostname file, which OUTLIVES a dead daemon — it is a
configuration artifact, and the dashboard treating it as liveness is
precisely why three dead nodes showed "Connected" for days. Liveness now
comes only from the probe; the address stays a separate fact.
The heal reports the truth: it re-probes after restarting and warns if
Tor still is not answering, rather than assuming success.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
No node in the fleet could mint a Trusted federation invite or promote a
peer to Trusted from the dashboard. The cause is one case comparison.
Granting Trusted requires the node password; when none is supplied the
handler bails with "PASSWORD_REQUIRED: node password required to grant
Trusted" — a sentinel the frontend machine-reads (isPasswordRequired
checks includes('PASSWORD_REQUIRED')) to know it should open the password
prompt. sanitize_error_message allowlists "Password", but starts_with is
case-sensitive, so the ALL-CAPS sentinel failed the allowlist and
collapsed into "Operation failed. Check server logs for details." The
frontend never saw the sentinel, the prompt never opened, and the flow
died with a generic error on every node.
Reproduced via direct RPC: federation.invite {trust_level:"trusted"}
without a password returned the generic message; the same call WITH the
password minted a code fine — the machinery was healthy, only the signal
was being eaten.
"Tor address not available. Tor may not be running." — the invite
handler's other user-actionable precondition — was masked the same way
and is allowlisted too.
Regression tests pin both passing through verbatim, and
internal_errors_stay_generic still passes, so the generic masking of real
internals is unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
All ten AI data-access categories offer a context toggle, but only five
(Apps, System, Network, Media, Bitcoin) have any tool behind them — Wallet,
Files, Search, Local AI and Notes grant the assistant visibility with
nothing to act on. The Settings copy implied actions that do not exist.
Operator decision (2026-08-09): ship honest copy now, build the tools as
ordinary backlog. Each of the five carries a contextOnly flag, a "context
only" badge, and an appended description line saying the assistant can see
this but can't take actions with it yet. Drop the flag per category as
real tools land.
Verified: vue-tsc clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two problems with running the gate against a mid-initial-sync electrumx:
1. The (now honest) protocol probe can only fail — ElectrumX serves no
sessions until it has caught up to its daemon, so the failure names a
state nobody can act on.
2. Worse, the destructive stop/start/restart tests actively destroy sync
progress: electrumx flushes its DB cache at 1GB, i.e. rarely, and every
restart discards all unflushed work back to the last flush. This node
spent 8d14h in initial sync largely because gate runs and reboots kept
taking hours of progress away — it restarted at 06:16 and resumed from
959,774, the same height it had reported hours earlier.
The suite now detects initial sync POSITIVELY — a fresh (<30 min)
"our height: N daemon: M" line from electrumx's own log, gap > 10 — and
skips the probe and the four destructive tests with the gap named:
# skip electrumx initial sync in progress (1672 blocks behind) — ...
This is not the container-absent skip trap fixed earlier: absence of the
log line means "unknown" and the tests run and fail honestly. Validated
against the live mid-sync node: all four guards fired with the real gap;
on a synced node the line shows gap 0-1 and everything runs.
Unblocks the release gate from waiting hours on a sync it was itself
prolonging.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
changeTrust() wrote failures into `error`, which renders inside <NodeList>
— page content that sits BEHIND the open NodeDetailModal. So when
promoting a peer to Trusted failed, the operator saw the dropdown snap
back and nothing else: the error banner was painted on a surface the
modal was covering. Reported as "the error doesn't show in the modal",
2026-08-09.
NodeDetailModal gains an actionError prop rendered directly under the
Trust Level control, and Federation.vue routes failures there whenever
that node's modal is open (clearing on close and on the next attempt).
The page banner still serves failures with no modal in the way, and the
password-retry path keeps its own in-modal error as before.
Verified: vue-tsc clean; federation component tests 5/5.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The dashboard's "Restart Tor" button dispatched to this helper and always
got {"ok":true} back. Two real defects, in order of importance — and one
disproved theory, recorded so nobody re-chases it:
1. reset-failed was missing. Once tor@default fails enough times systemd
latches "Start request repeated too quickly" and refuses to start it at
all; a plain restart is then a no-op no matter which unit you name.
All three fleet nodes found dead on 2026-08-09 were in exactly that
state, which is why the button appeared to do nothing.
2. The result was unconditional. The write-torrc branch waited up to 30s
for SOCKS and then ignored the outcome; the restart branch slept 3s and
claimed success. The UI reported "restarted" over a dead daemon.
Disproved: this was NOT wrong-unit targeting. `systemctl restart tor`
does propagate to tor@default — measured on austin-sapien, MainPID
changed. tor@default is still addressed explicitly because it is the
unit carrying the failed state worth resetting.
restart_tor_daemon() now: reset-failed, restart tor@default (fall back to
tor on single-instance installs), wait up to 30s for SOCKS on 9050, and
return {"ok":false,"error":...} pointing at journalctl when it never
comes up. Callers may no longer report success without a live SOCKS port.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
regenerate_torrc widened SOCKS to the archy-net gateway whenever `podman
network inspect archy-net` reported one. Podman reporting a gateway is not
proof the host can bind it: under rootless podman the bridge and its gateway
live inside a network namespace and never appear on a host interface.
Tor binds listeners at STARTUP, not at config-check time — `--verify-config`
passes happily — so the failure lands as a refusal to start. austin-sapien,
2026-08-09:
[warn] Could not bind to 10.89.0.1:9050: Cannot assign requested address
[warn] Failed to parse/validate config: Failed to bind one of the listener ports.
[err] Reading config failed--see warnings above.
Tor then died completely — loopback SOCKS and every hidden service with it —
having run 3d 18h before something restarted it. The node's Home tab still
read "connected" because tor.service is Debian's multi-instance MASTER unit,
which is active(exited) by design; the real instance, tor@default, was failed.
Whether a node is affected is pure timing: archi-dev-box's torrc has only
`SocksPort 9050` because archy-net was not discoverable when its torrc was
last regenerated, so it fell into the loopback branch and Tor starts fine.
Nodes whose torrc was generated while archy-net was visible are one Tor
restart away from the same outage.
The gateway is now probed the way Tor will use it — bind an ephemeral port on
that exact address — and a failure falls through to the existing loopback-only
branch, which was already written and commented "fail closed". Widening SOCKS
must never be able to take the whole daemon down.
Verified on this host: 127.0.0.1 binds, 10.89.0.1 returns EADDRNOTAVAIL.
Tor restored on austin-sapien and confirmed end to end — bootstrapped 100%,
and a request through the proxy returns {"IsTor":true}.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both electrumx tests did socket.create_connection(("127.0.0.1", 50001)) and
called it proof the service was up. It is not: that is the HOST-published
port, and podman's port forwarder completes the TCP handshake even when
nothing inside the container is listening.
The cost was real. Both tests were green while mempool-api sat in a hard
ECONNREFUSED loop against electrumx:50001 from inside archy-net — the
operator's "mempool doesn't connect to bitcoin". Measured side by side:
127.0.0.1:50001 from the host OPEN, electrumx:50001 from a sibling container
REFUSED.
The cause is not a misconfiguration — mempool-api's CORE_RPC_HOST,
ELECTRUM_HOST and ports are all correct. electrumx has not finished its
initial sync (DB height 959,774 vs chain tip 961,706, ~1,932 blocks, 8d 14h
elapsed) and ElectrumX does not serve sessions until it has caught up.
Now both tests do a server.version round-trip and require a JSON result, so a
forwarder that accepts-and-drops fails. Verified against the live node: the
new probe FAILS today where the old one passed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Moves the default off 29.3.knots20260508, which applies the BIP110/RDTS
network upgrade and HALTS until an operator sets consensusrules=rdts. A node
running it sits frozen — 100.64.204.114 is stuck at block 961,692 with blocks
AND headers static, 11 peers and no pruning, while reporting
initialblockdownload=false and verificationprogress 0.99998, i.e. "synced".
The nodes on 20260210 are at the tip.
The old catalog's default was {"version":"latest", image: ...20260508} while
the manifest's :latest tag resolves to 20260210 in the registry, so a fresh
install and a plain pull produced different daemons. That is almost certainly
how one node ended up on the RDTS build and its siblings did not.
20260508 stays selectable — adopting RDTS remains possible, just deliberate.
Diff against the previously-signed catalog is exactly one app, all four
fields: version, image, default and the embedded manifest. Registry hosts
unchanged and inside the trust floor; verified by
check-catalog-registry-trust.py and `ceremony verify`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>