chore: release v1.7.46-alpha

Follow-up to v1.7.45-alpha closing the remaining tasks identified by the
resilience sweeps + the new bitcoin orphan / install-fail-vanish bugs.

User-visible:
- Health monitor: stop paging on orphaned containers from variant switches
- Install fail: card stays visible (was vanishing) with error message
- Stack pull progress: interpolate 20→70% (was stuck at 20%)
- docker.io → lfg2025 mirror: bitcoin/gitea/nextcloud/valkey

Internal:
- Resilience harness — install-wait uses expected_containers_for, ui+auth
  probes retry with 60s backoff, dep-snapshot fix
- InstallProgress gains optional `message` field (frontend renders it
  when phase is None)

binary  $(stat -c %s releases/v1.7.46-alpha/archipelago)  sha256:$(sha256sum releases/v1.7.46-alpha/archipelago | awk '{print $1}')
tarball $(stat -c %s releases/v1.7.46-alpha/archipelago-frontend-1.7.46-alpha.tar.gz)  sha256:$(sha256sum releases/v1.7.46-alpha/archipelago-frontend-1.7.46-alpha.tar.gz | awk '{print $1}')

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
archipelago 2026-04-29 14:50:33 -04:00
parent 4ec6ca98c1
commit 992b673b20
14 changed files with 137 additions and 27 deletions

View File

@ -1,5 +1,13 @@
# Changelog
## v1.7.46-alpha (2026-04-29)
- Health monitor no longer pages "Auto-restart failed" for orphaned containers. After a variant switch (bitcoin-core ↔ bitcoin-knots) the previous variant's container could survive uninstall and the health monitor would try restarting it forever. Now skipped silently with a debug log.
- Apps no longer disappear from My Apps when an install fails. The card stays visible with state=Stopped so the user can retry or uninstall, with the failure reason surfaced via the new install_progress.message field.
- "Downloading…" progress now actually advances during multi-image stack pulls. Was sticking at 20% until all pulls finished; now interpolates 20%→70% based on which image of N has landed.
- Pulled four docker.io images (bitcoin, gitea, nextcloud, valkey) into the lfg2025 registries on OVH and tx1138. Removes a docker.io dependency from first-boot installs.
- Resilience harness improvements: install-fail entries no longer vanish, install/uninstall/probe cells are timing-tolerant (60s retry on ui_probe and auth_probe), dep snapshots no longer leak companion containers into the dependent app's "new containers" set.
## v1.7.45-alpha (2026-04-29)
- Bitcoin RPC auth is durable. The dashboard reliably connects across container restart, image update, and reboot. Was failing on registry-pulled images that shipped a stale baked-in password.

View File

@ -31,7 +31,7 @@
"author": "Bitcoin Core contributors",
"category": "money",
"tier": "optional",
"dockerImage": "docker.io/bitcoin/bitcoin:28.4",
"dockerImage": "146.59.87.168:3000/lfg2025/bitcoin:28.4",
"repoUrl": "https://github.com/bitcoin/bitcoin"
},
{
@ -125,7 +125,7 @@
"icon": "/assets/img/app-icons/gitea.svg",
"author": "Gitea",
"category": "development",
"dockerImage": "docker.io/gitea/gitea:1.23",
"dockerImage": "146.59.87.168:3000/lfg2025/gitea:1.23",
"repoUrl": "https://gitea.com"
},
{
@ -263,7 +263,7 @@
"icon": "/assets/img/app-icons/nextcloud.webp",
"author": "Nextcloud",
"category": "data",
"dockerImage": "docker.io/nextcloud:28",
"dockerImage": "146.59.87.168:3000/lfg2025/nextcloud:28",
"repoUrl": "https://github.com/nextcloud/server"
}
]

2
core/Cargo.lock generated
View File

