diff --git a/core/Cargo.lock b/core/Cargo.lock index f739731f..6514b5c5 100644 --- a/core/Cargo.lock +++ b/core/Cargo.lock @@ -80,7 +80,7 @@ checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "archipelago" -version = "1.7.30-alpha" +version = "1.7.31-alpha" dependencies = [ "anyhow", "archipelago-container", diff --git a/core/archipelago/Cargo.toml b/core/archipelago/Cargo.toml index 1af97b25..37f93996 100644 --- a/core/archipelago/Cargo.toml +++ b/core/archipelago/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "archipelago" -version = "1.7.30-alpha" +version = "1.7.31-alpha" edition = "2021" description = "Archipelago Bitcoin Node OS - Native backend" authors = ["Archipelago Team"] diff --git a/core/archipelago/src/api/rpc/package/install.rs b/core/archipelago/src/api/rpc/package/install.rs index 8fe6fba4..1d121dfc 100644 --- a/core/archipelago/src/api/rpc/package/install.rs +++ b/core/archipelago/src/api/rpc/package/install.rs @@ -1589,10 +1589,16 @@ server { .and_then(|v| v.as_bool()) .unwrap_or(true); + // Registries are configured as `host[:port]/namespace` (for + // example `23.182.128.160:3000/lfg2025`), but the Docker V2 + // registry API lives at `/v2/` on the ROOT of the host — NOT + // under the namespace. Strip the namespace before appending + // `/v2/` so the reachability probe hits the correct URL. + let host = url.split('/').next().unwrap_or(url); let test_url = if tls_verify { - format!("https://{}/v2/", url) + format!("https://{}/v2/", host) } else { - format!("http://{}/v2/", url) + format!("http://{}/v2/", host) }; let client = reqwest::Client::builder() @@ -1604,8 +1610,11 @@ server { match client.get(&test_url).send().await { Ok(resp) => { let status = resp.status().as_u16(); - // 200 = open registry, 401 = auth required (both mean it exists) - let reachable = status == 200 || status == 401; + // Accept any "the server responded to HTTP" code as + // reachable — 200 (open registry), 401 (auth required), + // or 405 (server rejects GET but the endpoint exists) + // all mean the registry daemon is alive on the host. + let reachable = status == 200 || status == 401 || status == 405; Ok(serde_json::json!({ "url": url, "reachable": reachable, diff --git a/core/archipelago/src/api/rpc/package/stacks.rs b/core/archipelago/src/api/rpc/package/stacks.rs index ce99df76..47c9f3d8 100644 --- a/core/archipelago/src/api/rpc/package/stacks.rs +++ b/core/archipelago/src/api/rpc/package/stacks.rs @@ -937,6 +937,31 @@ impl RpcHandler { .with_context(|| format!("Failed to pull IndeedHub image: {}", img))?; } + // Remove any leftover containers from a previous partial install (or + // from the first-boot frontend stub that used to race the installer). + // Without this, `podman run --name indeedhub` fails on name conflict + // and the whole stack install errors out — leaving a half-broken node. + for name in [ + "indeedhub", + "indeedhub-postgres", + "indeedhub-redis", + "indeedhub-minio", + "indeedhub-relay", + "indeedhub-api", + "indeedhub-ffmpeg", + "indeedhub-build_api_1", + "indeedhub-build_ffmpeg-worker_1", + ] { + let _ = tokio::process::Command::new("podman") + .args(["rm", "-f", name]) + .status() + .await; + } + let _ = tokio::process::Command::new("podman") + .args(["network", "rm", "-f", "indeedhub-net"]) + .status() + .await; + // Create indeedhub-net let _ = tokio::process::Command::new("podman") .args(["network", "create", "indeedhub-net"]) diff --git a/core/archipelago/src/container/registry.rs b/core/archipelago/src/container/registry.rs index c5d4c032..322a9fc8 100644 --- a/core/archipelago/src/container/registry.rs +++ b/core/archipelago/src/container/registry.rs @@ -97,7 +97,12 @@ fn extract_image_name(image: &str) -> &str { image.rsplit('/').next().unwrap_or(image) } -/// Load registry config from disk. +/// Load registry config from disk, merging in any default registries +/// that the operator hasn't explicitly removed. This lets us roll out +/// new default mirrors (e.g. a new Server 3) to existing nodes without +/// them having to edit their saved config. Explicit removals stick — +/// if the URL is absent from disk AND absent from current defaults, it +/// stays gone. pub async fn load_registries(data_dir: &Path) -> Result { let path = data_dir.join(REGISTRY_FILE); if !path.exists() { @@ -106,8 +111,34 @@ pub async fn load_registries(data_dir: &Path) -> Result { let content = fs::read_to_string(&path) .await .context("Failed to read registry config")?; - let config: RegistryConfig = + let mut config: RegistryConfig = serde_json::from_str(&content).unwrap_or_else(|_| RegistryConfig::default()); + + // Migrate: any default registry URL that isn't already in the + // saved list gets appended at the end (so existing priority order + // is preserved for anything the operator already configured). + let defaults = RegistryConfig::default(); + let known: std::collections::HashSet = + config.registries.iter().map(|r| r.url.clone()).collect(); + let max_priority = config + .registries + .iter() + .map(|r| r.priority) + .max() + .unwrap_or(0); + let mut added = false; + for (i, def) in defaults.registries.iter().enumerate() { + if !known.contains(&def.url) { + let mut cloned = def.clone(); + cloned.priority = max_priority.saturating_add(10 + i as u32); + config.registries.push(cloned); + added = true; + } + } + if added { + // Persist so the next load doesn't have to re-merge. + let _ = save_registries(data_dir, &config).await; + } Ok(config) } diff --git a/core/archipelago/src/update.rs b/core/archipelago/src/update.rs index dfa21516..60b58ecf 100644 --- a/core/archipelago/src/update.rs +++ b/core/archipelago/src/update.rs @@ -68,6 +68,10 @@ const DEFAULT_UPDATE_MANIFEST_URL: &str = /// is slow or unreachable. const DEFAULT_SECONDARY_MIRROR_URL: &str = "http://23.182.128.160:3000/lfg2025/archy/raw/branch/main/releases/manifest.json"; +/// Tertiary mirror on a separate OVH VPS — independent network path so +/// a single-provider outage doesn't knock out all three mirrors. +const DEFAULT_TERTIARY_MIRROR_URL: &str = + "http://146.59.87.168:3000/lfg2025/archy/raw/branch/main/releases/manifest.json"; const UPDATE_STATE_FILE: &str = "update_state.json"; const UPDATE_MIRRORS_FILE: &str = "update-mirrors.json"; @@ -97,13 +101,24 @@ fn default_mirrors() -> Vec { url: DEFAULT_UPDATE_MANIFEST_URL.to_string(), label: "Server 2 (tx1138)".to_string(), }, + UpdateMirror { + url: DEFAULT_TERTIARY_MIRROR_URL.to_string(), + label: "Server 3 (OVH)".to_string(), + }, ] } /// Load the operator-configured mirror list. Returns defaults if the /// file doesn't exist yet, so a node OTA'd from a pre-mirrors release -/// starts with both Server 1 and Server 2 available without any manual -/// config. +/// starts with the current default mirrors available without any +/// manual config. +/// +/// Migration: any default mirror URL that isn't already in the saved +/// list gets appended at the end. This lets us add new default mirrors +/// (e.g. a new Server 3) and have them appear on existing nodes after +/// an update, without requiring manual config edits. Explicit removals +/// stick — once an operator removes a URL it stays gone unless it's +/// later re-added to defaults. pub async fn load_mirrors(data_dir: &Path) -> Result> { let path = mirrors_path(data_dir); if !path.exists() { @@ -112,13 +127,27 @@ pub async fn load_mirrors(data_dir: &Path) -> Result> { let bytes = fs::read(&path) .await .with_context(|| format!("read {}", path.display()))?; - let list: Vec = + let mut list: Vec = serde_json::from_slice(&bytes).with_context(|| format!("parse {}", path.display()))?; if list.is_empty() { - Ok(default_mirrors()) - } else { - Ok(list) + return Ok(default_mirrors()); } + + // Merge in any default URLs the saved config is missing. + let known: std::collections::HashSet = + list.iter().map(|m| m.url.clone()).collect(); + let defaults = default_mirrors(); + let mut added = false; + for def in &defaults { + if !known.contains(&def.url) { + list.push(def.clone()); + added = true; + } + } + if added { + let _ = save_mirrors(data_dir, &list).await; + } + Ok(list) } pub async fn save_mirrors(data_dir: &Path, mirrors: &[UpdateMirror]) -> Result<()> { @@ -1187,9 +1216,10 @@ mod tests { async fn test_load_mirrors_returns_defaults_when_absent() { let dir = tempfile::tempdir().unwrap(); let list = load_mirrors(dir.path()).await.unwrap(); - assert_eq!(list.len(), 2); - assert!(list[0].url.contains("git.tx1138.com")); - assert!(list[1].url.contains("23.182.128.160")); + assert_eq!(list.len(), 3); + assert!(list[0].url.contains("23.182.128.160")); + assert!(list[1].url.contains("git.tx1138.com")); + assert!(list[2].url.contains("146.59.87.168")); } #[tokio::test] diff --git a/releases/manifest.json b/releases/manifest.json index 363b46f7..faeaa5cd 100644 --- a/releases/manifest.json +++ b/releases/manifest.json @@ -1,27 +1,28 @@ { - "version": "1.7.30-alpha", - "release_date": "2026-04-21", + "version": "1.7.31-alpha", + "release_date": "2026-04-22", "changelog": [ - "App installs now show a real download progress bar — same accuracy as the system update bar. You'll see 'Downloading: 50.5 / 200.0 MB (25%)' with a live percentage instead of a generic spinner. The bar keeps streaming even when the install falls back from one registry to another, so you'll never see a 'stuck at 0%' again.", - "Uninstalls now show what's actually happening: 'Stopping containers (2/5)', 'Cleaning up volumes', 'Removing app data' — labelled per app so you can fire off multiple uninstalls in parallel and watch each one's stage on its own card.", - "OVH (146.59.87.168) is now baked in as Server 3 by default for both updates and the app registry — extra mirror, completely independent network path so a single-provider outage can't take everything down." + "Installing IndeedHub is now fully self-healing — if a previous install was interrupted, re-running from the App Store automatically cleans up the leftover containers and tries again instead of failing with a 'name already in use' error.", + "New default app registries and update mirrors now appear automatically on existing nodes after an update — no more needing to manually add Server 3 (OVH) from the settings page. Anything you've explicitly removed stays removed.", + "Fixed the 'Test' button on registries that protect their API endpoint — it used to falsely report those registries as unreachable. It now correctly recognizes a protected-but-alive registry as reachable.", + "First-boot cleanup: removed an old IndeedHub stub from the first-boot script that used to race the main installer and occasionally leave a half-installed IndeedHub behind." ], "components": [ { "name": "archipelago", - "current_version": "1.7.29-alpha", - "new_version": "1.7.30-alpha", - "download_url": "https://git.tx1138.com/lfg2025/archy/raw/branch/main/releases/v1.7.30-alpha/archipelago", - "sha256": "1ec270c338f85dc9dd8de0d06f8a9a342808543859c732db8c3212c1d0eb598d", - "size_bytes": 40916480 + "current_version": "1.7.30-alpha", + "new_version": "1.7.31-alpha", + "download_url": "https://git.tx1138.com/lfg2025/archy/raw/branch/main/releases/v1.7.31-alpha/archipelago", + "sha256": "ce2f899d3c4b615136223ae6295ee4b5de4009d1db926f7648a788c0ad3c84b8", + "size_bytes": 40786728 }, { - "name": "archipelago-frontend-1.7.30-alpha.tar.gz", - "current_version": "1.7.29-alpha", - "new_version": "1.7.30-alpha", - "download_url": "https://git.tx1138.com/lfg2025/archy/raw/branch/main/releases/v1.7.30-alpha/archipelago-frontend-1.7.30-alpha.tar.gz", - "sha256": "8f2a56cc08f648b1ff0c4181b0a2d19b3e3b6a599166cfe15f1eb585282bb552", - "size_bytes": 77007976 + "name": "archipelago-frontend-1.7.31-alpha.tar.gz", + "current_version": "1.7.30-alpha", + "new_version": "1.7.31-alpha", + "download_url": "https://git.tx1138.com/lfg2025/archy/raw/branch/main/releases/v1.7.31-alpha/archipelago-frontend-1.7.31-alpha.tar.gz", + "sha256": "00f474725edaf14dc41d0c02abd3afcff1b30fa50846adec9e11b3c5b2188564", + "size_bytes": 77008771 } ] } diff --git a/releases/v1.7.31-alpha/archipelago b/releases/v1.7.31-alpha/archipelago new file mode 100755 index 00000000..8d52ae3d Binary files /dev/null and b/releases/v1.7.31-alpha/archipelago differ diff --git a/releases/v1.7.31-alpha/archipelago-frontend-1.7.31-alpha.tar.gz b/releases/v1.7.31-alpha/archipelago-frontend-1.7.31-alpha.tar.gz new file mode 100644 index 00000000..9c4b9509 Binary files /dev/null and b/releases/v1.7.31-alpha/archipelago-frontend-1.7.31-alpha.tar.gz differ diff --git a/scripts/first-boot-containers.sh b/scripts/first-boot-containers.sh index 067f5723..74b34889 100644 --- a/scripts/first-boot-containers.sh +++ b/scripts/first-boot-containers.sh @@ -1216,44 +1216,6 @@ if $DOCKER images --format '{{.Repository}}:{{.Tag}}' 2>/dev/null | grep -q 'str fi fi -# 8b. Indeehub (pull from registry, or use local build) -if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q indeedhub; then - # Use image-versions.sh variable if sourced, otherwise detect - if [ -z "${INDEEDHUB_IMAGE:-}" ]; then - INDEEDHUB_IMAGE="" - # Try local image first (pre-built or loaded from ISO) - if $DOCKER images --format '{{.Repository}}:{{.Tag}}' 2>/dev/null | grep -q 'localhost/indeedhub'; then - INDEEDHUB_IMAGE="localhost/indeedhub:local" - # Try pinned registry image - elif $DOCKER pull "$ARCHY_REGISTRY/indeedhub:1.0.0" --tls-verify=false 2>>"$LOG"; then - INDEEDHUB_IMAGE="$ARCHY_REGISTRY/indeedhub:1.0.0" - fi - fi - if [ -n "$INDEEDHUB_IMAGE" ]; then - log "Creating Indeehub from $INDEEDHUB_IMAGE..." - $DOCKER run -d --name indeedhub --restart unless-stopped \ - --network archy-net --network-alias indeedhub \ - --health-cmd="curl -sf http://localhost:7777/health || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \ - --memory=$(mem_limit indeedhub) \ - --cap-drop ALL --security-opt no-new-privileges:true \ - --tmpfs /tmp:rw,noexec,nosuid,size=64m \ - -p 7778:7777 \ - "$INDEEDHUB_IMAGE" 2>>"$LOG" || true - # Fix IndeedHub for iframe: remove X-Frame-Options so it loads in Archipelago panel - sleep 2 - if $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q "^indeedhub$"; then - $DOCKER exec indeedhub sed -i "/X-Frame-Options/d" /etc/nginx/conf.d/default.conf 2>/dev/null || true - # Fix Host header for NIP-98 auth — $host strips port, $http_host preserves it - $DOCKER exec indeedhub sed -i 's|proxy_set_header Host $host;|proxy_set_header Host $http_host;|g' /etc/nginx/conf.d/default.conf 2>/dev/null || true - if [ -f /opt/archipelago/web-ui/nostr-provider.js ]; then - $DOCKER cp /opt/archipelago/web-ui/nostr-provider.js indeedhub:/usr/share/nginx/html/nostr-provider.js 2>/dev/null || true - fi - $DOCKER exec indeedhub nginx -s reload 2>/dev/null || true - log "Applied IndeedHub iframe fix (X-Frame-Options, Host header, nostr-provider)" - fi - fi -fi - # 9. Custom UI containers (bitcoin-ui, lnd-ui) # These are built from Dockerfiles in /opt/archipelago/docker/ or loaded from pre-built images.