diff --git a/core/archipelago/src/api/rpc/package/install.rs b/core/archipelago/src/api/rpc/package/install.rs index 814e09a9..686d640b 100644 --- a/core/archipelago/src/api/rpc/package/install.rs +++ b/core/archipelago/src/api/rpc/package/install.rs @@ -13,6 +13,24 @@ use anyhow::{Context, Result}; use tokio::io::{AsyncBufReadExt, BufReader}; use tracing::{debug, info, warn}; +const INSTALL_LOG: &str = "/var/log/archipelago-container-installs.log"; + +/// Append a timestamped line to the persistent install log. +async fn install_log(msg: &str) { + let ts = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC"); + let line = format!("[{}] {}\n", ts, msg); + let _ = tokio::fs::OpenOptions::new() + .create(true) + .append(true) + .open(INSTALL_LOG) + .await + .and_then(|mut f| { + use tokio::io::AsyncWriteExt; + Box::pin(async move { f.write_all(line.as_bytes()).await }) + }) + .await; +} + impl RpcHandler { /// Install a package from a Docker image. /// Security: Image verification, resource limits, network isolation. @@ -33,12 +51,14 @@ impl RpcHandler { .and_then(|v| v.as_str()) .ok_or_else(|| anyhow::anyhow!("Missing dockerImage"))?; + install_log(&format!("INSTALL START: {} (image: {})", package_id, docker_image)).await; debug!( "Installing package {} from image {}", package_id, docker_image ); if !is_valid_docker_image(docker_image) { + install_log(&format!("INSTALL FAIL: {} — invalid image format", package_id)).await; return Err(anyhow::anyhow!("Invalid Docker image format")); } @@ -80,9 +100,11 @@ impl RpcHandler { } // Pull or verify image + install_log(&format!("INSTALL PULL: {} — pulling image {}", package_id, docker_image)).await; let has_local_fallback = self .pull_or_verify_image(package_id, docker_image) .await?; + install_log(&format!("INSTALL PULL OK: {} — image ready (local_fallback={})", package_id, has_local_fallback)).await; // Normalize container name for legacy aliases let container_name = match package_id { @@ -173,6 +195,11 @@ impl RpcHandler { self.write_bitcoin_conf(&rpc_user, &rpc_pass).await; } + // Pre-install: lnd.conf with Bitcoin RPC credentials + if package_id == "lnd" { + self.write_lnd_conf(&rpc_user, &rpc_pass).await; + } + // Pre-install: SearXNG settings.yml (required or container exits immediately) if package_id == "searxng" { let searx_dir = "/var/lib/archipelago/searxng"; @@ -244,6 +271,7 @@ impl RpcHandler { if !run_output.status.success() { let stderr = String::from_utf8_lossy(&run_output.stderr); + install_log(&format!("INSTALL FAIL: {} — podman run failed: {}", package_id, stderr)).await; // Rollback: remove partially created container let _ = tokio::process::Command::new("podman") .args(["rm", "-f", container_name]) @@ -305,6 +333,8 @@ impl RpcHandler { // Post-install hooks — await completion before returning success self.run_post_install_hooks(package_id).await; + install_log(&format!("INSTALL OK: {} (container: {})", package_id, &container_id[..12.min(container_id.len())])).await; + Ok(serde_json::json!({ "success": true, "package_id": package_id, @@ -544,6 +574,47 @@ printtoconsole=1\n", info!("Created bitcoin.conf with rpcauth (no plaintext credentials)"); } + /// Write LND config file with Bitcoin RPC credentials. + async fn write_lnd_conf(&self, rpc_user: &str, rpc_pass: &str) { + let lnd_dir = "/var/lib/archipelago/lnd"; + let conf_path = format!("{}/lnd.conf", lnd_dir); + + // Don't overwrite existing config (user may have customized it) + if tokio::fs::try_exists(&conf_path).await.unwrap_or(false) { + info!("lnd.conf already exists, skipping write"); + return; + } + + let lnd_conf = format!( + "\ +[Application Options]\n\ +listen=0.0.0.0:9735\n\ +rpclisten=0.0.0.0:10009\n\ +restlisten=0.0.0.0:8080\n\ +debuglevel=info\n\ +noseedbackup=true\n\ +\n\ +[Bitcoin]\n\ +bitcoin.mainnet=true\n\ +bitcoin.node=bitcoind\n\ +\n\ +[Bitcoind]\n\ +bitcoind.rpchost=bitcoin-knots:8332\n\ +bitcoind.rpcuser={user}\n\ +bitcoind.rpcpass={pass}\n\ +bitcoind.rpcpolling=true\n\ +bitcoind.estimatemode=ECONOMICAL\n\ +\n\ +[autopilot]\n\ +autopilot.active=false\n", + user = rpc_user, + pass = rpc_pass, + ); + let _ = tokio::fs::create_dir_all(lnd_dir).await; + let _ = tokio::fs::write(&conf_path, lnd_conf).await; + info!("Created lnd.conf with Bitcoin RPC credentials"); + } + /// Run post-install hooks (Nextcloud trusted domains, Bitcoin UI container). /// Critical hooks (credential setup, config) are awaited; UI container builds are background. async fn run_post_install_hooks(&self, package_id: &str) { @@ -647,54 +718,105 @@ printtoconsole=1\n", info!("Nextcloud trusted domains configured for {}", host_ip); } - // Build and start companion UI containers for headless services - let ui_builds: Vec<(&str, &str, &str, &str)> = match package_id { + // Pre-build: inject Bitcoin RPC auth into bitcoin-ui nginx.conf + 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); + if let Ok(content) = tokio::fs::read_to_string(&conf_path).await { + if content.contains("__BITCOIN_RPC_AUTH__") { + let updated = content.replace("__BITCOIN_RPC_AUTH__", &auth_b64); + let _ = tokio::fs::write(&conf_path, updated).await; + info!("Injected Bitcoin RPC auth into {}", conf_path); + } + } + } + } + + // Build and start companion UI containers for headless services. + // All UIs proxy to localhost (backend :5678 or bitcoin :8332) so they need --network=host. + let ui_builds: Vec<(&str, &str, &str)> = match package_id { "bitcoin" | "bitcoin-core" | "bitcoin-knots" => { - vec![("bitcoin-ui", "/opt/archipelago/docker/bitcoin-ui", "localhost/bitcoin-ui", "8334:80")] + vec![("archy-bitcoin-ui", "/opt/archipelago/docker/bitcoin-ui", "bitcoin-ui")] } "lnd" => { - vec![("archy-lnd-ui", "/opt/archipelago/docker/lnd-ui", "localhost/lnd-ui", "8081:80")] + vec![("archy-lnd-ui", "/opt/archipelago/docker/lnd-ui", "lnd-ui")] } "electrumx" | "electrs" | "mempool-electrs" => { - vec![("archy-electrs-ui", "/opt/archipelago/docker/electrs-ui", "localhost/electrs-ui", "50002:80")] + vec![("archy-electrs-ui", "/opt/archipelago/docker/electrs-ui", "electrs-ui")] } _ => vec![], }; - for (name, ui_dir, image, port) in ui_builds { + for (name, ui_dir, image_base) in ui_builds { let name = name.to_string(); let ui_dir = ui_dir.to_string(); - let image = image.to_string(); - let port = port.to_string(); + let image_base = image_base.to_string(); + let registry = "80.71.235.15:3000/archipelago"; + let registry_image = format!("{}/{}:latest", registry, image_base); + let local_image = format!("localhost/{}:latest", image_base); tokio::spawn(async move { - if !std::path::Path::new(&ui_dir).exists() { - info!("UI source not found at {}, skipping", ui_dir); - return; - } - info!("Building UI container {} from {}", name, ui_dir); - let _ = tokio::process::Command::new("podman") - .args(["build", "-t", &image, &ui_dir]) - .output() - .await; + // Remove existing container let _ = tokio::process::Command::new("podman") .args(["rm", "-f", &name]) .output() .await; - let _ = tokio::process::Command::new("podman") + + // Try registry image first, fall back to local build + let image = { + let pull = tokio::process::Command::new("podman") + .args(["pull", ®istry_image]) + .output() + .await; + if pull.map_or(false, |o| o.status.success()) { + info!("Pulled {} UI from registry", name); + registry_image.clone() + } else if std::path::Path::new(&ui_dir).exists() { + info!("Registry pull failed, building {} from {}", name, ui_dir); + let build = tokio::process::Command::new("podman") + .args(["build", "-t", &local_image, &ui_dir]) + .output() + .await; + match build { + Ok(o) if o.status.success() => local_image, + Ok(o) => { + warn!("Failed to build {}: {}", name, + String::from_utf8_lossy(&o.stderr)); + return; + } + Err(e) => { + warn!("Failed to build {}: {}", name, e); + return; + } + } + } else { + warn!("No registry image or source for {} — skipping", name); + return; + } + }; + + // Run with --network=host (UIs proxy to localhost backend/bitcoin) + let run = tokio::process::Command::new("podman") .args([ "run", "-d", "--name", &name, "--restart=unless-stopped", - "--network=archy-net", + "--network=host", "--cap-drop=ALL", "--cap-add=NET_BIND_SERVICE", "--memory=64m", - "-p", &port, - &format!("{}:latest", image), + &image, ]) .output() .await; - info!("{} UI container started on port {}", name, port); + match run { + Ok(o) if o.status.success() => info!("{} UI container started (host network)", name), + Ok(o) => warn!("Failed to start {}: {}", name, String::from_utf8_lossy(&o.stderr)), + Err(e) => warn!("Failed to start {}: {}", name, e), + } }); } } diff --git a/core/archipelago/src/container/docker_packages.rs b/core/archipelago/src/container/docker_packages.rs index f331207d..8176861a 100644 --- a/core/archipelago/src/container/docker_packages.rs +++ b/core/archipelago/src/container/docker_packages.rs @@ -50,6 +50,12 @@ impl DockerPackageScanner { "immich_redis", "endurain-db", "nextcloud-db", + "indeedhub-api", + "indeedhub-ffmpeg", + "indeedhub-postgres", + "indeedhub-redis", + "indeedhub-minio", + "indeedhub-relay", "indeedhub-build_api_1", "indeedhub-build_postgres_1", "indeedhub-build_redis_1", diff --git a/core/archipelago/src/session.rs b/core/archipelago/src/session.rs index 3db051af..554b0e8c 100644 --- a/core/archipelago/src/session.rs +++ b/core/archipelago/src/session.rs @@ -5,9 +5,12 @@ use std::collections::HashMap; use std::path::{Path, PathBuf}; use std::sync::Arc; use std::time::{SystemTime, UNIX_EPOCH}; -use tokio::sync::RwLock; +use tokio::sync::{OnceCell, RwLock}; use zeroize::Zeroize; +/// Cached remember secret — loaded once, never regenerated within a process. +static REMEMBER_SECRET: OnceCell> = OnceCell::const_new(); + type HmacSha256 = Hmac; const FULL_SESSION_TTL: u64 = 86400; // 24 hours of inactivity @@ -391,21 +394,23 @@ impl SessionStore { } pub async fn load_or_create_remember_secret() -> Vec { - // Try existing secret file first - if let Ok(secret) = tokio::fs::read(REMEMBER_SECRET_FILE).await { - if secret.len() == 32 { - return secret; + REMEMBER_SECRET.get_or_init(|| async { + // Try existing secret file first + if let Ok(secret) = tokio::fs::read(REMEMBER_SECRET_FILE).await { + if secret.len() == 32 { + return secret; + } } - } - // Generate a cryptographically random 32-byte secret on first boot - let mut secret = [0u8; 32]; - rand::rngs::OsRng.fill_bytes(&mut secret); - // Ensure parent directory exists - if let Some(parent) = std::path::Path::new(REMEMBER_SECRET_FILE).parent() { - let _ = tokio::fs::create_dir_all(parent).await; - } - let _ = tokio::fs::write(REMEMBER_SECRET_FILE, &secret).await; - secret.to_vec() + // Generate a cryptographically random 32-byte secret on first boot + let mut secret = [0u8; 32]; + rand::rngs::OsRng.fill_bytes(&mut secret); + // Ensure parent directory exists + if let Some(parent) = std::path::Path::new(REMEMBER_SECRET_FILE).parent() { + let _ = tokio::fs::create_dir_all(parent).await; + } + let _ = tokio::fs::write(REMEMBER_SECRET_FILE, &secret).await; + secret.to_vec() + }).await.clone() } } diff --git a/docker/bitcoin-ui/Dockerfile b/docker/bitcoin-ui/Dockerfile index 86183d48..0caa225e 100644 --- a/docker/bitcoin-ui/Dockerfile +++ b/docker/bitcoin-ui/Dockerfile @@ -1,7 +1,6 @@ FROM 80.71.235.15:3000/archipelago/nginx:1.27.4-alpine COPY index.html /usr/share/nginx/html/ COPY 50x.html /usr/share/nginx/html/ -COPY assets/ /usr/share/nginx/html/assets/ COPY nginx.conf /etc/nginx/conf.d/default.conf EXPOSE 8334 CMD ["nginx", "-g", "daemon off;"] diff --git a/docker/electrs-ui/Dockerfile b/docker/electrs-ui/Dockerfile index 7ef91e49..464fb7a6 100644 --- a/docker/electrs-ui/Dockerfile +++ b/docker/electrs-ui/Dockerfile @@ -1,7 +1,7 @@ FROM 80.71.235.15:3000/archipelago/nginx:1.27.4-alpine COPY index.html /usr/share/nginx/html/ COPY 50x.html /usr/share/nginx/html/ -COPY assets/ /usr/share/nginx/html/assets/ +COPY qrcode.js /usr/share/nginx/html/ COPY nginx.conf /etc/nginx/conf.d/default.conf -EXPOSE 80 +EXPOSE 50002 CMD ["nginx", "-g", "daemon off;"] diff --git a/image-recipe/configs/archipelago-tor-helper.path b/image-recipe/configs/archipelago-tor-helper.path index 939d6fe2..e8201314 100644 --- a/image-recipe/configs/archipelago-tor-helper.path +++ b/image-recipe/configs/archipelago-tor-helper.path @@ -1,6 +1,5 @@ [Unit] Description=Watch for Archipelago Tor management actions -After=tor.service [Path] PathExists=/var/lib/archipelago/tor-config/tor-action diff --git a/neode-ui/src/views/OnboardingSeedGenerate.vue b/neode-ui/src/views/OnboardingSeedGenerate.vue index 98bed44e..ce16d028 100644 --- a/neode-ui/src/views/OnboardingSeedGenerate.vue +++ b/neode-ui/src/views/OnboardingSeedGenerate.vue @@ -36,14 +36,14 @@
-
+
- {{ i + 1 }}. - {{ word }} + {{ i + 1 }}. + {{ word }}
diff --git a/neode-ui/src/views/apps/appsConfig.ts b/neode-ui/src/views/apps/appsConfig.ts index 22ecc73e..83a9ee40 100644 --- a/neode-ui/src/views/apps/appsConfig.ts +++ b/neode-ui/src/views/apps/appsConfig.ts @@ -12,6 +12,7 @@ export const SERVICE_NAMES = new Set([ 'mysql-mempool', 'mempool-api', 'archy-mempool-web', 'archy-bitcoin-ui', 'archy-lnd-ui', 'archy-electrs-ui', 'indeedhub-postgres', 'indeedhub-redis', 'indeedhub-minio', + 'indeedhub-api', 'indeedhub-ffmpeg', 'indeedhub-relay', 'indeedhub-build_api_1', 'indeedhub-build_ffmpeg-worker_1', 'indeedhub-build_postgres_1', 'indeedhub-build_redis_1', 'indeedhub-build_minio_1', 'indeedhub-build_minio-init_1', 'indeedhub-build_relay_1', diff --git a/scripts/image-versions.sh b/scripts/image-versions.sh index 461dc399..56b98b92 100644 --- a/scripts/image-versions.sh +++ b/scripts/image-versions.sh @@ -81,5 +81,10 @@ PENPOT_BACKEND_IMAGE="$ARCHY_REGISTRY/penpot-backend:2.4" PENPOT_EXPORTER_IMAGE="$ARCHY_REGISTRY/penpot-exporter:2.4" PENPOT_FRONTEND_IMAGE="$ARCHY_REGISTRY/penpot-frontend:2.4" +# Custom UI containers (built from docker/ dirs, pushed to registry) +BITCOIN_UI_IMAGE="$ARCHY_REGISTRY/bitcoin-ui:latest" +LND_UI_IMAGE="$ARCHY_REGISTRY/lnd-ui:latest" +ELECTRS_UI_IMAGE="$ARCHY_REGISTRY/electrs-ui:latest" + # Base images NGINX_ALPINE_IMAGE="$ARCHY_REGISTRY/nginx:1.27.4-alpine"