@ -80,7 +80,7 @@ checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
[[package]]
name = "archipelago"
version = "1.7.45-alpha"
version = "1.7.46-alpha"
dependencies = [
"anyhow",
"archipelago-container",

View File

@ -1,6 +1,6 @@
[package]
name = "archipelago"
version = "1.7.45-alpha"
version = "1.7.46-alpha"
edition = "2021"
description = "Archipelago Bitcoin Node OS - Native backend"
authors = ["Archipelago Team"]

View File

@ -113,11 +113,26 @@ impl RpcHandler {
Err(e) => {
error!("package.install {} failed: {:#}", package_id_spawn, e);
install_log(&format!("INSTALL FAIL: {}{:#}", package_id_spawn, e)).await;
// No pre-state to revert to — remove the entry entirely so
// the UI shows the app as not installed. The next package
// scan will re-create it only if podman actually has a
// container for it (partial install recovery).
remove_package_entry(&handler.state_manager, &package_id_spawn).await;
// Don't remove the entry — that's what made the card
// vanish from My Apps mid-install / between retry-loop
// attempts (e.g. tailscale's entrypoint failure). Leave
// the entry visible with state=Stopped + the install
// error in install_progress.message so the user can see
// what went wrong and decide whether to retry or
// uninstall. clear_install_progress would erase the
// message, so we set it explicitly here instead.
let err_msg = format!("Install failed: {:#}", e);
let (mut data, _) = handler.state_manager.get_snapshot().await;
if let Some(entry) = data.package_data.get_mut(&package_id_spawn) {
entry.state = PackageState::Stopped;
entry.install_progress = Some(crate::data_model::InstallProgress {
size: 0,
downloaded: 0,
phase: None,
message: Some(err_msg),
});
handler.state_manager.update_data(data).await;
}
}
}
});

View File

@ -25,6 +25,7 @@ impl RpcHandler {
size,
downloaded,
phase: existing_phase,
message: None,
});
self.state_manager.update_data(data).await;
}
@ -55,6 +56,7 @@ impl RpcHandler {
size,
downloaded,
phase: Some(phase),
message: None,
});
self.state_manager.update_data(data).await;
}
@ -97,6 +99,7 @@ impl RpcHandler {
size: total,
downloaded,
phase: existing_phase,
message: None,
});
state_manager.update_data(data).await;
}

View File

@ -201,7 +201,7 @@ impl RpcHandler {
let images = [
"146.59.87.168:3000/lfg2025/immich-postgres:14-vectorchord0.4.3-pgvectors0.2.0",
"docker.io/valkey/valkey:7-alpine",
"146.59.87.168:3000/lfg2025/valkey:7-alpine",
"146.59.87.168:3000/lfg2025/immich-server:release",
];
self.set_install_phase("immich", InstallPhase::PullingImage)
@ -300,7 +300,7 @@ impl RpcHandler {
"--health-cmd=valkey-cli ping || exit 1",
"--health-interval=30s",
"--health-retries=3",
"docker.io/valkey/valkey:7-alpine",
"146.59.87.168:3000/lfg2025/valkey:7-alpine",
])
.output()
.await;

View File

@ -255,6 +255,12 @@ pub struct InstallProgress {
/// a fixed UI percentage and a descriptive label.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub phase: Option<InstallPhase>,
/// Optional explicit message — used to surface install failures so
/// the UI can keep the app card visible with an error description
/// instead of silently removing the entry on fail. UI's PHASE_INFO
/// label takes precedence when phase is set.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
}
/// Phases of the install / update pipeline, surfaced to the UI so users

View File

