diff --git a/core/Cargo.lock b/core/Cargo.lock index a6fdf67a..4e959b45 100644 --- a/core/Cargo.lock +++ b/core/Cargo.lock @@ -80,7 +80,7 @@ checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "archipelago" -version = "1.7.50-alpha" +version = "1.7.51-alpha" dependencies = [ "anyhow", "archipelago-container", diff --git a/core/archipelago/Cargo.toml b/core/archipelago/Cargo.toml index 6b641a98..f5d5fb39 100644 --- a/core/archipelago/Cargo.toml +++ b/core/archipelago/Cargo.toml @@ -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"] diff --git a/core/archipelago/src/api/rpc/package/config.rs b/core/archipelago/src/api/rpc/package/config.rs index 92169665..a3917acc 100644 --- a/core/archipelago/src/api/rpc/package/config.rs +++ b/core/archipelago/src/api/rpc/package/config.rs @@ -405,6 +405,9 @@ pub(super) async fn get_containers_for_app(package_id: &str) -> Result Vec { 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), diff --git a/core/archipelago/src/api/rpc/package/install.rs b/core/archipelago/src/api/rpc/package/install.rs index c38e32f3..dccbc281 100644 --- a/core/archipelago/src/api/rpc/package/install.rs +++ b/core/archipelago/src/api/rpc/package/install.rs @@ -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 = - 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::>() - .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 = 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::>() + .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::() + )) + .await; + return Err(anyhow::anyhow!( + "Existing container {} exited after start. Logs: {}", + container_name, + log_output.chars().take(500).collect::() + )); + } + _ => {} + } + } + + 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 { + 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"); diff --git a/core/archipelago/src/api/rpc/package/stacks.rs b/core/archipelago/src/api/rpc/package/stacks.rs index 5c4b9400..074abebf 100644 --- a/core/archipelago/src/api/rpc/package/stacks.rs +++ b/core/archipelago/src/api/rpc/package/stacks.rs @@ -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::() + )) + .await; + return Err(anyhow::anyhow!( + "{} container {} exited after install. Logs: {}", + stack_name, + container, + logs.chars().take(500).collect::() + )); + } + 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; diff --git a/core/archipelago/src/bootstrap.rs b/core/archipelago/src/bootstrap.rs index 15fe5a86..f67e713d 100644 --- a/core/archipelago/src/bootstrap.rs +++ b/core/archipelago/src/bootstrap.rs @@ -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 { @@ -169,6 +174,54 @@ fn path_dot(path: &Path) -> String { p.to_string_lossy().to_string() } +async fn run_bitcoin_rpc_repair() -> Result { + // 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 { // Dev-box guard: on contributors' laptops `/home/archipelago/archy` is // typically a symlink into the git checkout, and writing through it diff --git a/core/archipelago/src/container/registry.rs b/core/archipelago/src/container/registry.rs index 92968148..ed8f5597 100644 --- a/core/archipelago/src/container/registry.rs +++ b/core/archipelago/src/container/registry.rs @@ -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 { 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::>(); 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 { 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"); diff --git a/core/archipelago/src/update.rs b/core/archipelago/src/update.rs index abce2909..4b727ee2 100644 --- a/core/archipelago/src/update.rs +++ b/core/archipelago/src/update.rs @@ -163,12 +163,40 @@ pub async fn load_mirrors(data_dir: &Path) -> Result> { changed = true; } } + let before_order: Vec = 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::>(); if changed { let _ = save_mirrors(data_dir, &list).await; } Ok(list) } +fn force_ovh_update_primary(list: &mut Vec) { + 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 diff --git a/neode-ui/package-lock.json b/neode-ui/package-lock.json index 886aaf78..d83f4cc1 100644 --- a/neode-ui/package-lock.json +++ b/neode-ui/package-lock.json @@ -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", diff --git a/neode-ui/package.json b/neode-ui/package.json index c22ea42d..8d8dfc47 100644 --- a/neode-ui/package.json +++ b/neode-ui/package.json @@ -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", diff --git a/neode-ui/src/views/Kiosk.vue b/neode-ui/src/views/Kiosk.vue index 3553e065..83087568 100644 --- a/neode-ui/src/views/Kiosk.vue +++ b/neode-ui/src/views/Kiosk.vue @@ -83,9 +83,11 @@ const launchableApps = computed(() => { 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 = { - 'bitcoin-knots': '/app/bitcoin-ui/', + 'bitcoin-knots': 'http://' + window.location.hostname + ':8334', 'lnd': '/app/lnd/', 'mempool': '/app/mempool/', 'btcpay-server': '/app/btcpay/', diff --git a/neode-ui/src/views/appSession/appSessionConfig.ts b/neode-ui/src/views/appSession/appSessionConfig.ts index 5d88d015..bf40ac85 100644 --- a/neode-ui/src/views/appSession/appSessionConfig.ts +++ b/neode-ui/src/views/appSession/appSessionConfig.ts @@ -55,9 +55,6 @@ export const PROXY_APPS: Record = { /** 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 = { - '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). diff --git a/release-manifest.json b/release-manifest.json index 3c511fe8..d481f510 100644 --- a/release-manifest.json +++ b/release-manifest.json @@ -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 } ] } diff --git a/releases/manifest.json b/releases/manifest.json index 3c511fe8..d481f510 100644 --- a/releases/manifest.json +++ b/releases/manifest.json @@ -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 } ] } diff --git a/releases/v1.7.51-alpha/archipelago b/releases/v1.7.51-alpha/archipelago new file mode 100755 index 00000000..26fd97c9 Binary files /dev/null and b/releases/v1.7.51-alpha/archipelago differ diff --git a/releases/v1.7.51-alpha/archipelago-frontend-1.7.51-alpha.tar.gz b/releases/v1.7.51-alpha/archipelago-frontend-1.7.51-alpha.tar.gz new file mode 100644 index 00000000..f8822922 Binary files /dev/null and b/releases/v1.7.51-alpha/archipelago-frontend-1.7.51-alpha.tar.gz differ