chore(release): stage v1.7.54-alpha
This commit is contained in:
parent
1a0d8a432c
commit
c0751e2551
@ -1,5 +1,13 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## v1.7.54-alpha (2026-05-06)
|
||||||
|
|
||||||
|
- Existing installs now self-repair nginx backend proxy locations for `/bitcoin-status` and `/api/app-catalog`, including hosts where `sites-enabled/archipelago` is a copied active file instead of a symlink.
|
||||||
|
- LND UI is consistently served on `18083` across first boot, Tor config, companion Quadlet reconciliation, OTA runtime payloads, and ISO scripts; stale companion units/images are rewritten instead of only checking service active state.
|
||||||
|
- OTA frontend tarballs now carry a clean runtime payload with updated scripts, docker UI sources, and canonical nginx config, preventing startup promotion from reintroducing stale host assets.
|
||||||
|
- Release ISO builds now support the primary HTTP app registry when bundling core images, so unbundled media includes File Browser/Cloud support instead of requiring a post-install Marketplace download.
|
||||||
|
- `.116` was live-updated with the new backend and runtime scripts; focused non-destructive lifecycle audit passes for Bitcoin Knots, LND, BTCPay, Mempool, and Grafana.
|
||||||
|
|
||||||
## v1.7.53-alpha (2026-05-05)
|
## v1.7.53-alpha (2026-05-05)
|
||||||
|
|
||||||
- Bitcoin Knots/Core config generation no longer duplicates RPC bind and port settings between `bitcoin.conf` and container command args, fixing `Unable to bind all endpoints for RPC server` startup failures.
|
- Bitcoin Knots/Core config generation no longer duplicates RPC bind and port settings between `bitcoin.conf` and container command args, fixing `Unable to bind all endpoints for RPC server` startup failures.
|
||||||
|
|||||||
@ -8,6 +8,7 @@ app:
|
|||||||
image: grafana/grafana:10.2.0
|
image: grafana/grafana:10.2.0
|
||||||
image_signature: cosign://...
|
image_signature: cosign://...
|
||||||
pull_policy: if-not-present
|
pull_policy: if-not-present
|
||||||
|
data_uid: "472:472"
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
- storage: 5Gi
|
- storage: 5Gi
|
||||||
|
|||||||
2
core/Cargo.lock
generated
2
core/Cargo.lock
generated
@ -80,7 +80,7 @@ checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "archipelago"
|
name = "archipelago"
|
||||||
version = "1.7.53-alpha"
|
version = "1.7.54-alpha"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"archipelago-container",
|
"archipelago-container",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "archipelago"
|
name = "archipelago"
|
||||||
version = "1.7.53-alpha"
|
version = "1.7.54-alpha"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "Archipelago Bitcoin Node OS - Native backend"
|
description = "Archipelago Bitcoin Node OS - Native backend"
|
||||||
authors = ["Archipelago Team"]
|
authors = ["Archipelago Team"]
|
||||||
|
|||||||
@ -192,7 +192,7 @@ pub(super) fn get_health_check_args(app_id: &str, _rpc_pass: &str) -> Vec<String
|
|||||||
("curl -sf http://localhost:8123/api/ || exit 1", "30s", "3")
|
("curl -sf http://localhost:8123/api/ || exit 1", "30s", "3")
|
||||||
}
|
}
|
||||||
"grafana" => (
|
"grafana" => (
|
||||||
"curl -sf http://localhost:3000/api/health || exit 1",
|
"test -w /var/lib/grafana && test -w /var/lib/grafana/grafana.db && curl -sf http://localhost:3000/api/health || exit 1",
|
||||||
"30s",
|
"30s",
|
||||||
"3",
|
"3",
|
||||||
),
|
),
|
||||||
@ -292,7 +292,8 @@ pub(super) fn get_memory_limit(app_id: &str) -> &'static str {
|
|||||||
"nginx-proxy-manager" => "256m",
|
"nginx-proxy-manager" => "256m",
|
||||||
// Databases
|
// Databases
|
||||||
"archy-btcpay-db" | "archy-mempool-db" | "mysql-mempool" => "512m",
|
"archy-btcpay-db" | "archy-mempool-db" | "mysql-mempool" => "512m",
|
||||||
"immich_postgres" | "penpot-postgres" => "256m",
|
"immich_postgres" => "2g",
|
||||||
|
"penpot-postgres" => "256m",
|
||||||
"immich_redis" | "penpot-valkey" => "128m",
|
"immich_redis" | "penpot-valkey" => "128m",
|
||||||
// Default
|
// Default
|
||||||
_ => "512m",
|
_ => "512m",
|
||||||
@ -428,7 +429,7 @@ pub(super) async fn get_containers_for_app(package_id: &str) -> Result<Vec<Strin
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::all_container_names;
|
use super::{all_container_names, get_health_check_args};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn bitcoin_variant_container_names_are_precise() {
|
fn bitcoin_variant_container_names_are_precise() {
|
||||||
@ -440,6 +441,19 @@ mod tests {
|
|||||||
assert!(knots.contains(&"bitcoin-knots".to_string()));
|
assert!(knots.contains(&"bitcoin-knots".to_string()));
|
||||||
assert!(!knots.contains(&"bitcoin-core".to_string()));
|
assert!(!knots.contains(&"bitcoin-core".to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn grafana_health_requires_writable_data_and_http_health() {
|
||||||
|
let args = get_health_check_args("grafana", "unused");
|
||||||
|
let health_cmd = args
|
||||||
|
.iter()
|
||||||
|
.find_map(|arg| arg.strip_prefix("--health-cmd="))
|
||||||
|
.expect("grafana should have a health command");
|
||||||
|
|
||||||
|
assert!(health_cmd.contains("test -w /var/lib/grafana"));
|
||||||
|
assert!(health_cmd.contains("test -w /var/lib/grafana/grafana.db"));
|
||||||
|
assert!(health_cmd.contains("http://localhost:3000/api/health"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get data directories to clean for an app.
|
/// Get data directories to clean for an app.
|
||||||
|
|||||||
@ -669,6 +669,9 @@ async fn do_package_start(to_start: &[String]) -> Result<()> {
|
|||||||
for name in to_start {
|
for name in to_start {
|
||||||
ensure_runtime_host_port_listener(name).await?;
|
ensure_runtime_host_port_listener(name).await?;
|
||||||
}
|
}
|
||||||
|
if to_start.iter().any(|name| name == "indeedhub") {
|
||||||
|
super::install::patch_indeedhub_nostr_provider().await;
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -826,6 +829,9 @@ async fn do_package_restart(containers: &[String]) -> Result<()> {
|
|||||||
}
|
}
|
||||||
ensure_runtime_host_port_listener(name).await?;
|
ensure_runtime_host_port_listener(name).await?;
|
||||||
}
|
}
|
||||||
|
if containers.iter().any(|name| name == "indeedhub") {
|
||||||
|
super::install::patch_indeedhub_nostr_provider().await;
|
||||||
|
}
|
||||||
if !errors.is_empty() {
|
if !errors.is_empty() {
|
||||||
return Err(anyhow::anyhow!("Restart failed: {}", errors.join("; ")));
|
return Err(anyhow::anyhow!("Restart failed: {}", errors.join("; ")));
|
||||||
}
|
}
|
||||||
@ -842,7 +848,10 @@ async fn repair_before_package_start(container_name: &str) {
|
|||||||
"btcpay-server" | "archy-nbxplorer" => repair_btcpay_dirs().await,
|
"btcpay-server" | "archy-nbxplorer" => repair_btcpay_dirs().await,
|
||||||
"indeedhub-postgres" | "indeedhub-redis" | "indeedhub-minio" | "indeedhub-relay"
|
"indeedhub-postgres" | "indeedhub-redis" | "indeedhub-minio" | "indeedhub-relay"
|
||||||
| "indeedhub-api" | "indeedhub-ffmpeg" | "indeedhub" => repair_indeedhub_network().await,
|
| "indeedhub-api" | "indeedhub-ffmpeg" | "indeedhub" => repair_indeedhub_network().await,
|
||||||
"grafana" => cleanup_stale_pasta_port("3000").await,
|
"grafana" => {
|
||||||
|
repair_grafana_dirs().await;
|
||||||
|
cleanup_stale_pasta_port("3000").await;
|
||||||
|
}
|
||||||
"gitea" => cleanup_gitea_stale_ports().await,
|
"gitea" => cleanup_gitea_stale_ports().await,
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
@ -943,6 +952,34 @@ async fn repair_btcpay_dirs() {
|
|||||||
repair_btcpay_database_password().await;
|
repair_btcpay_database_password().await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn repair_grafana_dirs() {
|
||||||
|
let _ = tokio::process::Command::new("sudo")
|
||||||
|
.args(["mkdir", "-p", "/var/lib/archipelago/grafana"])
|
||||||
|
.output()
|
||||||
|
.await;
|
||||||
|
let podman_chown = tokio::process::Command::new("podman")
|
||||||
|
.args([
|
||||||
|
"unshare",
|
||||||
|
"chown",
|
||||||
|
"-R",
|
||||||
|
"472:472",
|
||||||
|
"/var/lib/archipelago/grafana",
|
||||||
|
])
|
||||||
|
.output()
|
||||||
|
.await;
|
||||||
|
if !podman_chown.as_ref().is_ok_and(|o| o.status.success()) {
|
||||||
|
let _ = tokio::process::Command::new("sudo")
|
||||||
|
.args([
|
||||||
|
"chown",
|
||||||
|
"-R",
|
||||||
|
"100471:100471",
|
||||||
|
"/var/lib/archipelago/grafana",
|
||||||
|
])
|
||||||
|
.output()
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn repair_btcpay_database_password() {
|
async fn repair_btcpay_database_password() {
|
||||||
let Ok(db_pass) =
|
let Ok(db_pass) =
|
||||||
tokio::fs::read_to_string("/var/lib/archipelago/secrets/btcpay-db-password").await
|
tokio::fs::read_to_string("/var/lib/archipelago/secrets/btcpay-db-password").await
|
||||||
|
|||||||
@ -450,7 +450,7 @@ impl RpcHandler {
|
|||||||
"--cap-add=SETGID",
|
"--cap-add=SETGID",
|
||||||
"--cap-add=SETUID",
|
"--cap-add=SETUID",
|
||||||
"--security-opt=no-new-privileges:true",
|
"--security-opt=no-new-privileges:true",
|
||||||
"--memory=512m",
|
"--memory=2g",
|
||||||
"--pids-limit=4096",
|
"--pids-limit=4096",
|
||||||
"--health-cmd=pg_isready -U postgres || exit 1",
|
"--health-cmd=pg_isready -U postgres || exit 1",
|
||||||
"--health-interval=30s",
|
"--health-interval=30s",
|
||||||
|
|||||||
@ -8,8 +8,8 @@
|
|||||||
//!
|
//!
|
||||||
//! Two things are synced on startup:
|
//! Two things are synced on startup:
|
||||||
//! 1. Doctor artifacts (container-doctor.sh + service + timer).
|
//! 1. Doctor artifacts (container-doctor.sh + service + timer).
|
||||||
//! 2. An nginx `location /api/app-catalog` proxy block — required for
|
//! 2. Missing nginx backend proxy blocks required for frontend fetches to
|
||||||
//! the App Store catalog proxy to actually reach the backend.
|
//! reach the backend instead of the SPA fallback.
|
||||||
//!
|
//!
|
||||||
//! Idempotent: no-ops on boxes that are already in sync. All work is
|
//! Idempotent: no-ops on boxes that are already in sync. All work is
|
||||||
//! best-effort — failures are logged but never abort the backend.
|
//! best-effort — failures are logged but never abort the backend.
|
||||||
@ -31,6 +31,7 @@ const DOCTOR_SERVICE_PATH: &str = "/etc/systemd/system/archipelago-doctor.servic
|
|||||||
const DOCTOR_TIMER_PATH: &str = "/etc/systemd/system/archipelago-doctor.timer";
|
const DOCTOR_TIMER_PATH: &str = "/etc/systemd/system/archipelago-doctor.timer";
|
||||||
|
|
||||||
const NGINX_CONF_PATH: &str = "/etc/nginx/sites-available/archipelago";
|
const NGINX_CONF_PATH: &str = "/etc/nginx/sites-available/archipelago";
|
||||||
|
const NGINX_ENABLED_CONF_PATH: &str = "/etc/nginx/sites-enabled/archipelago";
|
||||||
const RUNTIME_ASSETS_DIR: &str = "/opt/archipelago/web-ui/archipelago-runtime";
|
const RUNTIME_ASSETS_DIR: &str = "/opt/archipelago/web-ui/archipelago-runtime";
|
||||||
|
|
||||||
/// Inserted into every server block of the nginx config that lacks the
|
/// Inserted into every server block of the nginx config that lacks the
|
||||||
@ -38,6 +39,8 @@ const RUNTIME_ASSETS_DIR: &str = "/opt/archipelago/web-ui/archipelago-runtime";
|
|||||||
/// image-recipe/configs/nginx-archipelago.conf.
|
/// image-recipe/configs/nginx-archipelago.conf.
|
||||||
const NGINX_APP_CATALOG_BLOCK: &str = "\n # App Store catalog proxy — backend fetches from configured registries\n # so the browser doesn't hit CORS/CSP. Without this block nginx falls\n # through to the SPA index.html and the frontend gets HTML back instead\n # of JSON.\n location /api/app-catalog {\n proxy_pass http://127.0.0.1:5678;\n proxy_http_version 1.1;\n proxy_set_header Host $host;\n proxy_set_header X-Real-IP $remote_addr;\n proxy_set_header Cookie $http_cookie;\n proxy_connect_timeout 15s;\n proxy_read_timeout 30s;\n proxy_send_timeout 15s;\n error_page 502 503 = @backend_unavailable;\n error_page 504 = @backend_timeout;\n }\n\n";
|
const NGINX_APP_CATALOG_BLOCK: &str = "\n # App Store catalog proxy — backend fetches from configured registries\n # so the browser doesn't hit CORS/CSP. Without this block nginx falls\n # through to the SPA index.html and the frontend gets HTML back instead\n # of JSON.\n location /api/app-catalog {\n proxy_pass http://127.0.0.1:5678;\n proxy_http_version 1.1;\n proxy_set_header Host $host;\n proxy_set_header X-Real-IP $remote_addr;\n proxy_set_header Cookie $http_cookie;\n proxy_connect_timeout 15s;\n proxy_read_timeout 30s;\n proxy_send_timeout 15s;\n error_page 502 503 = @backend_unavailable;\n error_page 504 = @backend_timeout;\n }\n\n";
|
||||||
|
|
||||||
|
const NGINX_BITCOIN_STATUS_BLOCK: &str = "\n location /bitcoin-status {\n proxy_pass http://127.0.0.1:5678/bitcoin-status;\n proxy_http_version 1.1;\n proxy_set_header Host $host;\n proxy_connect_timeout 10s;\n proxy_read_timeout 10s;\n proxy_send_timeout 5s;\n error_page 502 503 = @backend_unavailable;\n error_page 504 = @backend_timeout;\n }\n";
|
||||||
|
|
||||||
/// Entry point called from main startup. Never returns an error to the caller —
|
/// Entry point called from main startup. Never returns an error to the caller —
|
||||||
/// failing to bootstrap host artifacts must not prevent the backend from serving.
|
/// failing to bootstrap host artifacts must not prevent the backend from serving.
|
||||||
pub async fn ensure_doctor_installed() {
|
pub async fn ensure_doctor_installed() {
|
||||||
@ -57,8 +60,8 @@ pub async fn ensure_doctor_installed() {
|
|||||||
Err(e) => warn!("Doctor bootstrap failed (non-fatal): {:#}", e),
|
Err(e) => warn!("Doctor bootstrap failed (non-fatal): {:#}", e),
|
||||||
}
|
}
|
||||||
match run_nginx().await {
|
match run_nginx().await {
|
||||||
Ok(true) => info!("Patched nginx config to proxy /api/app-catalog"),
|
Ok(true) => info!("Patched nginx config to proxy missing backend endpoints"),
|
||||||
Ok(false) => debug!("Nginx already has /api/app-catalog block"),
|
Ok(false) => debug!("Nginx backend endpoint proxy blocks already present"),
|
||||||
Err(e) => warn!("Nginx bootstrap failed (non-fatal): {:#}", e),
|
Err(e) => warn!("Nginx bootstrap failed (non-fatal): {:#}", e),
|
||||||
}
|
}
|
||||||
match run_bitcoin_rpc_repair().await {
|
match run_bitcoin_rpc_repair().await {
|
||||||
@ -444,13 +447,10 @@ async fn write_root_if_needed(path: &str, content: &str) -> Result<bool> {
|
|||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Patch the nginx site config to add a `/api/app-catalog` proxy block if
|
/// Patch the nginx site config to add missing backend proxy blocks. Older ISO
|
||||||
/// it's missing. The original ISO shipped individual per-endpoint `location`
|
/// configs shipped individual per-endpoint `location` blocks, so missing
|
||||||
/// blocks and no catch-all `/api/`, so `/api/app-catalog` silently fell
|
/// endpoints silently fell through to the SPA `index.html` and the frontend
|
||||||
/// through to the SPA `index.html` and the frontend got HTML instead of
|
/// got HTML instead of JSON.
|
||||||
/// JSON. We anchor the insert to the DWN comment that already sits right
|
|
||||||
/// after the `/api/blob` block, so the new block lands in both the HTTP
|
|
||||||
/// and HTTPS server blocks.
|
|
||||||
///
|
///
|
||||||
/// Validates via `nginx -t` before reloading. On failure the patch is
|
/// Validates via `nginx -t` before reloading. On failure the patch is
|
||||||
/// rolled back from a backup written just before the write.
|
/// rolled back from a backup written just before the write.
|
||||||
@ -465,51 +465,90 @@ async fn run_nginx() -> Result<bool> {
|
|||||||
return Ok(false);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if !Path::new(NGINX_CONF_PATH).exists() {
|
let mut changed = false;
|
||||||
debug!("{} missing — skipping nginx bootstrap", NGINX_CONF_PATH);
|
let mut patched_paths = Vec::<PathBuf>::new();
|
||||||
return Ok(false);
|
for path in [NGINX_CONF_PATH, NGINX_ENABLED_CONF_PATH] {
|
||||||
|
let candidate = Path::new(path);
|
||||||
|
if !candidate.exists() {
|
||||||
|
debug!("{} missing — skipping nginx bootstrap", path);
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
let canonical = fs::canonicalize(candidate)
|
||||||
let content = fs::read_to_string(NGINX_CONF_PATH)
|
|
||||||
.await
|
.await
|
||||||
.with_context(|| format!("read {}", NGINX_CONF_PATH))?;
|
.unwrap_or_else(|_| candidate.to_path_buf());
|
||||||
if content.contains("location /api/app-catalog") {
|
if patched_paths.iter().any(|p| p == &canonical) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
patched_paths.push(canonical);
|
||||||
|
changed |= patch_nginx_conf(path).await?;
|
||||||
|
}
|
||||||
|
Ok(changed)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn patch_nginx_conf(path: &str) -> Result<bool> {
|
||||||
|
let content = fs::read_to_string(path)
|
||||||
|
.await
|
||||||
|
.with_context(|| format!("read {}", path))?;
|
||||||
|
let missing_app_catalog = !content.contains("location /api/app-catalog");
|
||||||
|
let missing_bitcoin_status = !content.contains("location /bitcoin-status");
|
||||||
|
if !missing_app_catalog && !missing_bitcoin_status {
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut patched = content.clone();
|
||||||
|
|
||||||
|
if missing_bitcoin_status {
|
||||||
|
let anchor = " location /electrs-status {";
|
||||||
|
if !patched.contains(anchor) {
|
||||||
|
warn!("nginx conf missing electrs-status anchor — skipping /bitcoin-status patch");
|
||||||
|
} else {
|
||||||
|
let replacement = format!("{}{}", NGINX_BITCOIN_STATUS_BLOCK, anchor);
|
||||||
|
patched = patched.replace(anchor, &replacement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if missing_app_catalog {
|
||||||
// The DWN comment sits at the same indent right after the `/api/blob`
|
// The DWN comment sits at the same indent right after the `/api/blob`
|
||||||
// block in both server blocks — a stable anchor that existed on every
|
// block in both server blocks — a stable anchor that existed on every
|
||||||
// ISO shipped to date. If it's absent (config got heavily customized),
|
// ISO shipped to date. If it's absent (config got heavily customized),
|
||||||
// we bail rather than guess where to splice.
|
// skip rather than guess where to splice.
|
||||||
let anchor = " # DWN endpoints — peer access over Tor (no auth)";
|
let anchor = " # DWN endpoints — peer access over Tor (no auth)";
|
||||||
if !content.contains(anchor) {
|
if !patched.contains(anchor) {
|
||||||
warn!("nginx conf missing DWN anchor — skipping /api/app-catalog patch");
|
warn!("nginx conf missing DWN anchor — skipping /api/app-catalog patch");
|
||||||
|
} else {
|
||||||
|
let replacement = format!("{}{}", NGINX_APP_CATALOG_BLOCK, anchor);
|
||||||
|
patched = patched.replace(anchor, &replacement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if patched == content {
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
let replacement = format!("{}{}", NGINX_APP_CATALOG_BLOCK, anchor);
|
|
||||||
let patched = content.replace(anchor, &replacement);
|
|
||||||
|
|
||||||
// Write patched config via a user-owned tmp + sudo mv, after stashing
|
// Write patched config via a user-owned tmp + sudo mv, after stashing
|
||||||
// a backup so we can revert if `nginx -t` hates what we produced.
|
// a backup outside nginx include dirs so validation cannot load it too.
|
||||||
let pid = std::process::id();
|
let pid = std::process::id();
|
||||||
let tmp = format!("/tmp/archipelago-nginx-{}.conf", pid);
|
let tmp = format!("/tmp/archipelago-nginx-{}.conf", pid);
|
||||||
fs::write(&tmp, &patched)
|
fs::write(&tmp, &patched)
|
||||||
.await
|
.await
|
||||||
.with_context(|| format!("write {}", tmp))?;
|
.with_context(|| format!("write {}", tmp))?;
|
||||||
|
|
||||||
let backup = format!("/tmp/archipelago-nginx-backup-{}.conf", pid);
|
let backup = format!(
|
||||||
if let Err(e) = host_sudo(&["cp", NGINX_CONF_PATH, &backup]).await {
|
"/tmp/archipelago-nginx-backup-{}-{}.conf",
|
||||||
|
pid,
|
||||||
|
patched.len()
|
||||||
|
);
|
||||||
|
if let Err(e) = host_sudo(&["cp", path, &backup]).await {
|
||||||
let _ = fs::remove_file(&tmp).await;
|
let _ = fs::remove_file(&tmp).await;
|
||||||
return Err(e.context("backup nginx conf"));
|
return Err(e.context("backup nginx conf"));
|
||||||
}
|
}
|
||||||
|
|
||||||
let mv = host_sudo(&["mv", &tmp, NGINX_CONF_PATH]).await;
|
let mv = host_sudo(&["mv", &tmp, path]).await;
|
||||||
match mv {
|
match mv {
|
||||||
Ok(s) if s.success() => {}
|
Ok(s) if s.success() => {}
|
||||||
Ok(s) => {
|
Ok(s) => {
|
||||||
let _ = fs::remove_file(&tmp).await;
|
let _ = fs::remove_file(&tmp).await;
|
||||||
anyhow::bail!("sudo mv nginx conf exited with {}", s);
|
anyhow::bail!("sudo mv nginx conf to {} exited with {}", path, s);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
let _ = fs::remove_file(&tmp).await;
|
let _ = fs::remove_file(&tmp).await;
|
||||||
@ -522,7 +561,7 @@ async fn run_nginx() -> Result<bool> {
|
|||||||
let valid = matches!(&test, Ok(s) if s.success());
|
let valid = matches!(&test, Ok(s) if s.success());
|
||||||
if !valid {
|
if !valid {
|
||||||
warn!("nginx -t failed after patch — reverting");
|
warn!("nginx -t failed after patch — reverting");
|
||||||
let _ = host_sudo(&["mv", &backup, NGINX_CONF_PATH]).await;
|
let _ = host_sudo(&["mv", &backup, path]).await;
|
||||||
if let Err(e) = test {
|
if let Err(e) = test {
|
||||||
return Err(e.context("nginx -t"));
|
return Err(e.context("nginx -t"));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -186,6 +186,7 @@ pub async fn install_one(spec: &CompanionSpec) -> Result<()> {
|
|||||||
/// URL for pull).
|
/// URL for pull).
|
||||||
async fn ensure_image_present(spec: &CompanionSpec) -> Result<String> {
|
async fn ensure_image_present(spec: &CompanionSpec) -> Result<String> {
|
||||||
let local_image = format!("localhost/{}:latest", spec.image_base);
|
let local_image = format!("localhost/{}:latest", spec.image_base);
|
||||||
|
let local_image_compat = format!("localhost/{}:local", spec.image_base);
|
||||||
let registry_image = format!("{}/{}:latest", COMPANION_REGISTRY, spec.image_base);
|
let registry_image = format!("{}/{}:latest", COMPANION_REGISTRY, spec.image_base);
|
||||||
|
|
||||||
// Prefer local build — companions can carry build-time customizations
|
// Prefer local build — companions can carry build-time customizations
|
||||||
@ -193,6 +194,9 @@ async fn ensure_image_present(spec: &CompanionSpec) -> Result<String> {
|
|||||||
for dir in spec.build_dir_candidates {
|
for dir in spec.build_dir_candidates {
|
||||||
let dockerfile = PathBuf::from(dir).join("Dockerfile");
|
let dockerfile = PathBuf::from(dir).join("Dockerfile");
|
||||||
if fs::try_exists(&dockerfile).await.unwrap_or(false) {
|
if fs::try_exists(&dockerfile).await.unwrap_or(false) {
|
||||||
|
if image_exists(&local_image_compat).await {
|
||||||
|
return Ok(local_image_compat);
|
||||||
|
}
|
||||||
if image_exists(&local_image).await {
|
if image_exists(&local_image).await {
|
||||||
return Ok(local_image);
|
return Ok(local_image);
|
||||||
}
|
}
|
||||||
@ -335,13 +339,18 @@ pub async fn reconcile(installed_apps: &[String]) -> Vec<(String, anyhow::Error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Does this companion need install_one to be re-run? Returns true if
|
/// Does this companion need install_one to be re-run? Returns true if
|
||||||
/// the unit file is missing OR the service is not active.
|
/// the unit file is missing, stale, or the service is not active.
|
||||||
async fn needs_repair(spec: &CompanionSpec) -> Result<bool> {
|
async fn needs_repair(spec: &CompanionSpec) -> Result<bool> {
|
||||||
let dir = quadlet::unit_dir().await?;
|
let dir = quadlet::unit_dir().await?;
|
||||||
let unit_path = dir.join(format!("{}.container", spec.name));
|
let unit_path = dir.join(format!("{}.container", spec.name));
|
||||||
if !fs::try_exists(&unit_path).await.unwrap_or(false) {
|
if !fs::try_exists(&unit_path).await.unwrap_or(false) {
|
||||||
return Ok(true);
|
return Ok(true);
|
||||||
}
|
}
|
||||||
|
let expected_image = ensure_image_present(spec).await?;
|
||||||
|
let expected_unit = build_unit(spec, &expected_image);
|
||||||
|
if expected_unit.render() != fs::read_to_string(&unit_path).await.unwrap_or_default() {
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
let svc = format!("{}.service", spec.name);
|
let svc = format!("{}.service", spec.name);
|
||||||
Ok(!quadlet::is_active(&svc).await)
|
Ok(!quadlet::is_active(&svc).await)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -113,6 +113,118 @@ async fn chown_for_rootless_container(uid_gid: &str, path: &str) -> Result<()> {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn wait_for_host_port(port: u16, timeout_secs: u64) -> bool {
|
||||||
|
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(timeout_secs);
|
||||||
|
loop {
|
||||||
|
if tokio::net::TcpStream::connect(("127.0.0.1", port))
|
||||||
|
.await
|
||||||
|
.is_ok()
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if std::time::Instant::now() >= deadline {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn patch_indeedhub_nostr_provider() {
|
||||||
|
let _ = tokio::process::Command::new("podman")
|
||||||
|
.args([
|
||||||
|
"exec",
|
||||||
|
"indeedhub",
|
||||||
|
"sed",
|
||||||
|
"-i",
|
||||||
|
"/X-Frame-Options/d",
|
||||||
|
"/etc/nginx/conf.d/default.conf",
|
||||||
|
])
|
||||||
|
.output()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let provider_src = "/opt/archipelago/web-ui/nostr-provider.js";
|
||||||
|
if tokio::fs::metadata(provider_src).await.is_ok() {
|
||||||
|
let _ = tokio::process::Command::new("podman")
|
||||||
|
.args([
|
||||||
|
"cp",
|
||||||
|
provider_src,
|
||||||
|
"indeedhub:/usr/share/nginx/html/nostr-provider.js",
|
||||||
|
])
|
||||||
|
.output()
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
let check = tokio::process::Command::new("podman")
|
||||||
|
.args([
|
||||||
|
"exec",
|
||||||
|
"indeedhub",
|
||||||
|
"grep",
|
||||||
|
"-q",
|
||||||
|
"nostr-provider",
|
||||||
|
"/etc/nginx/conf.d/default.conf",
|
||||||
|
])
|
||||||
|
.output()
|
||||||
|
.await;
|
||||||
|
let already_patched = check.map(|o| o.status.success()).unwrap_or(false);
|
||||||
|
|
||||||
|
if !already_patched {
|
||||||
|
let cat_out = tokio::process::Command::new("podman")
|
||||||
|
.args(["exec", "indeedhub", "cat", "/etc/nginx/conf.d/default.conf"])
|
||||||
|
.output()
|
||||||
|
.await;
|
||||||
|
if let Ok(out) = cat_out {
|
||||||
|
if out.status.success() {
|
||||||
|
let conf = String::from_utf8_lossy(&out.stdout).to_string();
|
||||||
|
let conf = conf.replace(
|
||||||
|
"location = /sw.js {",
|
||||||
|
"location = /nostr-provider.js {\n\
|
||||||
|
add_header Cache-Control \"no-cache, no-store, must-revalidate\";\n\
|
||||||
|
expires off;\n\
|
||||||
|
}\n\n\
|
||||||
|
location = /sw.js {",
|
||||||
|
);
|
||||||
|
let conf = if conf.contains("try_files") && !conf.contains("sub_filter") {
|
||||||
|
conf.replacen(
|
||||||
|
"try_files $uri $uri/ /index.html;",
|
||||||
|
"try_files $uri $uri/ /index.html;\n\
|
||||||
|
sub_filter_once on;\n\
|
||||||
|
sub_filter '</head>' '<script src=\"/nostr-provider.js\"></script></head>';",
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
conf
|
||||||
|
};
|
||||||
|
|
||||||
|
let tmp_path = "/tmp/indeedhub-nginx-patch.conf";
|
||||||
|
if tokio::fs::write(tmp_path, &conf).await.is_ok() {
|
||||||
|
let _ = tokio::process::Command::new("podman")
|
||||||
|
.args(["cp", tmp_path, "indeedhub:/etc/nginx/conf.d/default.conf"])
|
||||||
|
.output()
|
||||||
|
.await;
|
||||||
|
let _ = tokio::fs::remove_file(tmp_path).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = tokio::process::Command::new("podman")
|
||||||
|
.args([
|
||||||
|
"exec",
|
||||||
|
"indeedhub",
|
||||||
|
"sed",
|
||||||
|
"-i",
|
||||||
|
"s|proxy_set_header X-Forwarded-Prefix /api;|proxy_set_header X-Forwarded-Prefix $http_x_forwarded_prefix/api;|",
|
||||||
|
"/etc/nginx/conf.d/default.conf",
|
||||||
|
])
|
||||||
|
.output()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let _ = tokio::process::Command::new("podman")
|
||||||
|
.args(["exec", "indeedhub", "nginx", "-s", "reload"])
|
||||||
|
.output()
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
/// Outcome of `reconcile_all` for a single app.
|
/// Outcome of `reconcile_all` for a single app.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub enum ReconcileAction {
|
pub enum ReconcileAction {
|
||||||
@ -501,9 +613,10 @@ impl ProdContainerOrchestrator {
|
|||||||
let app_id = lm.manifest.app.id.clone();
|
let app_id = lm.manifest.app.id.clone();
|
||||||
if app_id == "indeedhub" {
|
if app_id == "indeedhub" {
|
||||||
// IndeedHub is a multi-container stack installed by the package
|
// IndeedHub is a multi-container stack installed by the package
|
||||||
// stack path. Reconciling its single manifest races stack installs
|
// stack path. Boot reconcile must not fresh-install the catalog
|
||||||
// and can recreate a broken frontend container with the same name.
|
// manifest, but it does need to start/repair an already-installed
|
||||||
return Ok(ReconcileAction::Left("stack-managed".to_string()));
|
// stack and reapply the frontend's Nostr provider patch after boot.
|
||||||
|
return self.reconcile_indeedhub_stack(mode).await;
|
||||||
}
|
}
|
||||||
let lock = self.app_lock(&app_id).await;
|
let lock = self.app_lock(&app_id).await;
|
||||||
let _guard = lock.lock().await;
|
let _guard = lock.lock().await;
|
||||||
@ -720,10 +833,24 @@ impl ProdContainerOrchestrator {
|
|||||||
async fn run_post_data_uid_hooks(&self, app_id: &str) -> Result<()> {
|
async fn run_post_data_uid_hooks(&self, app_id: &str) -> Result<()> {
|
||||||
match app_id {
|
match app_id {
|
||||||
"fedimint" | "fedimint-gateway" => self.ensure_fedimint_dirs().await,
|
"fedimint" | "fedimint-gateway" => self.ensure_fedimint_dirs().await,
|
||||||
|
"grafana" => self.ensure_grafana_dirs().await,
|
||||||
_ => Ok(()),
|
_ => Ok(()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn ensure_grafana_dirs(&self) -> Result<()> {
|
||||||
|
let dir = "/var/lib/archipelago/grafana";
|
||||||
|
let mkdir = host_sudo(&["mkdir", "-p", dir])
|
||||||
|
.await
|
||||||
|
.context("mkdir grafana data dir")?;
|
||||||
|
if !mkdir.success() {
|
||||||
|
return Err(anyhow::anyhow!("mkdir -p {dir} failed with status {mkdir}"));
|
||||||
|
}
|
||||||
|
chown_for_rootless_container("472:472", dir)
|
||||||
|
.await
|
||||||
|
.context("chown grafana data dir for rootless uid 472")
|
||||||
|
}
|
||||||
|
|
||||||
/// Phase 3.3 in-place migration. When `use_quadlet_backends` flips
|
/// Phase 3.3 in-place migration. When `use_quadlet_backends` flips
|
||||||
/// from off → on, existing nodes have backend containers parented
|
/// from off → on, existing nodes have backend containers parented
|
||||||
/// under archipelago.service's cgroup (the bad shape). They need to
|
/// under archipelago.service's cgroup (the bad shape). They need to
|
||||||
@ -1138,6 +1265,59 @@ impl ProdContainerOrchestrator {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn reconcile_indeedhub_stack(&self, mode: ReconcileMode) -> Result<ReconcileAction> {
|
||||||
|
let frontend_status = match self.runtime.get_container_status("indeedhub").await {
|
||||||
|
Ok(status) => status,
|
||||||
|
Err(_) => {
|
||||||
|
if mode == ReconcileMode::ExistingOnly {
|
||||||
|
return Ok(ReconcileAction::Left("absent".to_string()));
|
||||||
|
}
|
||||||
|
// Fresh stack creation is owned by package::stacks so we do not
|
||||||
|
// create a single broken frontend container from the manifest.
|
||||||
|
return Ok(ReconcileAction::Left("stack-managed".to_string()));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
self.start_indeedhub_backends().await?;
|
||||||
|
|
||||||
|
let mut started = false;
|
||||||
|
match frontend_status.state {
|
||||||
|
ContainerState::Running => {}
|
||||||
|
ContainerState::Stopped | ContainerState::Exited | ContainerState::Created => {
|
||||||
|
self.runtime
|
||||||
|
.start_container("indeedhub")
|
||||||
|
.await
|
||||||
|
.context("start IndeedHub frontend during reconcile")?;
|
||||||
|
started = true;
|
||||||
|
}
|
||||||
|
ContainerState::Paused => return Ok(ReconcileAction::Left("paused".to_string())),
|
||||||
|
ContainerState::Unknown(s) => return Ok(ReconcileAction::Left(s)),
|
||||||
|
}
|
||||||
|
|
||||||
|
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
|
||||||
|
self.repair_indeedhub_network_aliases().await;
|
||||||
|
patch_indeedhub_nostr_provider().await;
|
||||||
|
|
||||||
|
if !wait_for_host_port(7778, 10).await {
|
||||||
|
tracing::warn!(
|
||||||
|
"IndeedHub frontend running but host port 7778 is not listening; restarting"
|
||||||
|
);
|
||||||
|
let _ = self.runtime.stop_container("indeedhub").await;
|
||||||
|
self.runtime
|
||||||
|
.start_container("indeedhub")
|
||||||
|
.await
|
||||||
|
.context("restart IndeedHub frontend after missing host port")?;
|
||||||
|
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
|
||||||
|
patch_indeedhub_nostr_provider().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
if started {
|
||||||
|
Ok(ReconcileAction::Started)
|
||||||
|
} else {
|
||||||
|
Ok(ReconcileAction::NoOp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn repair_indeedhub_network_aliases(&self) {
|
async fn repair_indeedhub_network_aliases(&self) {
|
||||||
for (container, alias) in [
|
for (container, alias) in [
|
||||||
("indeedhub-postgres", "postgres"),
|
("indeedhub-postgres", "postgres"),
|
||||||
@ -1302,6 +1482,10 @@ impl ProdContainerOrchestrator {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if self.container_command_drifted(name, manifest).await {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
let inspect = tokio::process::Command::new("podman")
|
let inspect = tokio::process::Command::new("podman")
|
||||||
.args([
|
.args([
|
||||||
"inspect",
|
"inspect",
|
||||||
@ -1334,6 +1518,52 @@ impl ProdContainerOrchestrator {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn container_command_drifted(&self, name: &str, manifest: &AppManifest) -> bool {
|
||||||
|
if manifest.app.container.entrypoint.is_none()
|
||||||
|
&& manifest.app.container.custom_args.is_empty()
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let inspect = tokio::process::Command::new("podman")
|
||||||
|
.args([
|
||||||
|
"inspect",
|
||||||
|
name,
|
||||||
|
"--format",
|
||||||
|
"entry={{json .Config.Entrypoint}}\ncmd={{json .Config.Cmd}}",
|
||||||
|
])
|
||||||
|
.output()
|
||||||
|
.await;
|
||||||
|
let Ok(output) = inspect else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
if !output.status.success() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let text = String::from_utf8_lossy(&output.stdout);
|
||||||
|
let current_entry = text
|
||||||
|
.lines()
|
||||||
|
.find_map(|line| line.strip_prefix("entry="))
|
||||||
|
.and_then(|json| serde_json::from_str::<Option<Vec<String>>>(json).ok())
|
||||||
|
.flatten()
|
||||||
|
.unwrap_or_default();
|
||||||
|
let current_cmd = text
|
||||||
|
.lines()
|
||||||
|
.find_map(|line| line.strip_prefix("cmd="))
|
||||||
|
.and_then(|json| serde_json::from_str::<Option<Vec<String>>>(json).ok())
|
||||||
|
.flatten()
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let expected_entry = manifest
|
||||||
|
.app
|
||||||
|
.container
|
||||||
|
.entrypoint
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_default();
|
||||||
|
current_entry != expected_entry || current_cmd != manifest.app.container.custom_args
|
||||||
|
}
|
||||||
|
|
||||||
async fn apply_data_uid(&self, manifest: &AppManifest) -> Result<()> {
|
async fn apply_data_uid(&self, manifest: &AppManifest) -> Result<()> {
|
||||||
let Some(uid_gid) = manifest.app.container.data_uid.as_ref() else {
|
let Some(uid_gid) = manifest.app.container.data_uid.as_ref() else {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
|
|||||||
@ -829,6 +829,41 @@ app:
|
|||||||
assert_eq!(u.restart_policy, RestartPolicy::OnFailure);
|
assert_eq!(u.restart_policy, RestartPolicy::OnFailure);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn from_manifest_preserves_grafana_data_uid_and_volume_shape() {
|
||||||
|
let yaml = r#"
|
||||||
|
app:
|
||||||
|
id: grafana
|
||||||
|
name: Grafana
|
||||||
|
version: 10.2.0
|
||||||
|
container:
|
||||||
|
image: grafana/grafana:10.2.0
|
||||||
|
data_uid: "472:472"
|
||||||
|
volumes:
|
||||||
|
- type: bind
|
||||||
|
source: /var/lib/archipelago/grafana
|
||||||
|
target: /var/lib/grafana
|
||||||
|
options: [rw]
|
||||||
|
resources:
|
||||||
|
memory_limit: 1g
|
||||||
|
"#;
|
||||||
|
let m = AppManifest::parse(yaml).unwrap();
|
||||||
|
assert_eq!(m.app.container.data_uid.as_deref(), Some("472:472"));
|
||||||
|
|
||||||
|
let u = QuadletUnit::from_manifest(&m, "grafana");
|
||||||
|
assert_eq!(u.memory_mb, Some(1024));
|
||||||
|
assert_eq!(u.bind_mounts.len(), 1);
|
||||||
|
assert_eq!(
|
||||||
|
u.bind_mounts[0].host,
|
||||||
|
PathBuf::from("/var/lib/archipelago/grafana")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
u.bind_mounts[0].container,
|
||||||
|
PathBuf::from("/var/lib/grafana")
|
||||||
|
);
|
||||||
|
assert!(!u.bind_mounts[0].read_only);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn from_manifest_marks_ro_volumes_read_only() {
|
fn from_manifest_marks_ro_volumes_read_only() {
|
||||||
let yaml = r#"
|
let yaml = r#"
|
||||||
|
|||||||
317
docs/CHAT_TRANSCRIPT_2026-05-02.md
Normal file
317
docs/CHAT_TRANSCRIPT_2026-05-02.md
Normal file
@ -0,0 +1,317 @@
|
|||||||
|
# Chat Transcript And Working Notes
|
||||||
|
|
||||||
|
Date: 2026-05-02
|
||||||
|
|
||||||
|
This file captures the current chat context, decisions, progress, and next steps so work can continue from another device/session.
|
||||||
|
|
||||||
|
## User Request
|
||||||
|
|
||||||
|
The user asked to continue hardening Archipelago app/container lifecycle, then asked multiple times to save the plan/progress/next steps and finally to save the entire chat to Markdown.
|
||||||
|
|
||||||
|
Key user constraints and corrections:
|
||||||
|
|
||||||
|
- Continue if next steps are clear; ask only if blocked.
|
||||||
|
- Exhaustively harden app/container lifecycle before release.
|
||||||
|
- Preserve data during destructive lifecycle testing unless explicitly instructed otherwise.
|
||||||
|
- Do not rely on `/app/...` proxy paths for app launch/testing. The user corrected: “we never use paths only ports.”
|
||||||
|
- LND/Electrum wallet-connect tests must validate real connection details and QR, including Tor.
|
||||||
|
|
||||||
|
## Earlier Progress Summary
|
||||||
|
|
||||||
|
Before the latest work, the project already had substantial lifecycle hardening in progress:
|
||||||
|
|
||||||
|
- Remote lifecycle harness exists at `tests/lifecycle/remote-lifecycle.sh`.
|
||||||
|
- `.198` SSH works with `/home/archipelago/.ssh/id_ed25519`.
|
||||||
|
- `.228` RPC works, but SSH is blocked with `Permission denied (publickey,password)`.
|
||||||
|
- Multiple backend release binaries were built and deployed to `.198` with backups in `/usr/local/bin/archipelago.bak-*`.
|
||||||
|
- Fixed stale package scanner state recovery from `Removing -> Running` when a container is actually live.
|
||||||
|
- Fixed startup ordering so crash recovery runs before BootReconciler.
|
||||||
|
- Removed dangerous automatic Podman runtime directory deletion on `podman info` failure.
|
||||||
|
- Narrowed generic crash recovery to safe legacy containers.
|
||||||
|
- Fixed companion reconciliation on install/start/restart.
|
||||||
|
- Fixed uninstall/reinstall behavior so uninstall disables manifest apps instead of deleting manifest availability, and reinstall re-enables them.
|
||||||
|
- Fixed LND config generation/repair:
|
||||||
|
- `bitcoin.active=true`
|
||||||
|
- `bitcoin.mainnet=true`
|
||||||
|
- `bitcoin.node=bitcoind`
|
||||||
|
- `bitcoind.rpchost=bitcoin-knots:8332`
|
||||||
|
- sudo fallback for writing container-owned config paths.
|
||||||
|
- `.198` had previously passed focused lifecycle for `filebrowser`, `bitcoin-knots`, and a looser LND launch test.
|
||||||
|
|
||||||
|
## Major Files Touched In This Session
|
||||||
|
|
||||||
|
- `docs/CONTAINER_LIFECYCLE_HANDOFF.md`
|
||||||
|
- `docs/CHAT_TRANSCRIPT_2026-05-02.md`
|
||||||
|
- `tests/lifecycle/remote-lifecycle.sh`
|
||||||
|
- `core/archipelago/src/container/lnd.rs`
|
||||||
|
- `core/archipelago/src/container/companion.rs`
|
||||||
|
- `core/archipelago/src/container/prod_orchestrator.rs`
|
||||||
|
- `core/archipelago/src/container/docker_packages.rs`
|
||||||
|
- `core/container/src/podman_client.rs`
|
||||||
|
- `core/archipelago/src/port_allocator.rs`
|
||||||
|
- `apps/lnd-ui/manifest.yml`
|
||||||
|
- `neode-ui/src/views/appSession/appSessionConfig.ts`
|
||||||
|
- `neode-ui/src/stores/container.ts`
|
||||||
|
- `neode-ui/src/stores/appLauncher.ts`
|
||||||
|
- `neode-ui/src/views/appDetails/appDetailsData.ts`
|
||||||
|
- nginx config/snippet files under `scripts/` and `image-recipe/`
|
||||||
|
|
||||||
|
## LND Wallet Bootstrap Investigation
|
||||||
|
|
||||||
|
Initial strict LND probe failed because `/lnd-connect-info` could not read `admin.macaroon`:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Failed to read LND admin macaroon — is LND installed?
|
||||||
|
direct: Permission denied (os error 13)
|
||||||
|
sudo: cat: /var/lib/archipelago/lnd/data/chain/bitcoin/mainnet/admin.macaroon: No such file or directory
|
||||||
|
```
|
||||||
|
|
||||||
|
LND logs showed the wallet was uninitialized/locked:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Waiting for wallet encryption password. Use lncli create...
|
||||||
|
```
|
||||||
|
|
||||||
|
Tests showed `lncli create` is interactive and does not support `--stdin`:
|
||||||
|
|
||||||
|
```text
|
||||||
|
[lncli] flag provided but not defined: -stdin
|
||||||
|
```
|
||||||
|
|
||||||
|
`lncli unlock --stdin` is supported, so the final approach was:
|
||||||
|
|
||||||
|
- Use LND REST unlocker endpoints for new wallet creation.
|
||||||
|
- Use `lncli unlock --stdin` only for an existing wallet.
|
||||||
|
- Treat “wallet already exists” from REST as a signal to unlock.
|
||||||
|
- Use sudo-aware checks/reads for wallet artifacts because LND data directories are container-owned and `0700`.
|
||||||
|
|
||||||
|
Implemented in `core/archipelago/src/container/lnd.rs`:
|
||||||
|
|
||||||
|
- `ensure_wallet_initialized()`
|
||||||
|
- `file_exists_as_root()`
|
||||||
|
- `read_file_as_root()`
|
||||||
|
- `init_wallet_via_rest()`
|
||||||
|
- `get_lnd_unlocker_json()`
|
||||||
|
- `post_lnd_unlocker_json()`
|
||||||
|
- `unlock_existing_wallet()`
|
||||||
|
- `wait_for_admin_macaroon()`
|
||||||
|
- `lnd_getinfo_ready()`
|
||||||
|
|
||||||
|
Focused Rust test passes:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/archipelago/Projects/archy/core
|
||||||
|
cargo test -p archipelago --bin archipelago lnd
|
||||||
|
```
|
||||||
|
|
||||||
|
Result:
|
||||||
|
|
||||||
|
```text
|
||||||
|
7 passed; 0 failed
|
||||||
|
```
|
||||||
|
|
||||||
|
## LND UI Port Collision
|
||||||
|
|
||||||
|
The strict LND UI test then failed with `502`.
|
||||||
|
|
||||||
|
Investigation found a real port collision:
|
||||||
|
|
||||||
|
- `nostr-rs-relay` uses host `8081`.
|
||||||
|
- Old `archy-lnd-ui` also used host `8081`.
|
||||||
|
- nginx `/app/lnd/` proxy also pointed at `8081`.
|
||||||
|
|
||||||
|
Fix implemented:
|
||||||
|
|
||||||
|
- Move LND UI companion to host port `18083`, container port `80`.
|
||||||
|
- Keep `nostr-rs-relay` on `8081`.
|
||||||
|
- Update app metadata/routing to `18083`.
|
||||||
|
- Update tests to expect direct port launch.
|
||||||
|
|
||||||
|
Important correction from user:
|
||||||
|
|
||||||
|
```text
|
||||||
|
we never use paths only ports, how many times do you need to be told
|
||||||
|
```
|
||||||
|
|
||||||
|
Action taken after correction:
|
||||||
|
|
||||||
|
- Stop validating through `/app/lnd/` and `/app/electrumx/` in the lifecycle harness.
|
||||||
|
- Switch `launch_url_for()` to direct app ports.
|
||||||
|
- Switch app session resolver to direct `http://host:port` launch, even from HTTPS parent pages.
|
||||||
|
- Remove use of `HTTPS_PROXY_PATHS[id]` in `resolveAppUrl()`.
|
||||||
|
|
||||||
|
Direct-port LND audit command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ARCHY_HOST=192.168.1.198 ARCHY_PASSWORD=password123 ARCHY_APPS=lnd tests/lifecycle/remote-lifecycle.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Result:
|
||||||
|
|
||||||
|
```text
|
||||||
|
### 192.168.1.198 iteration 1 / 1 ###
|
||||||
|
lnd state=running
|
||||||
|
all checks passed
|
||||||
|
```
|
||||||
|
|
||||||
|
The audit now validates `http://192.168.1.198:18083/`, not `/app/lnd/`.
|
||||||
|
|
||||||
|
## Lifecycle Harness Changes
|
||||||
|
|
||||||
|
`tests/lifecycle/remote-lifecycle.sh` changes made:
|
||||||
|
|
||||||
|
- Normalize package states with `ascii_downcase` because API returned `Running`.
|
||||||
|
- Direct port launch URLs:
|
||||||
|
- LND: `http://${ARCHY_HOST}:18083/`
|
||||||
|
- Electrum/Electrs: `http://${ARCHY_HOST}:50002/`
|
||||||
|
- Bitcoin UI: `http://${ARCHY_HOST}:8334/`
|
||||||
|
- Other apps mapped to direct ports where known.
|
||||||
|
- LND probe checks:
|
||||||
|
- `Connect Your Wallet`
|
||||||
|
- `id="lndQrBox"`
|
||||||
|
- `id="connHost"`
|
||||||
|
- `value="rest-tor"`
|
||||||
|
- `value="grpc-tor"`
|
||||||
|
- `value="rest-local"`
|
||||||
|
- `value="grpc-local"`
|
||||||
|
- `Copy lndconnect URI`
|
||||||
|
- `/lnd-connect-info` cert, macaroon, ports, and Tor onion.
|
||||||
|
- Electrum probe checks:
|
||||||
|
- local QR container and address field
|
||||||
|
- Tor QR container and onion field
|
||||||
|
- port `50001`
|
||||||
|
- QR renderer
|
||||||
|
- direct `http://${ARCHY_HOST}:50002/qrcode.js`
|
||||||
|
- `/electrs-status` Tor onion.
|
||||||
|
- Full lifecycle now fails immediately on any failed phase with `|| return 1` so a later reinstall cannot mask a failed restart/probe.
|
||||||
|
|
||||||
|
## Deployments To `.198`
|
||||||
|
|
||||||
|
Several release builds were made and deployed:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/archipelago/Projects/archy/core
|
||||||
|
cargo build -p archipelago --bin archipelago --release
|
||||||
|
```
|
||||||
|
|
||||||
|
Deploy pattern:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scp -i /home/archipelago/.ssh/id_ed25519 -o StrictHostKeyChecking=no \
|
||||||
|
/home/archipelago/Projects/archy/core/target/release/archipelago \
|
||||||
|
archipelago@192.168.1.198:/tmp/archipelago.new
|
||||||
|
|
||||||
|
ssh -i /home/archipelago/.ssh/id_ed25519 -o StrictHostKeyChecking=no \
|
||||||
|
archipelago@192.168.1.198 \
|
||||||
|
"sudo cp /usr/local/bin/archipelago /usr/local/bin/archipelago.bak-<timestamp> && \
|
||||||
|
sudo install -m 0755 /tmp/archipelago.new /usr/local/bin/archipelago && \
|
||||||
|
sudo systemctl restart archipelago.service && \
|
||||||
|
systemctl is-active archipelago.service"
|
||||||
|
```
|
||||||
|
|
||||||
|
Latest deploy returned:
|
||||||
|
|
||||||
|
```text
|
||||||
|
active
|
||||||
|
```
|
||||||
|
|
||||||
|
## `.198` Current Observations
|
||||||
|
|
||||||
|
After forcing LND package restart, companion reconciliation succeeded:
|
||||||
|
|
||||||
|
```text
|
||||||
|
nostr-rs-relay Up ... 0.0.0.0:8081->8080/tcp
|
||||||
|
lnd Up ... 0.0.0.0:8080->8080/tcp, 0.0.0.0:9735->9735/tcp, 0.0.0.0:10009->10009/tcp
|
||||||
|
archy-lnd-ui Up ... 0.0.0.0:18083->80/tcp
|
||||||
|
```
|
||||||
|
|
||||||
|
Direct UI test from `.198` returned `200`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -i http://127.0.0.1:18083/
|
||||||
|
```
|
||||||
|
|
||||||
|
Strict direct-port LND audit is green:
|
||||||
|
|
||||||
|
```text
|
||||||
|
lnd state=running
|
||||||
|
all checks passed
|
||||||
|
```
|
||||||
|
|
||||||
|
## Full LND Lifecycle Status
|
||||||
|
|
||||||
|
Full direct-port lifecycle was started:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ARCHY_HOST=192.168.1.198 ARCHY_PASSWORD=password123 ARCHY_APPS=lnd ARCHY_FULL_LIFECYCLE=1 tests/lifecycle/remote-lifecycle.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
It reached:
|
||||||
|
|
||||||
|
```text
|
||||||
|
### 192.168.1.198 iteration 1 / 1 ###
|
||||||
|
== lnd: install ==
|
||||||
|
== lnd: stop ==
|
||||||
|
```
|
||||||
|
|
||||||
|
Then the user aborted the command while asking to save memory/transcript.
|
||||||
|
|
||||||
|
The next continuation point is to rerun full LND direct-port lifecycle from scratch and inspect the stop phase if it hangs/fails.
|
||||||
|
|
||||||
|
## Handoff File
|
||||||
|
|
||||||
|
A durable handoff file was also created:
|
||||||
|
|
||||||
|
```text
|
||||||
|
docs/CONTAINER_LIFECYCLE_HANDOFF.md
|
||||||
|
```
|
||||||
|
|
||||||
|
It contains the plan, progress, current blockers, and next steps.
|
||||||
|
|
||||||
|
## Immediate Next Steps
|
||||||
|
|
||||||
|
1. Rerun full strict LND direct-port lifecycle:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ARCHY_HOST=192.168.1.198 ARCHY_PASSWORD=password123 ARCHY_APPS=lnd ARCHY_FULL_LIFECYCLE=1 tests/lifecycle/remote-lifecycle.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
2. If it hangs/fails at `stop`, inspect package runtime stop path and logs:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh -i /home/archipelago/.ssh/id_ed25519 -o StrictHostKeyChecking=no archipelago@192.168.1.198 \
|
||||||
|
'journalctl -u archipelago.service -n 260 --no-pager | egrep -i "package\.(stop|start|restart|install|uninstall)|lnd|companion|error|failed" | sed -n "1,220p"; podman ps -a --format "{{.Names}} {{.Status}} {{.Ports}}" | egrep "lnd|nostr" || true'
|
||||||
|
```
|
||||||
|
|
||||||
|
3. If stop is unreliable, inspect/fix:
|
||||||
|
|
||||||
|
- `core/archipelago/src/api/rpc/package/runtime.rs`
|
||||||
|
- `core/archipelago/src/container/prod_orchestrator.rs`
|
||||||
|
|
||||||
|
Likely causes to check:
|
||||||
|
|
||||||
|
- Reconciler restarting LND while stop is expected.
|
||||||
|
- State scanner reporting stale `running`.
|
||||||
|
- Companion handling interfering with parent app state.
|
||||||
|
- Async lifecycle returning before actual stop completes.
|
||||||
|
|
||||||
|
4. Once LND full lifecycle is green, run Electrum strict lifecycle with direct port `50002`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ARCHY_HOST=192.168.1.198 ARCHY_PASSWORD=password123 ARCHY_APPS=electrumx ARCHY_FULL_LIFECYCLE=1 tests/lifecycle/remote-lifecycle.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Continue with app groups after LND/Electrum:
|
||||||
|
|
||||||
|
- `filebrowser`
|
||||||
|
- `bitcoin-knots`
|
||||||
|
- `lnd`
|
||||||
|
- `electrumx`
|
||||||
|
- `mempool`
|
||||||
|
- `btcpay-server`
|
||||||
|
- `fedimint`
|
||||||
|
- remaining catalog apps.
|
||||||
|
|
||||||
|
## Important Instruction To Preserve
|
||||||
|
|
||||||
|
Use ports only for app launch/testing. Do not add or rely on `/app/...` path proxy launch behavior unless the user explicitly changes this requirement.
|
||||||
1033
docs/CONTAINER_LIFECYCLE_HANDOFF.md
Normal file
1033
docs/CONTAINER_LIFECYCLE_HANDOFF.md
Normal file
File diff suppressed because it is too large
Load Diff
@ -211,10 +211,15 @@ check_tools() {
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Ensure insecure registry config for Archipelago app registry (HTTP)
|
# Ensure insecure registry config for Archipelago app registries that are
|
||||||
if [ "$CONTAINER_CMD" = "podman" ]; then
|
# intentionally served over HTTP during ISO builds.
|
||||||
|
if [[ "$CONTAINER_CMD" == podman* ]]; then
|
||||||
mkdir -p /etc/containers/registries.conf.d
|
mkdir -p /etc/containers/registries.conf.d
|
||||||
cat > /etc/containers/registries.conf.d/archipelago.conf <<'REGCONF'
|
cat > /etc/containers/registries.conf.d/archipelago.conf <<'REGCONF'
|
||||||
|
[[registry]]
|
||||||
|
location = "146.59.87.168:3000"
|
||||||
|
insecure = true
|
||||||
|
|
||||||
[[registry]]
|
[[registry]]
|
||||||
location = "git.tx1138.com"
|
location = "git.tx1138.com"
|
||||||
insecure = true
|
insecure = true
|
||||||
@ -227,6 +232,15 @@ check_tools
|
|||||||
mkdir -p "$WORK_DIR"
|
mkdir -p "$WORK_DIR"
|
||||||
mkdir -p "$OUTPUT_DIR"
|
mkdir -p "$OUTPUT_DIR"
|
||||||
|
|
||||||
|
container_pull() {
|
||||||
|
local image="$1"
|
||||||
|
if [[ "$CONTAINER_CMD" == podman* && "$image" == 146.59.87.168:3000/* ]]; then
|
||||||
|
$CONTAINER_CMD pull --tls-verify=false --platform "$CONTAINER_PLATFORM" "$image"
|
||||||
|
else
|
||||||
|
$CONTAINER_CMD pull --platform "$CONTAINER_PLATFORM" "$image"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# STEP 1: Build complete root filesystem using Docker
|
# STEP 1: Build complete root filesystem using Docker
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
@ -1289,7 +1303,7 @@ if [ "$UNBUNDLED" = "1" ]; then
|
|||||||
echo " ✅ Using cached: $CORE_FILE"
|
echo " ✅ Using cached: $CORE_FILE"
|
||||||
else
|
else
|
||||||
echo " Pulling $CORE_IMAGE ($CONTAINER_PLATFORM)..."
|
echo " Pulling $CORE_IMAGE ($CONTAINER_PLATFORM)..."
|
||||||
if $CONTAINER_CMD pull --platform $CONTAINER_PLATFORM "$CORE_IMAGE"; then
|
if container_pull "$CORE_IMAGE"; then
|
||||||
$CONTAINER_CMD save "$CORE_IMAGE" -o "$IMAGES_DIR/$CORE_FILE" 2>/dev/null && \
|
$CONTAINER_CMD save "$CORE_IMAGE" -o "$IMAGES_DIR/$CORE_FILE" 2>/dev/null && \
|
||||||
echo " ✅ Saved core: $CORE_FILE ($(du -h "$IMAGES_DIR/$CORE_FILE" | cut -f1))" || \
|
echo " ✅ Saved core: $CORE_FILE ($(du -h "$IMAGES_DIR/$CORE_FILE" | cut -f1))" || \
|
||||||
echo " ⚠️ Failed to save $CORE_IMAGE"
|
echo " ⚠️ Failed to save $CORE_IMAGE"
|
||||||
@ -1367,7 +1381,7 @@ echo "$CONTAINER_IMAGES" | while read -r image filename; do
|
|||||||
echo " ✅ Using cached: $filename"
|
echo " ✅ Using cached: $filename"
|
||||||
else
|
else
|
||||||
echo " Pulling $image ($CONTAINER_PLATFORM)..."
|
echo " Pulling $image ($CONTAINER_PLATFORM)..."
|
||||||
if $CONTAINER_CMD pull --platform $CONTAINER_PLATFORM "$image"; then
|
if container_pull "$image"; then
|
||||||
echo " Saving $filename..."
|
echo " Saving $filename..."
|
||||||
if $CONTAINER_CMD save "$image" -o "$tarpath" 2>/dev/null; then
|
if $CONTAINER_CMD save "$image" -o "$tarpath" 2>/dev/null; then
|
||||||
echo " ✅ Saved: $(du -h "$tarpath" | cut -f1)"
|
echo " ✅ Saved: $(du -h "$tarpath" | cut -f1)"
|
||||||
@ -3456,9 +3470,9 @@ echo ""
|
|||||||
echo "Step 6: Creating bootable ISO..."
|
echo "Step 6: Creating bootable ISO..."
|
||||||
|
|
||||||
if [ "$UNBUNDLED" = "1" ]; then
|
if [ "$UNBUNDLED" = "1" ]; then
|
||||||
OUTPUT_ISO="$OUTPUT_DIR/archipelago-installer-unbundled-${ARCH}.iso"
|
OUTPUT_ISO="$OUTPUT_DIR/archipelago-installer-${BUILD_VERSION}-unbundled-${ARCH}.iso"
|
||||||
else
|
else
|
||||||
OUTPUT_ISO="$OUTPUT_DIR/archipelago-installer-${ARCH}.iso"
|
OUTPUT_ISO="$OUTPUT_DIR/archipelago-installer-${BUILD_VERSION}-${ARCH}.iso"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Use the proven MBR code for hybrid USB boot
|
# Use the proven MBR code for hybrid USB boot
|
||||||
|
|||||||
@ -156,6 +156,16 @@ server {
|
|||||||
error_page 502 503 = @backend_unavailable;
|
error_page 502 503 = @backend_unavailable;
|
||||||
error_page 504 = @backend_timeout;
|
error_page 504 = @backend_timeout;
|
||||||
}
|
}
|
||||||
|
location /bitcoin-status {
|
||||||
|
proxy_pass http://127.0.0.1:5678/bitcoin-status;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_connect_timeout 10s;
|
||||||
|
proxy_read_timeout 10s;
|
||||||
|
proxy_send_timeout 5s;
|
||||||
|
error_page 502 503 = @backend_unavailable;
|
||||||
|
error_page 504 = @backend_timeout;
|
||||||
|
}
|
||||||
location /electrs-status {
|
location /electrs-status {
|
||||||
proxy_pass http://127.0.0.1:5678/electrs-status;
|
proxy_pass http://127.0.0.1:5678/electrs-status;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
@ -969,6 +979,16 @@ server {
|
|||||||
error_page 502 503 = @backend_unavailable;
|
error_page 502 503 = @backend_unavailable;
|
||||||
error_page 504 = @backend_timeout;
|
error_page 504 = @backend_timeout;
|
||||||
}
|
}
|
||||||
|
location /bitcoin-status {
|
||||||
|
proxy_pass http://127.0.0.1:5678/bitcoin-status;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_connect_timeout 10s;
|
||||||
|
proxy_read_timeout 10s;
|
||||||
|
proxy_send_timeout 5s;
|
||||||
|
error_page 502 503 = @backend_unavailable;
|
||||||
|
error_page 504 = @backend_timeout;
|
||||||
|
}
|
||||||
location /electrs-status {
|
location /electrs-status {
|
||||||
proxy_pass http://127.0.0.1:5678/electrs-status;
|
proxy_pass http://127.0.0.1:5678/electrs-status;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
|
|||||||
4
neode-ui/package-lock.json
generated
4
neode-ui/package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "neode-ui",
|
"name": "neode-ui",
|
||||||
"version": "1.7.53-alpha",
|
"version": "1.7.54-alpha",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "neode-ui",
|
"name": "neode-ui",
|
||||||
"version": "1.7.53-alpha",
|
"version": "1.7.54-alpha",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/dompurify": "^3.0.5",
|
"@types/dompurify": "^3.0.5",
|
||||||
"@vue-leaflet/vue-leaflet": "^0.10.1",
|
"@vue-leaflet/vue-leaflet": "^0.10.1",
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "neode-ui",
|
"name": "neode-ui",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.7.53-alpha",
|
"version": "1.7.54-alpha",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "./start-dev.sh",
|
"start": "./start-dev.sh",
|
||||||
|
|||||||
@ -14,7 +14,7 @@ export const useAppStore = defineStore('app', () => {
|
|||||||
|
|
||||||
// Writable refs — delegate reads and writes to the sub-stores
|
// Writable refs — delegate reads and writes to the sub-stores
|
||||||
const { isAuthenticated, isLoading, error } = storeToRefs(auth)
|
const { isAuthenticated, isLoading, error } = storeToRefs(auth)
|
||||||
const { data, isConnected, isReconnecting } = storeToRefs(sync)
|
const { data, isConnected, isReconnecting, hasLoadedInitialData } = storeToRefs(sync)
|
||||||
|
|
||||||
// Read-only computed — delegate to sub-stores
|
// Read-only computed — delegate to sub-stores
|
||||||
const { serverInfo, packages, peerHealth, uiData } = storeToRefs(sync)
|
const { serverInfo, packages, peerHealth, uiData } = storeToRefs(sync)
|
||||||
@ -30,6 +30,7 @@ export const useAppStore = defineStore('app', () => {
|
|||||||
data,
|
data,
|
||||||
isConnected,
|
isConnected,
|
||||||
isReconnecting,
|
isReconnecting,
|
||||||
|
hasLoadedInitialData,
|
||||||
|
|
||||||
// Sync computed (read-only)
|
// Sync computed (read-only)
|
||||||
serverInfo,
|
serverInfo,
|
||||||
|
|||||||
@ -11,6 +11,7 @@ export const useSyncStore = defineStore('sync', () => {
|
|||||||
const data = ref<DataModel | null>(null)
|
const data = ref<DataModel | null>(null)
|
||||||
const isConnected = ref(false)
|
const isConnected = ref(false)
|
||||||
const isReconnecting = ref(false)
|
const isReconnecting = ref(false)
|
||||||
|
const hasLoadedInitialData = ref(false)
|
||||||
let isWsSubscribed = false
|
let isWsSubscribed = false
|
||||||
let isWsConnecting = false
|
let isWsConnecting = false
|
||||||
|
|
||||||
@ -47,12 +48,14 @@ export const useSyncStore = defineStore('sync', () => {
|
|||||||
if (update?.type === 'initial' && update?.data) {
|
if (update?.type === 'initial' && update?.data) {
|
||||||
if (import.meta.env.DEV) console.log('[Store] Received initial data from mock backend')
|
if (import.meta.env.DEV) console.log('[Store] Received initial data from mock backend')
|
||||||
data.value = update.data
|
data.value = update.data
|
||||||
|
hasLoadedInitialData.value = true
|
||||||
isConnected.value = true
|
isConnected.value = true
|
||||||
isReconnecting.value = false
|
isReconnecting.value = false
|
||||||
}
|
}
|
||||||
// Handle real backend format: {rev: 0, data: {...}}
|
// Handle real backend format: {rev: 0, data: {...}}
|
||||||
else if (update?.data && update?.rev !== undefined) {
|
else if (update?.data && update?.rev !== undefined) {
|
||||||
data.value = update.data
|
data.value = update.data
|
||||||
|
hasLoadedInitialData.value = true
|
||||||
isConnected.value = true
|
isConnected.value = true
|
||||||
isReconnecting.value = false
|
isReconnecting.value = false
|
||||||
}
|
}
|
||||||
@ -90,6 +93,7 @@ export const useSyncStore = defineStore('sync', () => {
|
|||||||
const freshState = await rpcClient.call<{ data: DataModel }>({ method: 'server.get-state' })
|
const freshState = await rpcClient.call<{ data: DataModel }>({ method: 'server.get-state' })
|
||||||
if (freshState?.data) {
|
if (freshState?.data) {
|
||||||
data.value = freshState.data
|
data.value = freshState.data
|
||||||
|
hasLoadedInitialData.value = true
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Non-fatal: WebSocket patches will still work
|
// Non-fatal: WebSocket patches will still work
|
||||||
@ -149,11 +153,13 @@ export const useSyncStore = defineStore('sync', () => {
|
|||||||
theme: 'dark',
|
theme: 'dark',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
hasLoadedInitialData.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Reset sync state on logout — called by auth store */
|
/** Reset sync state on logout — called by auth store */
|
||||||
function resetOnLogout(): void {
|
function resetOnLogout(): void {
|
||||||
data.value = null
|
data.value = null
|
||||||
|
hasLoadedInitialData.value = false
|
||||||
isWsSubscribed = false
|
isWsSubscribed = false
|
||||||
wsClient.disconnect()
|
wsClient.disconnect()
|
||||||
isConnected.value = false
|
isConnected.value = false
|
||||||
@ -165,6 +171,7 @@ export const useSyncStore = defineStore('sync', () => {
|
|||||||
data,
|
data,
|
||||||
isConnected,
|
isConnected,
|
||||||
isReconnecting,
|
isReconnecting,
|
||||||
|
hasLoadedInitialData,
|
||||||
|
|
||||||
// Computed
|
// Computed
|
||||||
serverInfo,
|
serverInfo,
|
||||||
|
|||||||
@ -267,8 +267,7 @@ const canLaunch = computed(() => {
|
|||||||
if (!pkg.value) return false
|
if (!pkg.value) return false
|
||||||
if (isWebOnly.value) return true
|
if (isWebOnly.value) return true
|
||||||
const hasUI = !!(pkg.value.manifest.interfaces?.main?.ui || pkg.value.installed?.['interface-addresses']?.main)
|
const hasUI = !!(pkg.value.manifest.interfaces?.main?.ui || pkg.value.installed?.['interface-addresses']?.main)
|
||||||
const isRunning = pkg.value.state === 'running'
|
return hasUI && pkg.value.state === 'running' && pkg.value.health !== 'starting' && pkg.value.health !== 'unhealthy'
|
||||||
return hasUI && isRunning
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const features = computed(() => [
|
const features = computed(() => [
|
||||||
|
|||||||
@ -40,19 +40,14 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Loading Skeleton -->
|
<!-- Loading Skeleton -->
|
||||||
<div v-if="!store.isConnected && sortedPackageEntries.length === 0 && !connectionError" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 pb-6">
|
<div v-if="isLoadingApps" class="text-center py-16 pb-6">
|
||||||
<div v-for="i in 3" :key="i" class="glass-card p-6 animate-pulse">
|
<div class="glass-card p-8 max-w-md mx-auto">
|
||||||
<div class="flex items-start gap-4">
|
<svg class="animate-spin h-8 w-8 mx-auto mb-4 text-white/70" viewBox="0 0 24 24" fill="none">
|
||||||
<div class="w-16 h-16 rounded-lg bg-white/10"></div>
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||||
<div class="flex-1">
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||||
<div class="h-5 w-32 bg-white/10 rounded mb-2"></div>
|
</svg>
|
||||||
<div class="h-4 w-48 bg-white/5 rounded mb-3"></div>
|
<h3 class="text-lg font-semibold text-white mb-2">Loading apps</h3>
|
||||||
<div class="h-6 w-20 bg-white/5 rounded"></div>
|
<p class="text-white/60 text-sm">Checking the latest app status before showing launch controls.</p>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mt-4 flex gap-2">
|
|
||||||
<div class="flex-1 h-9 bg-white/5 rounded-lg"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -222,6 +217,8 @@ const packages = computed(() => {
|
|||||||
|
|
||||||
const categoriesWithApps = useCategoriesWithApps(packages, ALL_CATEGORIES)
|
const categoriesWithApps = useCategoriesWithApps(packages, ALL_CATEGORIES)
|
||||||
|
|
||||||
|
const isLoadingApps = computed(() => !store.hasLoadedInitialData && !connectionError.value)
|
||||||
|
|
||||||
// Connection error state
|
// Connection error state
|
||||||
const connectionError = ref('')
|
const connectionError = ref('')
|
||||||
let connectionTimer: ReturnType<typeof setTimeout> | undefined
|
let connectionTimer: ReturnType<typeof setTimeout> | undefined
|
||||||
@ -230,7 +227,7 @@ onMounted(() => {
|
|||||||
appsAnimationDone = true
|
appsAnimationDone = true
|
||||||
if (!store.isConnected) {
|
if (!store.isConnected) {
|
||||||
connectionTimer = setTimeout(() => {
|
connectionTimer = setTimeout(() => {
|
||||||
if (!store.isConnected && sortedPackageEntries.value.length === 0) {
|
if (!store.hasLoadedInitialData && sortedPackageEntries.value.length === 0) {
|
||||||
connectionError.value = 'Unable to connect to server. Check that the backend is running.'
|
connectionError.value = 'Unable to connect to server. Check that the backend is running.'
|
||||||
}
|
}
|
||||||
}, 15000)
|
}, 15000)
|
||||||
|
|||||||
@ -34,7 +34,7 @@
|
|||||||
<template v-if="mustOpenNewTab">{{ appTitle }} sets security headers that prevent iframe embedding.<br>Open it in a new browser tab instead.</template>
|
<template v-if="mustOpenNewTab">{{ appTitle }} sets security headers that prevent iframe embedding.<br>Open it in a new browser tab instead.</template>
|
||||||
<template v-else>{{ appTitle }} may still be starting up or the container is stopped.<br><span v-if="autoRetryCount > 0" class="text-yellow-400/70">Retrying automatically ({{ autoRetryCount }})...</span></template>
|
<template v-else>{{ appTitle }} may still be starting up or the container is stopped.<br><span v-if="autoRetryCount > 0" class="text-yellow-400/70">Retrying automatically ({{ autoRetryCount }})...</span></template>
|
||||||
</p>
|
</p>
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex flex-wrap items-center justify-center gap-3">
|
||||||
<button
|
<button
|
||||||
v-if="!mustOpenNewTab"
|
v-if="!mustOpenNewTab"
|
||||||
@click="$emit('refresh')"
|
@click="$emit('refresh')"
|
||||||
|
|||||||
@ -145,8 +145,7 @@ export function resolveAppIcon(id: string, pkg: PackageDataEntry, curatedIcon?:
|
|||||||
export function canLaunch(pkg: PackageDataEntry): boolean {
|
export function canLaunch(pkg: PackageDataEntry): boolean {
|
||||||
if (isWebOnlyApp(pkg.manifest.id)) return true
|
if (isWebOnlyApp(pkg.manifest.id)) return true
|
||||||
const hasUI = pkg.manifest.interfaces?.main?.ui || pkg.installed?.['interface-addresses']?.main
|
const hasUI = pkg.manifest.interfaces?.main?.ui || pkg.installed?.['interface-addresses']?.main
|
||||||
const canLaunchState = pkg.state === 'running' || pkg.state === 'starting'
|
return !!hasUI && pkg.state === 'running' && pkg.health !== 'starting' && pkg.health !== 'unhealthy'
|
||||||
return !!hasUI && canLaunchState
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getStatusClass(state: PackageState, health?: string | null, exitCode?: number | null): string {
|
export function getStatusClass(state: PackageState, health?: string | null, exitCode?: number | null): string {
|
||||||
|
|||||||
@ -1,27 +1,29 @@
|
|||||||
{
|
{
|
||||||
"version": "1.7.53-alpha",
|
"version": "1.7.54-alpha",
|
||||||
"release_date": "2026-05-05",
|
"release_date": "2026-05-06",
|
||||||
"changelog": [
|
"changelog": [
|
||||||
"Bitcoin Knots/Core config generation no longer duplicates RPC bind and port settings between `bitcoin.conf` and container command args, fixing `Unable to bind all endpoints for RPC server` startup failures.",
|
"Existing installs now self-repair nginx backend proxy locations for `/bitcoin-status` and `/api/app-catalog`, including hosts where `sites-enabled/archipelago` is a copied active file instead of a symlink.",
|
||||||
"Legacy Bitcoin container healthchecks no longer depend on `bitcoin-cli`, which is absent from current Knots images and can wedge Podman healthcheck runners.",
|
"LND UI is consistently served on `18083` across first boot, Tor config, companion Quadlet reconciliation, OTA runtime payloads, and ISO scripts; stale companion units/images are rewritten instead of only checking service active state.",
|
||||||
"Update checks now prefer manifest OTA releases over stale git remotes unless `ARCHIPELAGO_GIT_UPDATES` is explicitly enabled, so installed nodes can see published releases from the VPS mirror."
|
"OTA frontend tarballs now carry a clean runtime payload with updated scripts, docker UI sources, and canonical nginx config, preventing startup promotion from reintroducing stale host assets.",
|
||||||
|
"Release ISO builds now support the primary HTTP app registry when bundling core images, so unbundled media includes File Browser/Cloud support instead of requiring a post-install Marketplace download.",
|
||||||
|
"`.116` was live-updated with the new backend and runtime scripts; focused non-destructive lifecycle audit passes for Bitcoin Knots, LND, BTCPay, Mempool, and Grafana."
|
||||||
],
|
],
|
||||||
"components": [
|
"components": [
|
||||||
{
|
{
|
||||||
"name": "archipelago",
|
"name": "archipelago",
|
||||||
"current_version": "1.7.53-alpha",
|
"current_version": "1.7.54-alpha",
|
||||||
"new_version": "1.7.53-alpha",
|
"new_version": "1.7.54-alpha",
|
||||||
"download_url": "https://git.tx1138.com/lfg2025/archy/raw/branch/main/releases/v1.7.53-alpha/archipelago",
|
"download_url": "https://git.tx1138.com/lfg2025/archy/raw/branch/main/releases/v1.7.54-alpha/archipelago",
|
||||||
"sha256": "86cf408ed84c7a7a72d1b5529aa97561dd02db38aab57c523999d1f5e7bf48b7",
|
"sha256": "77e3a236a6196a5ab9ec2411b150490e78ffc95ea6ab8eb34ab29b3df53cd632",
|
||||||
"size_bytes": 42352112
|
"size_bytes": 42600560
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "archipelago-frontend-1.7.53-alpha.tar.gz",
|
"name": "archipelago-frontend-1.7.54-alpha.tar.gz",
|
||||||
"current_version": "1.7.53-alpha",
|
"current_version": "1.7.54-alpha",
|
||||||
"new_version": "1.7.53-alpha",
|
"new_version": "1.7.54-alpha",
|
||||||
"download_url": "https://git.tx1138.com/lfg2025/archy/raw/branch/main/releases/v1.7.53-alpha/archipelago-frontend-1.7.53-alpha.tar.gz",
|
"download_url": "https://git.tx1138.com/lfg2025/archy/raw/branch/main/releases/v1.7.54-alpha/archipelago-frontend-1.7.54-alpha.tar.gz",
|
||||||
"sha256": "87590acd32cb79866d39d87f37c7a91d85774d06aa318352b24d2b2177ccac31",
|
"sha256": "a010ac43a2dd02f528202cb2f7b99b61ceab80adc6827877594e41df4ea951fb",
|
||||||
"size_bytes": 166460672
|
"size_bytes": 166461921
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -551,7 +551,7 @@ load_spec_archy-lnd-ui() {
|
|||||||
reset_spec
|
reset_spec
|
||||||
SPEC_NAME="archy-lnd-ui"
|
SPEC_NAME="archy-lnd-ui"
|
||||||
SPEC_IMAGE="localhost/lnd-ui:local"
|
SPEC_IMAGE="localhost/lnd-ui:local"
|
||||||
SPEC_PORTS="8081:80"
|
SPEC_PORTS="18083:80"
|
||||||
SPEC_MEMORY="$(mem_limit archy-lnd-ui)"
|
SPEC_MEMORY="$(mem_limit archy-lnd-ui)"
|
||||||
SPEC_TIER="4"
|
SPEC_TIER="4"
|
||||||
SPEC_LOCAL_IMAGE="true"
|
SPEC_LOCAL_IMAGE="true"
|
||||||
|
|||||||
@ -109,8 +109,15 @@ if [ -z "$FRONTEND_ARCHIVE" ]; then
|
|||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
if [ -f "$PROJECT_ROOT/image-recipe/configs/nginx-archipelago.conf" ]; then
|
||||||
|
mkdir -p "$RUNTIME_DIR/image-recipe/configs"
|
||||||
|
echo " Including runtime nginx-archipelago.conf"
|
||||||
|
cp "$PROJECT_ROOT/image-recipe/configs/nginx-archipelago.conf" \
|
||||||
|
"$RUNTIME_DIR/image-recipe/configs/nginx-archipelago.conf"
|
||||||
|
fi
|
||||||
rm -rf "$RUNTIME_DIR/scripts/resilience/reports"
|
rm -rf "$RUNTIME_DIR/scripts/resilience/reports"
|
||||||
find "$RUNTIME_DIR" -type f \( -name '*.bak' -o -name '._*' -o -name '*.log' \) -delete
|
find "$RUNTIME_DIR" -type d -name '__pycache__' -prune -exec rm -rf {} +
|
||||||
|
find "$RUNTIME_DIR" -type f \( -name '*.bak' -o -name '*.bak-*' -o -name '._*' -o -name '*.log' -o -name '*.pyc' \) -delete
|
||||||
# Force world-readable perms on every entry BEFORE tar, so the
|
# Force world-readable perms on every entry BEFORE tar, so the
|
||||||
# archive's internal mode bits are 755/644 regardless of what
|
# archive's internal mode bits are 755/644 regardless of what
|
||||||
# the staging dir's umask gave us. Without this, mktemp -d
|
# the staging dir's umask gave us. Without this, mktemp -d
|
||||||
|
|||||||
@ -944,7 +944,7 @@ LNDCONF
|
|||||||
fi
|
fi
|
||||||
case \$ui in
|
case \$ui in
|
||||||
bitcoin-ui) PORT_ARG=''; NET_ARG='--network host' ;;
|
bitcoin-ui) PORT_ARG=''; NET_ARG='--network host' ;;
|
||||||
lnd-ui) PORT_ARG='-p 8081:80'; NET_ARG='' ;;
|
lnd-ui) PORT_ARG='-p 18083:80'; NET_ARG='' ;;
|
||||||
electrs-ui) PORT_ARG=''; NET_ARG='--network host' ;;
|
electrs-ui) PORT_ARG=''; NET_ARG='--network host' ;;
|
||||||
esac
|
esac
|
||||||
if [ -d \"$TARGET_DIR/docker/\$ui\" ]; then
|
if [ -d \"$TARGET_DIR/docker/\$ui\" ]; then
|
||||||
|
|||||||
@ -1003,14 +1003,14 @@ BUILDINFO_EOF
|
|||||||
if false; then # Legacy app installation removed — kept for reference in git history
|
if false; then # Legacy app installation removed — kept for reference in git history
|
||||||
progress "Rebuilding LND UI"
|
progress "Rebuilding LND UI"
|
||||||
if ssh $SSH_OPTS "$TARGET_HOST" "cd $TARGET_DIR/docker/lnd-ui && (command -v podman >/dev/null 2>&1 && podman build --no-cache -t lnd-ui:local . || docker build --no-cache -t lnd-ui:local .)" 2>&1 | tail -12 | sed 's/^/ /'; then
|
if ssh $SSH_OPTS "$TARGET_HOST" "cd $TARGET_DIR/docker/lnd-ui && (command -v podman >/dev/null 2>&1 && podman build --no-cache -t lnd-ui:local . || docker build --no-cache -t lnd-ui:local .)" 2>&1 | tail -12 | sed 's/^/ /'; then
|
||||||
echo " Recreating LND UI container (port 8081)..."
|
echo " Recreating LND UI container (port 18083)..."
|
||||||
ssh $SSH_OPTS "$TARGET_HOST" '
|
ssh $SSH_OPTS "$TARGET_HOST" '
|
||||||
DOCKER=podman
|
DOCKER=podman
|
||||||
command -v podman >/dev/null 2>&1 || DOCKER=docker
|
command -v podman >/dev/null 2>&1 || DOCKER=docker
|
||||||
for c in $($DOCKER ps -a --format "{{.Names}}" 2>/dev/null | grep -i lnd-ui); do
|
for c in $($DOCKER ps -a --format "{{.Names}}" 2>/dev/null | grep -i lnd-ui); do
|
||||||
[ -n "$c" ] && $DOCKER stop "$c" 2>/dev/null; $DOCKER rm -f "$c" 2>/dev/null
|
[ -n "$c" ] && $DOCKER stop "$c" 2>/dev/null; $DOCKER rm -f "$c" 2>/dev/null
|
||||||
done
|
done
|
||||||
$DOCKER run -d --name archy-lnd-ui -p 8081:80 --memory=256m --restart unless-stopped lnd-ui:local
|
$DOCKER run -d --name archy-lnd-ui -p 18083:80 --memory=256m --restart unless-stopped lnd-ui:local
|
||||||
' 2>&1 | sed 's/^/ /' || true
|
' 2>&1 | sed 's/^/ /' || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
14
scripts/first-boot-containers.sh
Normal file → Executable file
14
scripts/first-boot-containers.sh
Normal file → Executable file
@ -464,8 +464,8 @@ done
|
|||||||
for dir in mempool mysql-mempool; do
|
for dir in mempool mysql-mempool; do
|
||||||
[ -d "/var/lib/archipelago/$dir" ] && chown -R 100999:100999 "/var/lib/archipelago/$dir" 2>/dev/null
|
[ -d "/var/lib/archipelago/$dir" ] && chown -R 100999:100999 "/var/lib/archipelago/$dir" 2>/dev/null
|
||||||
done
|
done
|
||||||
# Grafana: container UID 472 → host UID 100472
|
# Grafana: chown inside podman's user namespace so container UID 472 can write SQLite.
|
||||||
[ -d /var/lib/archipelago/grafana ] && chown -R 100472:100472 /var/lib/archipelago/grafana 2>/dev/null
|
[ -d /var/lib/archipelago/grafana ] && podman unshare chown -R 472:472 /var/lib/archipelago/grafana 2>/dev/null || true
|
||||||
log "UID mapping done"
|
log "UID mapping done"
|
||||||
|
|
||||||
# ── Memory limits per container ──────────────────────────────────────────
|
# ── Memory limits per container ──────────────────────────────────────────
|
||||||
@ -995,9 +995,9 @@ track_container "homeassistant"
|
|||||||
if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q grafana; then
|
if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q grafana; then
|
||||||
log "Creating Grafana..."
|
log "Creating Grafana..."
|
||||||
mkdir -p /var/lib/archipelago/grafana
|
mkdir -p /var/lib/archipelago/grafana
|
||||||
chown 472:472 /var/lib/archipelago/grafana 2>/dev/null || true
|
podman unshare chown -R 472:472 /var/lib/archipelago/grafana 2>/dev/null || true
|
||||||
$DOCKER run -d --name grafana --restart unless-stopped \
|
$DOCKER run -d --name grafana --restart unless-stopped \
|
||||||
--health-cmd="curl -sf http://localhost:3000/api/health || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \
|
--health-cmd="test -w /var/lib/grafana && test -w /var/lib/grafana/grafana.db && curl -sf http://localhost:3000/api/health || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \
|
||||||
--memory=$(mem_limit grafana) \
|
--memory=$(mem_limit grafana) \
|
||||||
--cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID \
|
--cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID \
|
||||||
--security-opt no-new-privileges:true \
|
--security-opt no-new-privileges:true \
|
||||||
@ -1247,9 +1247,9 @@ for ui in bitcoin-ui lnd-ui electrs-ui; do
|
|||||||
fi
|
fi
|
||||||
case $ui in
|
case $ui in
|
||||||
# UI containers use --network host so they can proxy to localhost services
|
# UI containers use --network host so they can proxy to localhost services
|
||||||
# Internal nginx ports: bitcoin-ui=8334, electrs-ui=50002, lnd-ui=80 (host 8081)
|
# Internal nginx ports: bitcoin-ui=8334, electrs-ui=50002, lnd-ui=80 (host 18083)
|
||||||
bitcoin-ui) PORT_ARG=""; NET_ARG="--network host"; REG_IMG="${BITCOIN_UI_IMAGE}" ;;
|
bitcoin-ui) PORT_ARG=""; NET_ARG="--network host"; REG_IMG="${BITCOIN_UI_IMAGE}" ;;
|
||||||
lnd-ui) PORT_ARG="-p 8081:80"; NET_ARG=""; REG_IMG="${LND_UI_IMAGE}" ;;
|
lnd-ui) PORT_ARG="-p 18083:80"; NET_ARG=""; REG_IMG="${LND_UI_IMAGE}" ;;
|
||||||
electrs-ui) PORT_ARG=""; NET_ARG="--network host"; REG_IMG="${ELECTRS_UI_IMAGE}" ;;
|
electrs-ui) PORT_ARG=""; NET_ARG="--network host"; REG_IMG="${ELECTRS_UI_IMAGE}" ;;
|
||||||
esac
|
esac
|
||||||
CONTAINER_NAME="archy-$ui"
|
CONTAINER_NAME="archy-$ui"
|
||||||
@ -1284,7 +1284,7 @@ if [ ! -f "$SERVICES_JSON" ]; then
|
|||||||
cat > "$SERVICES_JSON" <<'SJSON'
|
cat > "$SERVICES_JSON" <<'SJSON'
|
||||||
{"services":[
|
{"services":[
|
||||||
{"name":"archipelago","local_port":80,"enabled":true},
|
{"name":"archipelago","local_port":80,"enabled":true},
|
||||||
{"name":"lnd","local_port":8081,"enabled":true},
|
{"name":"lnd","local_port":18083,"enabled":true},
|
||||||
{"name":"btcpay","local_port":23000,"enabled":true},
|
{"name":"btcpay","local_port":23000,"enabled":true},
|
||||||
{"name":"mempool","local_port":4080,"enabled":true},
|
{"name":"mempool","local_port":4080,"enabled":true},
|
||||||
{"name":"fedimint","local_port":8175,"enabled":true}
|
{"name":"fedimint","local_port":8175,"enabled":true}
|
||||||
|
|||||||
@ -7,7 +7,7 @@ Each service gets its own .onion address. Tor runs in a container with host netw
|
|||||||
| Service | LAN Port | Tor Hidden Service Dir |
|
| Service | LAN Port | Tor Hidden Service Dir |
|
||||||
|-----------|----------|-------------------------------|
|
|-----------|----------|-------------------------------|
|
||||||
| Archipelago | 80 | hidden_service_archipelago |
|
| Archipelago | 80 | hidden_service_archipelago |
|
||||||
| LND UI | 8081 | hidden_service_lnd |
|
| LND UI | 18083 | hidden_service_lnd |
|
||||||
| BTCPay | 23000 | hidden_service_btcpay |
|
| BTCPay | 23000 | hidden_service_btcpay |
|
||||||
| Mempool | 4080 | hidden_service_mempool |
|
| Mempool | 4080 | hidden_service_mempool |
|
||||||
| Fedimint | 8175 | hidden_service_fedimint |
|
| Fedimint | 8175 | hidden_service_fedimint |
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user