fix: release v1.7.51-alpha install hardening

This commit is contained in:
archipelago 2026-05-01 05:02:39 -04:00
parent e376fec825
commit 63a33de229
16 changed files with 536 additions and 133 deletions

2
core/Cargo.lock generated
View File

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

View File

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

View File

@ -405,6 +405,9 @@ pub(super) async fn get_containers_for_app(package_id: &str) -> Result<Vec<Strin
pub(super) fn get_data_dirs_for_app(package_id: &str) -> Vec<String> {
let base = "/var/lib/archipelago";
match package_id {
"bitcoin" | "bitcoin-core" | "bitcoin-knots" => {
vec![format!("{}/bitcoin", base), format!("{}/bitcoin-ui", base)]
}
"mempool" | "mempool-web" => vec![
format!("{}/mempool", base),
format!("{}/mysql-mempool", base),

View File

@ -10,6 +10,7 @@ use super::progress::parse_pull_progress;
use super::validation::validate_app_id;
use crate::api::rpc::RpcHandler;
use crate::data_model::InstallPhase;
use crate::update::host_sudo;
use anyhow::{Context, Result};
use tokio::io::{AsyncBufReadExt, BufReader};
use tracing::{debug, info, warn};
@ -138,14 +139,20 @@ async fn inject_bitcoin_rpc_auth_into_running_container(container: &str, auth_b6
.await;
match reload {
Ok(o) if o.status.success() => {
info!("Injected Bitcoin RPC auth into {} (post-start, cp+SIGHUP)", container);
info!(
"Injected Bitcoin RPC auth into {} (post-start, cp+SIGHUP)",
container
);
}
Ok(o) => warn!(
"Patched nginx.conf in {} but SIGHUP failed: {}",
container,
String::from_utf8_lossy(&o.stderr)
),
Err(e) => warn!("Patched nginx.conf in {} but SIGHUP errored: {}", container, e),
Err(e) => warn!(
"Patched nginx.conf in {} but SIGHUP errored: {}",
container, e
),
}
}
@ -230,6 +237,11 @@ impl RpcHandler {
check_install_deps(package_id, &deps)?;
log_optional_dep_info(package_id, &deps);
check_bitcoin_implementation_conflict(package_id).await?;
let repaired_bitcoin_conf = if matches!(package_id, "bitcoin" | "bitcoin-core" | "bitcoin-knots") {
ensure_bitcoin_rpc_bindings().await?
} else {
false
};
// Check if container already exists
let check_output = tokio::process::Command::new("podman")
@ -270,7 +282,35 @@ impl RpcHandler {
.trim()
.to_string();
if state != "running" {
if state == "running" && repaired_bitcoin_conf {
info!(
"Restarting existing container {} after bitcoin.conf RPC repair",
package_id
);
let restart_output = tokio::process::Command::new("podman")
.args(["restart", package_id])
.output()
.await
.context("Failed to restart existing container after bitcoin.conf repair")?;
if !restart_output.status.success() {
let stderr = String::from_utf8_lossy(&restart_output.stderr);
install_log(&format!(
"INSTALL ADOPT FAIL: {} - restart after RPC repair failed: {}",
package_id, stderr
))
.await;
return Err(anyhow::anyhow!(
"Container {} exists but failed to restart after RPC repair: {}",
package_id,
stderr
));
}
let _ = tokio::process::Command::new("podman")
.args(["restart", "archy-bitcoin-ui"])
.output()
.await;
wait_for_adopted_container(package_id, package_id).await?;
} else if state != "running" {
// Start the stopped/exited container
info!("Starting existing container {} (was {})", package_id, state);
let start_output = tokio::process::Command::new("podman")
@ -291,6 +331,8 @@ impl RpcHandler {
stderr
));
}
wait_for_adopted_container(package_id, package_id).await?;
}
install_log(&format!(
@ -575,7 +617,12 @@ impl RpcHandler {
// during its initial reorg/indexing phase.
let cpu_capped = !matches!(
package_id,
"bitcoin" | "bitcoin-core" | "bitcoin-knots" | "electrumx" | "electrs" | "mempool-electrs"
"bitcoin"
| "bitcoin-core"
| "bitcoin-knots"
| "electrumx"
| "electrs"
| "mempool-electrs"
);
if cpu_capped {
run_args.push("--cpus=2");
@ -1092,12 +1139,14 @@ impl RpcHandler {
// user" and skip. Matches the lnd.conf behavior below.
match tokio::fs::metadata(&conf_path).await {
Ok(_) => {
info!("bitcoin.conf already exists, skipping write");
ensure_bitcoin_rpc_bindings().await?;
info!("bitcoin.conf already exists, ensured RPC bind settings");
return Ok(());
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
Err(_) => {
info!("bitcoin.conf path inaccessible (container-owned data dir), skipping write");
ensure_bitcoin_rpc_bindings().await?;
info!("bitcoin.conf path inaccessible, ensured RPC bind settings via host helper");
return Ok(());
}
}
@ -1512,56 +1561,60 @@ autopilot.active=false\n",
// 2. Post-start: `podman exec` into the running container to patch
// nginx.conf and reload. Authoritative for both paths — runs
// regardless of how the image was built.
let bitcoin_rpc_auth_b64: Option<String> =
if matches!(package_id, "bitcoin" | "bitcoin-core" | "bitcoin-knots") {
let (rpc_user, rpc_pass) = crate::bitcoin_rpc::bitcoin_rpc_credentials().await;
use base64::Engine;
let auth_b64 = base64::engine::general_purpose::STANDARD
.encode(format!("{}:{}", rpc_user, rpc_pass));
for dir in [
"/opt/archipelago/docker/bitcoin-ui",
"/home/archipelago/archy/docker/bitcoin-ui",
] {
let conf_path = format!("{}/nginx.conf", dir);
match tokio::fs::read_to_string(&conf_path).await {
Ok(content) => {
let updated = content
.replace("__BITCOIN_RPC_AUTH__", &auth_b64)
.lines()
.map(|line| {
if line.contains("proxy_set_header Authorization")
&& line.contains("Basic")
{
format!(
" proxy_set_header Authorization \"Basic {}\";",
auth_b64
)
} else {
line.to_string()
}
})
.collect::<Vec<_>>()
.join("\n");
if let Err(e) =
tokio::fs::write(&conf_path, format!("{}\n", updated)).await
{
warn!("Failed to write {} with injected RPC auth: {}", conf_path, e);
} else {
info!("Injected Bitcoin RPC auth into {} (build-time)", conf_path);
}
let bitcoin_rpc_auth_b64: Option<String> = if matches!(
package_id,
"bitcoin" | "bitcoin-core" | "bitcoin-knots"
) {
let (rpc_user, rpc_pass) = crate::bitcoin_rpc::bitcoin_rpc_credentials().await;
use base64::Engine;
let auth_b64 = base64::engine::general_purpose::STANDARD
.encode(format!("{}:{}", rpc_user, rpc_pass));
for dir in [
"/opt/archipelago/docker/bitcoin-ui",
"/home/archipelago/archy/docker/bitcoin-ui",
] {
let conf_path = format!("{}/nginx.conf", dir);
match tokio::fs::read_to_string(&conf_path).await {
Ok(content) => {
let updated = content
.replace("__BITCOIN_RPC_AUTH__", &auth_b64)
.lines()
.map(|line| {
if line.contains("proxy_set_header Authorization")
&& line.contains("Basic")
{
format!(
" proxy_set_header Authorization \"Basic {}\";",
auth_b64
)
} else {
line.to_string()
}
})
.collect::<Vec<_>>()
.join("\n");
if let Err(e) = tokio::fs::write(&conf_path, format!("{}\n", updated)).await
{
warn!(
"Failed to write {} with injected RPC auth: {}",
conf_path, e
);
} else {
info!("Injected Bitcoin RPC auth into {} (build-time)", conf_path);
}
Err(_) => {
debug!(
}
Err(_) => {
debug!(
"No build-time nginx.conf at {} (will patch running container after start)",
conf_path
);
}
}
}
Some(auth_b64)
} else {
None
};
}
Some(auth_b64)
} else {
None
};
// Build and start companion UI containers for headless services.
// All UIs proxy to localhost (backend :5678 or bitcoin :8332) so they need --network=host.
@ -1958,6 +2011,97 @@ async fn resolve_host_gateway() -> String {
"--add-host=host.containers.internal:10.0.2.2".to_string()
}
async fn wait_for_adopted_container(package_id: &str, container_name: &str) -> Result<()> {
for _ in 0..12u32 {
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
let status = tokio::process::Command::new("podman")
.args(["inspect", container_name, "--format", "{{.State.Status}}"])
.output()
.await;
let Ok(output) = status else {
continue;
};
let state = String::from_utf8_lossy(&output.stdout).trim().to_string();
match state.as_str() {
"running" => return Ok(()),
"exited" | "dead" => {
let logs = tokio::process::Command::new("podman")
.args(["logs", "--tail", "40", container_name])
.output()
.await;
let log_output = logs
.map(|o| {
let stdout = String::from_utf8_lossy(&o.stdout);
let stderr = String::from_utf8_lossy(&o.stderr);
format!("{}{}", stdout, stderr)
})
.unwrap_or_default();
install_log(&format!(
"INSTALL ADOPT CRASH: {} - existing container {} exited. Logs:\n{}",
package_id,
container_name,
&log_output.chars().take(1000).collect::<String>()
))
.await;
return Err(anyhow::anyhow!(
"Existing container {} exited after start. Logs: {}",
container_name,
log_output.chars().take(500).collect::<String>()
));
}
_ => {}
}
}
install_log(&format!(
"INSTALL ADOPT TIMEOUT: {} - existing container {} did not stay running",
package_id, container_name
))
.await;
Err(anyhow::anyhow!(
"Existing container {} did not reach running state within 60s",
container_name
))
}
async fn ensure_bitcoin_rpc_bindings() -> Result<bool> {
let script = r#"
set -eu
conf=/var/lib/archipelago/bitcoin/bitcoin.conf
[ -f "$conf" ] || exit 0
changed=0
ensure_line() {
line="$1"
key="${line%%=*}"
if ! grep -q "^${key}=" "$conf"; then
printf '%s\n' "$line" >> "$conf"
changed=1
fi
}
ensure_line server=1
ensure_line rpcbind=0.0.0.0
ensure_line rpcallowip=0.0.0.0/0
ensure_line rpcport=8332
ensure_line listen=1
[ "$changed" -eq 0 ] && exit 0
exit 2
"#;
let status = host_sudo(&["sh", "-lc", script])
.await
.context("ensure bitcoin.conf RPC bind settings")?;
match status.code() {
Some(0) => Ok(false),
Some(2) => {
install_log("INSTALL REPAIR: bitcoin.conf RPC bind settings added").await;
Ok(true)
}
_ => Err(anyhow::anyhow!(
"bitcoin.conf RPC repair helper exited with {}",
status
)),
}
}
fn should_try_orchestrator_install(package_id: &str, orchestrator_available: bool) -> bool {
orchestrator_available && uses_orchestrator_install_flow(package_id)
}
@ -2092,7 +2236,10 @@ mod tests {
#[test]
fn install_aliases_map_to_manifest_app_ids() {
assert_eq!(orchestrator_install_app_id("bitcoin-knots"), "bitcoin-knots");
assert_eq!(
orchestrator_install_app_id("bitcoin-knots"),
"bitcoin-knots"
);
assert_eq!(orchestrator_install_app_id("bitcoin-core"), "bitcoin-core");
assert_eq!(orchestrator_install_app_id("electrs"), "electrumx");
assert_eq!(orchestrator_install_app_id("mempool-electrs"), "electrumx");

View File

@ -48,6 +48,12 @@ async fn adopt_stack_if_exists(
.await;
}
}
let existing: Vec<&str> = all_containers
.iter()
.copied()
.filter(|container| names.iter().any(|n| n == container))
.collect();
wait_for_stack_containers(stack_name, &existing, 60).await?;
install_log(&format!(
"INSTALL ADOPT OK: {} — started existing containers",
@ -61,6 +67,107 @@ async fn adopt_stack_if_exists(
})))
}
async fn run_required_stack_command(
stack_name: &str,
label: &str,
cmd: &mut tokio::process::Command,
) -> Result<()> {
let output = cmd
.output()
.await
.with_context(|| format!("{}: failed to run {}", stack_name, label))?;
if output.status.success() {
return Ok(());
}
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
let msg = format!("{} failed: {}{}", label, stdout, stderr);
install_log(&format!("INSTALL FAIL: {} - {}", stack_name, msg.trim())).await;
Err(anyhow::anyhow!("{} {}", stack_name, msg.trim()))
}
async fn wait_for_stack_containers(
stack_name: &str,
containers: &[&str],
timeout_secs: u64,
) -> Result<()> {
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(timeout_secs);
loop {
let mut pending = Vec::new();
for container in containers {
let status = tokio::process::Command::new("podman")
.args(["inspect", container, "--format", "{{.State.Status}}"])
.output()
.await;
match status {
Ok(output) if output.status.success() => {
let state = String::from_utf8_lossy(&output.stdout).trim().to_string();
match state.as_str() {
"running" => {}
"exited" | "dead" => {
let logs = stack_container_logs(container, 40).await;
install_log(&format!(
"INSTALL CRASH: {} - container {} exited. Logs:\n{}",
stack_name,
container,
logs.chars().take(1000).collect::<String>()
))
.await;
return Err(anyhow::anyhow!(
"{} container {} exited after install. Logs: {}",
stack_name,
container,
logs.chars().take(500).collect::<String>()
));
}
other => pending.push(format!("{}={}", container, other)),
}
}
Ok(output) => {
pending.push(format!(
"{}=missing({})",
container,
String::from_utf8_lossy(&output.stderr).trim()
));
}
Err(e) => pending.push(format!("{}=inspect-error({})", container, e)),
}
}
if pending.is_empty() {
return Ok(());
}
if std::time::Instant::now() >= deadline {
install_log(&format!(
"INSTALL TIMEOUT: {} - containers not running: {}",
stack_name,
pending.join(", ")
))
.await;
return Err(anyhow::anyhow!(
"{} containers did not reach running state: {}",
stack_name,
pending.join(", ")
));
}
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
}
}
async fn stack_container_logs(container: &str, lines: u32) -> String {
tokio::process::Command::new("podman")
.args(["logs", "--tail", &lines.to_string(), container])
.output()
.await
.map(|o| {
let stdout = String::from_utf8_lossy(&o.stdout);
let stderr = String::from_utf8_lossy(&o.stderr);
format!("{}{}", stdout, stderr)
})
.unwrap_or_default()
}
async fn install_stack_via_orchestrator(
handler: &RpcHandler,
stack_name: &str,
@ -208,10 +315,12 @@ impl RpcHandler {
.await;
let n_images = images.len() as u64;
for (i, img) in images.iter().enumerate() {
self.set_install_progress("immich", i as u64, n_images).await;
self.set_install_progress("immich", i as u64, n_images)
.await;
pull_image_with_retry(img).await?;
}
self.set_install_progress("immich", n_images, n_images).await;
self.set_install_progress("immich", n_images, n_images)
.await;
self.set_install_phase("immich", InstallPhase::CreatingContainer)
.await;
@ -626,10 +735,12 @@ impl RpcHandler {
.await;
let n_images = images.len() as u64;
for (i, img) in images.iter().enumerate() {
self.set_install_progress("mempool", i as u64, n_images).await;
self.set_install_progress("mempool", i as u64, n_images)
.await;
pull_image_with_retry(img).await?;
}
self.set_install_progress("mempool", n_images, n_images).await;
self.set_install_progress("mempool", n_images, n_images)
.await;
self.set_install_phase("mempool", InstallPhase::CreatingContainer)
.await;
@ -901,7 +1012,8 @@ impl RpcHandler {
let minio_pass = super::config::read_or_generate_secret("indeedhub-minio-password").await;
// 1. Postgres
let _ = tokio::process::Command::new("podman")
let mut postgres_cmd = tokio::process::Command::new("podman");
postgres_cmd
.args([
"run",
"-d",
@ -923,12 +1035,12 @@ impl RpcHandler {
"indeedhub-postgres-data:/var/lib/postgresql/data",
&format!("{}/postgres:16.13-alpine", registry),
])
.env("TMPDIR", &user_tmp)
.status()
.await;
.env("TMPDIR", &user_tmp);
run_required_stack_command("indeedhub", "create postgres", &mut postgres_cmd).await?;
// 2. Redis
let _ = tokio::process::Command::new("podman")
let mut redis_cmd = tokio::process::Command::new("podman");
redis_cmd
.args([
"run",
"-d",
@ -944,12 +1056,12 @@ impl RpcHandler {
"indeedhub-redis-data:/data",
&format!("{}/redis:7.4.8-alpine", registry),
])
.env("TMPDIR", &user_tmp)
.status()
.await;
.env("TMPDIR", &user_tmp);
run_required_stack_command("indeedhub", "create redis", &mut redis_cmd).await?;
// 3. MinIO
let _ = tokio::process::Command::new("podman")
let mut minio_cmd = tokio::process::Command::new("podman");
minio_cmd
.args([
"run",
"-d",
@ -971,12 +1083,12 @@ impl RpcHandler {
"server",
"/data",
])
.env("TMPDIR", &user_tmp)
.status()
.await;
.env("TMPDIR", &user_tmp);
run_required_stack_command("indeedhub", "create minio", &mut minio_cmd).await?;
// 4. Nostr relay
let _ = tokio::process::Command::new("podman")
let mut relay_cmd = tokio::process::Command::new("podman");
relay_cmd
.args([
"run",
"-d",
@ -992,12 +1104,12 @@ impl RpcHandler {
"indeedhub-relay-data:/usr/src/app/db",
&format!("{}/nostr-rs-relay:0.9.0", registry),
])
.env("TMPDIR", &user_tmp)
.status()
.await;
.env("TMPDIR", &user_tmp);
run_required_stack_command("indeedhub", "create relay", &mut relay_cmd).await?;
// 5. API
let _ = tokio::process::Command::new("podman")
let mut api_cmd = tokio::process::Command::new("podman");
api_cmd
.args([
"run",
"-d",
@ -1049,12 +1161,12 @@ impl RpcHandler {
"ENVIRONMENT=production",
&format!("{}/indeedhub-api:1.0.0", registry),
])
.env("TMPDIR", &user_tmp)
.status()
.await;
.env("TMPDIR", &user_tmp);
run_required_stack_command("indeedhub", "create api", &mut api_cmd).await?;
// 6. FFmpeg worker
let _ = tokio::process::Command::new("podman")
let mut ffmpeg_cmd = tokio::process::Command::new("podman");
ffmpeg_cmd
.args([
"run",
"-d",
@ -1096,15 +1208,15 @@ impl RpcHandler {
"AES_MASTER_SECRET=0123456789abcdef0123456789abcdef",
&format!("{}/indeedhub-ffmpeg:1.0.0", registry),
])
.env("TMPDIR", &user_tmp)
.status()
.await;
.env("TMPDIR", &user_tmp);
run_required_stack_command("indeedhub", "create ffmpeg worker", &mut ffmpeg_cmd).await?;
// Wait for backend services to start
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
// 7. Frontend (nginx)
let run = tokio::process::Command::new("podman")
let mut frontend_cmd = tokio::process::Command::new("podman");
frontend_cmd
.args([
"run",
"-d",
@ -1118,15 +1230,23 @@ impl RpcHandler {
"7778:7777",
&format!("{}/indeedhub:1.0.0", registry),
])
.env("TMPDIR", &user_tmp)
.output()
.await
.context("Failed to create indeedhub container")?;
.env("TMPDIR", &user_tmp);
run_required_stack_command("indeedhub", "create frontend", &mut frontend_cmd).await?;
if !run.status.success() {
let err = String::from_utf8_lossy(&run.stderr);
return Err(anyhow::anyhow!("IndeedHub frontend failed: {}", err));
}
wait_for_stack_containers(
"indeedhub",
&[
"indeedhub-postgres",
"indeedhub-redis",
"indeedhub-minio",
"indeedhub-relay",
"indeedhub-api",
"indeedhub-ffmpeg",
"indeedhub",
],
60,
)
.await?;
// Phase: WaitingHealthy → PostInstall → clear. The actual readiness
// gate is the package scanner's next sweep; this just gives the UI a
@ -1136,7 +1256,8 @@ impl RpcHandler {
.await;
self.set_install_phase("indeedhub", InstallPhase::PostInstall)
.await;
self.set_install_phase("indeedhub", InstallPhase::Done).await;
self.set_install_phase("indeedhub", InstallPhase::Done)
.await;
self.clear_install_progress("indeedhub").await;
install_log("INSTALL OK: indeedhub stack").await;

View File

@ -56,6 +56,11 @@ pub async fn ensure_doctor_installed() {
Ok(false) => debug!("Nginx already has /api/app-catalog block"),
Err(e) => warn!("Nginx bootstrap failed (non-fatal): {:#}", e),
}
match run_bitcoin_rpc_repair().await {
Ok(true) => info!("Repaired Bitcoin RPC bind settings and restarted Bitcoin containers"),
Ok(false) => debug!("Bitcoin RPC bind settings already usable"),
Err(e) => warn!("Bitcoin RPC repair failed (non-fatal): {:#}", e),
}
}
async fn run_runtime_assets() -> Result<bool> {
@ -169,6 +174,54 @@ fn path_dot(path: &Path) -> String {
p.to_string_lossy().to_string()
}
async fn run_bitcoin_rpc_repair() -> Result<bool> {
// Older installs can have a container-owned bitcoin.conf with only rpcauth
// and printtoconsole. In that state bitcoind is healthy internally, but the
// host-network bitcoin-ui proxy to 127.0.0.1:8332 gets connection resets.
// Repair it at startup so OTA fixes existing nodes without a manual
// uninstall/reinstall.
let script = r#"
set -eu
conf=/var/lib/archipelago/bitcoin/bitcoin.conf
[ -f "$conf" ] || exit 0
changed=0
ensure_line() {
line="$1"
key="${line%%=*}"
if ! grep -q "^${key}=" "$conf"; then
printf '%s\n' "$line" >> "$conf"
changed=1
fi
}
ensure_line server=1
ensure_line rpcbind=0.0.0.0
ensure_line rpcallowip=0.0.0.0/0
ensure_line rpcport=8332
ensure_line listen=1
[ "$changed" -eq 0 ] && exit 0
exit 2
"#;
let status = host_sudo(&["sh", "-lc", script])
.await
.context("repair bitcoin.conf RPC bind settings")?;
match status.code() {
Some(0) => Ok(false),
Some(2) => {
for name in ["bitcoin-knots", "bitcoin-core", "archy-bitcoin-ui"] {
let _ = tokio::process::Command::new("podman")
.args(["restart", name])
.status()
.await;
}
Ok(true)
}
_ => {
warn!("Bitcoin RPC repair helper exited with {}", status);
Ok(false)
}
}
}
async fn run() -> Result<bool> {
// Dev-box guard: on contributors' laptops `/home/archipelago/archy` is
// typically a symlink into the git checkout, and writing through it

View File

@ -11,6 +11,8 @@ use tokio::fs;
use tracing::{debug, info};
const REGISTRY_FILE: &str = "config/registries.json";
const OVH_REGISTRY_URL: &str = "146.59.87.168:3000/lfg2025";
const TX1138_REGISTRY_URL: &str = "git.tx1138.com/lfg2025";
/// A single container registry.
#[derive(Debug, Clone, Serialize, Deserialize)]
@ -44,14 +46,14 @@ impl Default for RegistryConfig {
Self {
registries: vec![
Registry {
url: "146.59.87.168:3000/lfg2025".to_string(),
url: OVH_REGISTRY_URL.to_string(),
name: "Server 1 (OVH)".to_string(),
tls_verify: false,
enabled: true,
priority: 0,
},
Registry {
url: "git.tx1138.com/lfg2025".to_string(),
url: TX1138_REGISTRY_URL.to_string(),
name: "Server 2 (tx1138)".to_string(),
tls_verify: true,
enabled: true,
@ -139,6 +141,19 @@ pub async fn load_registries(data_dir: &Path) -> Result<RegistryConfig> {
changed = true;
}
}
let before_order: Vec<(String, bool, u32)> = config
.registries
.iter()
.map(|r| (r.url.clone(), r.enabled, r.priority))
.collect();
force_ovh_registry_primary(&mut config);
changed = changed
|| before_order
!= config
.registries
.iter()
.map(|r| (r.url.clone(), r.enabled, r.priority))
.collect::<Vec<_>>();
if changed {
// Persist so the next load doesn't have to re-merge.
let _ = save_registries(data_dir, &config).await;
@ -146,6 +161,37 @@ pub async fn load_registries(data_dir: &Path) -> Result<RegistryConfig> {
Ok(config)
}
fn force_ovh_registry_primary(config: &mut RegistryConfig) {
let defaults = RegistryConfig::default();
for def in defaults.registries {
if !config.registries.iter().any(|r| r.url == def.url) {
config.registries.push(def);
}
}
for registry in config.registries.iter_mut() {
match registry.url.as_str() {
OVH_REGISTRY_URL => {
registry.name = "Server 1 (OVH)".to_string();
registry.tls_verify = false;
registry.enabled = true;
registry.priority = 0;
}
TX1138_REGISTRY_URL => {
registry.name = "Server 2 (tx1138)".to_string();
registry.tls_verify = true;
registry.enabled = true;
registry.priority = 10;
}
_ => {
if registry.priority <= 10 {
registry.priority = registry.priority.saturating_add(20);
}
}
}
}
}
/// Save registry config to disk.
pub async fn save_registries(data_dir: &Path, config: &RegistryConfig) -> Result<()> {
let dir = data_dir.join("config");

View File

@ -163,12 +163,40 @@ pub async fn load_mirrors(data_dir: &Path) -> Result<Vec<UpdateMirror>> {
changed = true;
}
}
let before_order: Vec<String> = list.iter().map(|m| m.url.clone()).collect();
force_ovh_update_primary(&mut list);
changed = changed || before_order != list.iter().map(|m| m.url.clone()).collect::<Vec<_>>();
if changed {
let _ = save_mirrors(data_dir, &list).await;
}
Ok(list)
}
fn force_ovh_update_primary(list: &mut Vec<UpdateMirror>) {
let defaults = default_mirrors();
for def in &defaults {
if !list.iter().any(|m| m.url == def.url) {
list.push(def.clone());
}
}
for mirror in list.iter_mut() {
if mirror.url == DEFAULT_UPDATE_MANIFEST_URL {
mirror.label = "Server 1 (OVH)".to_string();
} else if mirror.url == DEFAULT_SECONDARY_MIRROR_URL {
mirror.label = "Server 2 (tx1138)".to_string();
}
}
list.sort_by_key(|m| {
if m.url == DEFAULT_UPDATE_MANIFEST_URL {
0
} else if m.url == DEFAULT_SECONDARY_MIRROR_URL {
1
} else {
2
}
});
}
pub async fn save_mirrors(data_dir: &Path, mirrors: &[UpdateMirror]) -> Result<()> {
fs::create_dir_all(data_dir)
.await

View File

@ -1,12 +1,12 @@
{
"name": "neode-ui",
"version": "1.7.50-alpha",
"version": "1.7.51-alpha",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "neode-ui",
"version": "1.7.50-alpha",
"version": "1.7.51-alpha",
"dependencies": {
"@types/dompurify": "^3.0.5",
"@vue-leaflet/vue-leaflet": "^0.10.1",

View File

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

View File

@ -83,9 +83,11 @@ const launchableApps = computed<KioskApp[]>(() => {
const pkgs = store.data?.['package-data'] || {}
const apps: KioskApp[] = []
// App URL mappings use nginx proxy paths for local apps
// App URL mappings. Bitcoin UI uses its direct host-network port; loading it
// through /app/bitcoin-ui/ can render a blank shell because its assets are
// rooted at /.
const urlMap: Record<string, string> = {
'bitcoin-knots': '/app/bitcoin-ui/',
'bitcoin-knots': 'http://' + window.location.hostname + ':8334',
'lnd': '/app/lnd/',
'mempool': '/app/mempool/',
'btcpay-server': '/app/btcpay/',

View File

@ -55,9 +55,6 @@ export const PROXY_APPS: Record<string, string> = {
/** Nginx proxy paths -- used on HTTPS to avoid mixed content (HTTPS parent + HTTP port iframe).
* On HTTP, direct port access is used instead (faster, no proxy). */
export const HTTPS_PROXY_PATHS: Record<string, string> = {
'bitcoin-knots': '/app/bitcoin-ui/',
'bitcoin-core': '/app/bitcoin-ui/',
'bitcoin-ui': '/app/bitcoin-ui/',
'lnd': '/app/lnd/',
'electrumx': '/app/electrumx/',
'electrs': '/app/electrumx/',
@ -137,9 +134,11 @@ export function resolveAppUrl(id: string, routeQueryPath?: string): string {
const ext = EXTERNAL_URLS[id]
if (ext) return ext
// Bitcoin apps always go through nginx proxy so browser basic-auth prompts never appear.
// Bitcoin UI is a host-network companion on :8334. Do not launch it via
// /app/bitcoin-ui/: the static UI is built for root and renders a blank
// shell when proxied under a path prefix on some nodes.
if (id === 'bitcoin-knots' || id === 'bitcoin-core' || id === 'bitcoin-ui') {
return window.location.protocol + '//' + window.location.hostname + '/app/bitcoin-ui/'
return 'http://' + window.location.hostname + ':8334'
}
// HTTPS pages cannot embed plain HTTP port origins (mixed-content).

View File

@ -1,27 +1,29 @@
{
"version": "1.7.50-alpha",
"version": "1.7.51-alpha",
"release_date": "2026-05-01",
"changelog": [
"OTA now carries app manifests, scripts, Docker contexts, and doctor units inside the frontend payload so older nodes restore /opt/archipelago runtime assets after updating.",
"Startup bootstrap promotes the embedded runtime payload into /opt/archipelago and reloads the doctor timer without requiring rsync.",
"Update checks now continue past stale mirrors so nodes can discover a newer manifest from the next configured mirror."
"Install success now requires adopted containers and IndeedHub stack containers to stay running; failed starts surface logs instead of disappearing from My Apps.",
"Bitcoin uninstall removes the shared Bitcoin data/UI directories when data is not preserved, preventing stale partial installs from being adopted as success.",
"Bitcoin RPC bind settings are repaired on startup and before adopting existing Bitcoin containers, fixing older nodes where bitcoin-ui showed endless getblockchaininfo/502.",
"Bitcoin Core/Knots launch the Bitcoin UI on direct port 8334 instead of the /app/bitcoin-ui path proxy.",
"Nodes force OVH as the primary update mirror and app registry on next startup, with tx1138 retained as fallback."
],
"components": [
{
"name": "archipelago",
"current_version": "1.7.49-alpha",
"new_version": "1.7.50-alpha",
"download_url": "https://git.tx1138.com/lfg2025/archy/raw/branch/main/releases/v1.7.50-alpha/archipelago",
"sha256": "494f71d58608787a4f485ec6e295057e70045e905d745837d1cd6089e484197d",
"size_bytes": 41779280
"current_version": "1.7.50-alpha",
"new_version": "1.7.51-alpha",
"download_url": "https://git.tx1138.com/lfg2025/archy/raw/branch/main/releases/v1.7.51-alpha/archipelago",
"sha256": "f761e659d661f0a83cd3a67a086bb2279398bc05e50ee3c52e769e52d11e476c",
"size_bytes": 41637536
},
{
"name": "archipelago-frontend-1.7.50-alpha.tar.gz",
"current_version": "1.7.49-alpha",
"new_version": "1.7.50-alpha",
"download_url": "https://git.tx1138.com/lfg2025/archy/raw/branch/main/releases/v1.7.50-alpha/archipelago-frontend-1.7.50-alpha.tar.gz",
"sha256": "6684d5f1083fd2d1db236fcc4a94efee7986ab54602b3231b031f3e9f7dec361",
"size_bytes": 165153241
"name": "archipelago-frontend-1.7.51-alpha.tar.gz",
"current_version": "1.7.50-alpha",
"new_version": "1.7.51-alpha",
"download_url": "https://git.tx1138.com/lfg2025/archy/raw/branch/main/releases/v1.7.51-alpha/archipelago-frontend-1.7.51-alpha.tar.gz",
"sha256": "3403f4e38202bf56c53407dd62e66899693ee73252bf203475715532ac6ae326",
"size_bytes": 165155462
}
]
}

View File

@ -1,27 +1,29 @@
{
"version": "1.7.50-alpha",
"version": "1.7.51-alpha",
"release_date": "2026-05-01",
"changelog": [
"OTA now carries app manifests, scripts, Docker contexts, and doctor units inside the frontend payload so older nodes restore /opt/archipelago runtime assets after updating.",
"Startup bootstrap promotes the embedded runtime payload into /opt/archipelago and reloads the doctor timer without requiring rsync.",
"Update checks now continue past stale mirrors so nodes can discover a newer manifest from the next configured mirror."
"Install success now requires adopted containers and IndeedHub stack containers to stay running; failed starts surface logs instead of disappearing from My Apps.",
"Bitcoin uninstall removes the shared Bitcoin data/UI directories when data is not preserved, preventing stale partial installs from being adopted as success.",
"Bitcoin RPC bind settings are repaired on startup and before adopting existing Bitcoin containers, fixing older nodes where bitcoin-ui showed endless getblockchaininfo/502.",
"Bitcoin Core/Knots launch the Bitcoin UI on direct port 8334 instead of the /app/bitcoin-ui path proxy.",
"Nodes force OVH as the primary update mirror and app registry on next startup, with tx1138 retained as fallback."
],
"components": [
{
"name": "archipelago",
"current_version": "1.7.49-alpha",
"new_version": "1.7.50-alpha",
"download_url": "https://git.tx1138.com/lfg2025/archy/raw/branch/main/releases/v1.7.50-alpha/archipelago",
"sha256": "494f71d58608787a4f485ec6e295057e70045e905d745837d1cd6089e484197d",
"size_bytes": 41779280
"current_version": "1.7.50-alpha",
"new_version": "1.7.51-alpha",
"download_url": "https://git.tx1138.com/lfg2025/archy/raw/branch/main/releases/v1.7.51-alpha/archipelago",
"sha256": "f761e659d661f0a83cd3a67a086bb2279398bc05e50ee3c52e769e52d11e476c",
"size_bytes": 41637536
},
{
"name": "archipelago-frontend-1.7.50-alpha.tar.gz",
"current_version": "1.7.49-alpha",
"new_version": "1.7.50-alpha",
"download_url": "https://git.tx1138.com/lfg2025/archy/raw/branch/main/releases/v1.7.50-alpha/archipelago-frontend-1.7.50-alpha.tar.gz",
"sha256": "6684d5f1083fd2d1db236fcc4a94efee7986ab54602b3231b031f3e9f7dec361",
"size_bytes": 165153241
"name": "archipelago-frontend-1.7.51-alpha.tar.gz",
"current_version": "1.7.50-alpha",
"new_version": "1.7.51-alpha",
"download_url": "https://git.tx1138.com/lfg2025/archy/raw/branch/main/releases/v1.7.51-alpha/archipelago-frontend-1.7.51-alpha.tar.gz",
"sha256": "3403f4e38202bf56c53407dd62e66899693ee73252bf203475715532ac6ae326",
"size_bytes": 165155462
}
]
}

Binary file not shown.