@ -539,6 +539,20 @@ pub fn spawn_health_monitor(state: Arc<StateManager>, data_dir: PathBuf) {
debug!("Skipping uninstalled container: {}", container.name);
continue;
}
} else {
// Orphan: container exists in podman but archipelago has
// no package_data entry for it. Common after a variant
// switch (bitcoin-core ↔ bitcoin-knots) where the
// uninstall removed the package entry but the prior
// variant's container survived in stopped state. Without
// this guard the health monitor pages every minute with
// "Auto-restart failed (attempt N/10)" for an app the
// user can no longer see in the dashboard.
debug!(
"Skipping orphan container (not in package_data): {}",
container.name
);
continue;
}
if container.healthy {

View File

@ -1,7 +1,7 @@
{
"name": "neode-ui",
"private": true,
"version": "1.7.45-alpha",
"version": "1.7.46-alpha",
"type": "module",
"scripts": {
"start": "./start-dev.sh",

View File

@ -31,7 +31,7 @@
"author": "Bitcoin Core contributors",
"category": "money",
"tier": "optional",
"dockerImage": "docker.io/bitcoin/bitcoin:28.4",
"dockerImage": "146.59.87.168:3000/lfg2025/bitcoin:28.4",
"repoUrl": "https://github.com/bitcoin/bitcoin"
},
{
@ -125,7 +125,7 @@
"icon": "/assets/img/app-icons/gitea.svg",
"author": "Gitea",
"category": "development",
"dockerImage": "docker.io/gitea/gitea:1.23",
"dockerImage": "146.59.87.168:3000/lfg2025/gitea:1.23",
"repoUrl": "https://gitea.com"
},
{
@ -263,7 +263,7 @@
"icon": "/assets/img/app-icons/nextcloud.webp",
"author": "Nextcloud",
"category": "data",
"dockerImage": "docker.io/nextcloud:28",
"dockerImage": "146.59.87.168:3000/lfg2025/nextcloud:28",
"repoUrl": "https://github.com/nextcloud/server"
}
]

View File

@ -63,18 +63,44 @@ export const useServerStore = defineStore('server', () => {
if (progress.phase) {
const info = PHASE_INFO[progress.phase]
if (info) {
// Within the PullingImage band (20→70%), interpolate the
// bar based on how many images / bytes have landed so far.
// Without this, multi-container stacks (indeedhub: 7,
// mempool: 3, btcpay: 4) just sit at 20% for the entire
// pull duration — exactly what the user reported as
// "Downloading sticks at 20% mostly". X-of-N progress
// comes from set_install_progress(i, n) in stacks.rs.
let bandProgress = info.progress
if (progress.phase === 'pulling-image' && progress.size > 0) {
const fraction = Math.min(progress.downloaded / progress.size, 1)
// PullingImage band: 20% → 70%, so 50pp to interpolate over.
bandProgress = 20 + Math.round(fraction * 50)
}
// Only advance forward — never let the bar step backward
// between patches (can happen briefly during scan merges).
const nextProgress = Math.max(current.progress, info.progress)
const nextProgress = Math.max(current.progress, bandProgress)
// Show explicit message when set (e.g. install-fail descriptions
// surfaced via install_progress.message) — otherwise PHASE_INFO label.
const label = progress.message || info.message
installingApps.value.set(appId, {
...current,
status: info.status,
progress: nextProgress,
message: info.message,
message: label,
})
continue
}
}
// No phase but message is set (install-fail path) — show the message
// even if PHASE_INFO doesn't apply. Status stays whatever the watcher
// currently has.
if (progress.message) {
installingApps.value.set(appId, {
...current,
message: progress.message,
})
continue
}
// Fallback: byte counters (rare — podman usually doesn't
// emit parseable progress on a piped stderr).
const pct = progress.size > 0 ? Math.round((progress.downloaded / progress.size) * 100) : 0

View File

@ -166,6 +166,9 @@ export interface InstallProgress {
* counters podman pull doesn't emit parseable progress when
* stderr is piped, so byte counters are usually (0,0). */
phase?: InstallPhase
/** Optional explicit message surfaced on install failures so the
* UI can show what went wrong instead of silently removing the card. */
message?: string
}
// RPC Request/Response types

View File

@ -129,19 +129,34 @@ snapshot_containers() {
ssh_run "podman ps -a --format '{{.Names}}' | sort"
}
# Whether $app currently has any of its expected containers running. Uses
# Whether $app currently has ALL of its expected containers running. Uses
# the per-app metadata table in lib.sh (expected_containers_for) so variant
# apps (bitcoin-knots/bitcoin-core sharing slots) and stacks are detected
# correctly. Falls back to name-prefix match for apps the table doesn't know.
#
# Returns true only when every expected container is present. Earlier
# versions returned true on ANY match — that caused dep installs (e.g.
# bitcoin-knots required by btcpay) to be declared "installed" as soon as
# the backend container appeared, before the UI companion (archy-bitcoin-ui)
# was up. The before-snapshot then missed the companion, the after-snapshot
# caught it, and it leaked into the dependent app's "new containers" set,
# false-positive-FAILing stop/uninstall when the companion (correctly) did
# not respond to the dependent app's package.stop.
app_already_installed() {
local app="$1"
local snap; snap=$(snapshot_containers)
local expected
expected=$(expected_containers_for "$app")
local c
if [ -n "$expected" ] && [ "$expected" != "$app" ]; then
local c missing=0
for c in $expected; do
echo "$snap" | grep -qxF "$c" && return 0
echo "$snap" | grep -qxF "$c" || missing=1
done
[ "$missing" -eq 0 ] && return 0
# Fall through to prefix match if the expected_containers list has
# gaps; a partial install still counts as "installed enough" for
# preclean purposes.
fi
# Generic prefix fallback for apps not in the expected_containers_for table.
echo "$snap" | grep -qE "^(${app}|${app}-|archy-${app}|archy-${app}-)"
}
@ -291,8 +306,18 @@ run_app_matrix() {
fi
# ── 02 ui_probe ──────────────────────────────────────────────
# Retry with backoff — install just finished, but the app's backend
# (fedimint, immich, mempool stack) may take 30+s to be ready to serve
# HTTP. Probing immediately false-positive-FAILed those apps; pass on
# first 2xx/3xx within 60s.
local code
local ui_deadline=$(($(date +%s) + 60))
while :; do
code=$(probe_app_proxy "$app")
[[ "$code" =~ ^(2[0-9][0-9]|3[0-9][0-9])$ ]] && break
[ "$(date +%s)" -ge "$ui_deadline" ] && break
sleep 5
done
# Accept all 2xx/3xx — proxy reaches backend, app may redirect to login,
# serve OAuth flow (307), or use 308 permanent. 401/403 still fail because
# those mean "backend reached, app rejected request" which is the
@ -300,17 +325,27 @@ run_app_matrix() {
if [[ "$code" =~ ^(2[0-9][0-9]|3[0-9][0-9])$ ]]; then
record "$app" ui_probe PASS "HTTP $code"
else
record "$app" ui_probe FAIL "HTTP $code (expected 2xx/3xx)"
record "$app" ui_probe FAIL "HTTP $code (expected 2xx/3xx, retried 60s)"
fi
# ── 03 auth_probe (only for apps with a credentialed/data endpoint) ──
# Same backoff treatment: bitcoin-ui's nginx config bind-mount is
# picked up at start, but the bitcoin-core backend may not have
# accepted RPC connections yet on a fresh install.
local probe_code; local pass_codes
if probe_code=$(auth_probe_for "$app" 2>/dev/null) && [ -n "$probe_code" ]; then
pass_codes=$(auth_probe_pass_codes "$app")
if probe_code=$(auth_probe_for "$app" 2>/dev/null) && [ -n "$probe_code" ]; then
local auth_deadline=$(($(date +%s) + 60))
while :; do
echo " $pass_codes " | grep -qF " $probe_code " && break
[ "$(date +%s)" -ge "$auth_deadline" ] && break
sleep 5
probe_code=$(auth_probe_for "$app" 2>/dev/null) || break
done
if echo " $pass_codes " | grep -qF " $probe_code "; then
record "$app" auth_probe PASS "HTTP $probe_code"
else
record "$app" auth_probe FAIL "HTTP $probe_code (expected one of: $pass_codes — credential plumbing broken)"
record "$app" auth_probe FAIL "HTTP $probe_code (expected one of: $pass_codes; retried 60s — credential plumbing broken)"
fi
else
record "$app" auth_probe SKIP "no authenticated probe defined"