Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5818541721 | ||
|
|
b8053c00ca | ||
|
|
f95e9a1cd0 | ||
|
|
be50dc3235 | ||
|
|
2ff47f88a7 |
@@ -7,6 +7,14 @@
|
||||
# Allow demo assets (AIUI pre-built dist)
|
||||
!demo/
|
||||
|
||||
# Allow backend source for ISO source builds
|
||||
!core/
|
||||
!scripts/
|
||||
!image-recipe/
|
||||
image-recipe/build/
|
||||
image-recipe/results/
|
||||
image-recipe/output/
|
||||
|
||||
# Exclude nested node_modules (will npm install in container)
|
||||
neode-ui/node_modules
|
||||
neode-ui/dist
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# Changelog
|
||||
|
||||
## v1.7.56-alpha (2026-05-14)
|
||||
|
||||
- Health notifications now clear when an app is no longer unhealthy, including stale alerts for removed containers such as Portainer.
|
||||
- Quadlet environment values with spaces or shell metacharacters are quoted consistently, preventing env drift recreate loops for apps like nostr-rs-relay and Grafana.
|
||||
- Boot/bootstrap reconcile avoids restarting running Bitcoin containers while repairing RPC config, preserving IBD progress on active nodes.
|
||||
- Exit code 137 is labeled as SIGKILL instead of assuming OOM, avoiding false OOM alerts for orchestrator-managed recreates.
|
||||
|
||||
## v1.7.55-alpha (2026-05-13)
|
||||
|
||||
- Container reconcile now force-recreates Podman records stuck in `Stopping`, preserving bind-mounted app data while recovering wedged containers automatically.
|
||||
|
||||
@@ -30,7 +30,7 @@ app:
|
||||
RPC_PASS="$(printenv BITCOIN_RPC_PASS)";
|
||||
DISK_GB_VALUE="$(printenv DISK_GB || true)";
|
||||
if [ "${DISK_GB_VALUE:-0}" -lt 1000 ]; then
|
||||
exec "$BITCOIND" -datadir=/home/bitcoin/.bitcoin -noconf -server=1 -prune=550 -rpcallowip=0.0.0.0/0 -rpcbind=0.0.0.0:8332 -listen=1 -bind=0.0.0.0:8333 -dbcache=1024 -par=0 -maxconnections=125 -rpcuser="$RPC_USER" -rpcpassword="$RPC_PASS";
|
||||
exec "$BITCOIND" -datadir=/home/bitcoin/.bitcoin -noconf -server=1 -prune=550 -rpcallowip=0.0.0.0/0 -rpcbind=0.0.0.0:8332 -listen=1 -bind=0.0.0.0:8333 -dbcache=2048 -par=0 -maxconnections=125 -rpcuser="$RPC_USER" -rpcpassword="$RPC_PASS";
|
||||
else
|
||||
exec "$BITCOIND" -datadir=/home/bitcoin/.bitcoin -noconf -server=1 -txindex=1 -rpcallowip=0.0.0.0/0 -rpcbind=0.0.0.0:8332 -listen=1 -bind=0.0.0.0:8333 -dbcache=4096 -par=0 -maxconnections=125 -rpcuser="$RPC_USER" -rpcpassword="$RPC_PASS";
|
||||
fi
|
||||
|
||||
Generated
+1
-1
@@ -80,7 +80,7 @@ checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
|
||||
|
||||
[[package]]
|
||||
name = "archipelago"
|
||||
version = "1.7.55-alpha"
|
||||
version = "1.7.56-alpha"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"archipelago-container",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "archipelago"
|
||||
version = "1.7.55-alpha"
|
||||
version = "1.7.56-alpha"
|
||||
edition = "2021"
|
||||
description = "Archipelago Bitcoin Node OS - Native backend"
|
||||
authors = ["Archipelago Team"]
|
||||
|
||||
@@ -65,7 +65,9 @@ pub async fn ensure_doctor_installed() {
|
||||
Err(e) => warn!("Nginx bootstrap failed (non-fatal): {:#}", e),
|
||||
}
|
||||
match run_bitcoin_rpc_repair().await {
|
||||
Ok(true) => info!("Repaired Bitcoin RPC bind settings and restarted Bitcoin containers"),
|
||||
Ok(true) => {
|
||||
info!("Repaired Bitcoin RPC bind settings; running Bitcoin containers left untouched")
|
||||
}
|
||||
Ok(false) => debug!("Bitcoin RPC bind settings already usable"),
|
||||
Err(e) => warn!("Bitcoin RPC repair failed (non-fatal): {:#}", e),
|
||||
}
|
||||
@@ -334,15 +336,11 @@ exit 2
|
||||
.context("repair bitcoin.conf RPC bind settings")?;
|
||||
match status.code() {
|
||||
Some(0) => Ok(false),
|
||||
Some(2) => {
|
||||
for name in ["bitcoin-knots", "bitcoin-core", "archy-bitcoin-ui"] {
|
||||
let _ = tokio::process::Command::new("podman")
|
||||
.args(["restart", name])
|
||||
.status()
|
||||
.await;
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
// Do not restart Bitcoin from bootstrap. During IBD, an automatic
|
||||
// restart can cost hours of progress. The repaired file is only a
|
||||
// fallback for future starts; current containers keep their command-line
|
||||
// RPC args until an operator or update intentionally restarts them.
|
||||
Some(2) => Ok(true),
|
||||
_ => {
|
||||
warn!("Bitcoin RPC repair helper exited with {}", status);
|
||||
Ok(false)
|
||||
|
||||
@@ -62,6 +62,19 @@ fn is_required_baseline_app(app_id: &str) -> bool {
|
||||
)
|
||||
}
|
||||
|
||||
fn is_restart_sensitive_app(app_id: &str) -> bool {
|
||||
matches!(
|
||||
app_id,
|
||||
"bitcoin-knots"
|
||||
| "bitcoin-core"
|
||||
| "bitcoin"
|
||||
| "lnd"
|
||||
| "btcpay-server"
|
||||
| "fedimint"
|
||||
| "fedimint-gateway"
|
||||
)
|
||||
}
|
||||
|
||||
fn requires_archival_bitcoin(app_id: &str) -> bool {
|
||||
matches!(
|
||||
app_id,
|
||||
@@ -713,6 +726,17 @@ impl ProdContainerOrchestrator {
|
||||
return Ok(ReconcileAction::Started);
|
||||
}
|
||||
if self.container_env_drifted(&name, &resolved_manifest).await {
|
||||
if mode == ReconcileMode::ExistingOnly
|
||||
&& is_restart_sensitive_app(&app_id)
|
||||
{
|
||||
tracing::info!(
|
||||
app_id = %app_id,
|
||||
container = %name,
|
||||
"container drift detected during boot reconcile; leaving running restart-sensitive app untouched"
|
||||
);
|
||||
self.run_post_start_hooks(&app_id).await?;
|
||||
return Ok(ReconcileAction::NoOp);
|
||||
}
|
||||
tracing::info!(app_id = %app_id, container = %name, "container env drift detected — recreating");
|
||||
let _ = self.runtime.stop_container(&name).await;
|
||||
let _ = self.runtime.remove_container(&name).await;
|
||||
@@ -2252,6 +2276,7 @@ mod tests {
|
||||
runtime,
|
||||
PathBuf::from("/nonexistent-for-tests"),
|
||||
);
|
||||
orch.set_data_dir(PathBuf::from("/nonexistent-for-tests"));
|
||||
// Redirect the bitcoin-ui pre-start hook to a test-scoped
|
||||
// tmpdir, seeded with a fake password file. Shared across
|
||||
// every test in this module (OnceLock), so the hook can run
|
||||
@@ -2259,6 +2284,7 @@ mod tests {
|
||||
// this redirection, any test that installs the bitcoin-ui
|
||||
// fixture would try to write under /var/lib/archipelago.
|
||||
orch.set_bitcoin_ui_paths(test_bitcoin_ui_paths());
|
||||
orch.set_filebrowser_paths(test_filebrowser_paths());
|
||||
orch
|
||||
}
|
||||
|
||||
@@ -2339,6 +2365,17 @@ app:
|
||||
}
|
||||
}
|
||||
|
||||
fn test_filebrowser_paths() -> filebrowser::EnsurePaths {
|
||||
use std::sync::OnceLock;
|
||||
static DIR: OnceLock<tempfile::TempDir> = OnceLock::new();
|
||||
let dir = DIR.get_or_init(|| tempfile::TempDir::new().expect("test tmpdir"));
|
||||
filebrowser::EnsurePaths {
|
||||
srv_root: dir.path().join("filebrowser"),
|
||||
data_dir: dir.path().join("filebrowser-data"),
|
||||
config_path: dir.path().join("filebrowser-data/.filebrowser.json"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn install_fresh_pull() {
|
||||
let rt = Arc::new(MockRuntime::default());
|
||||
|
||||
@@ -222,7 +222,7 @@ impl QuadletUnit {
|
||||
for env in &self.environment {
|
||||
// env entries already arrive shaped as "KEY=VALUE"; quadlet
|
||||
// accepts that form on a single Environment= line per pair.
|
||||
let _ = writeln!(s, "Environment={env}");
|
||||
let _ = writeln!(s, "Environment={}", quote_environment(env));
|
||||
}
|
||||
for dev in &self.devices {
|
||||
let _ = writeln!(s, "AddDevice={dev}");
|
||||
@@ -296,6 +296,23 @@ fn shell_join(parts: &[String]) -> String {
|
||||
.join(" ")
|
||||
}
|
||||
|
||||
fn quote_environment(env: &str) -> String {
|
||||
let env = env.replace(['\r', '\n'], " ");
|
||||
if env.is_empty()
|
||||
|| env
|
||||
.chars()
|
||||
.any(|c| c.is_whitespace() || "\"\\$`".contains(c))
|
||||
{
|
||||
let escaped = env
|
||||
.replace('\\', "\\\\")
|
||||
.replace('"', "\\\"")
|
||||
.replace('$', "$$");
|
||||
format!("\"{escaped}\"")
|
||||
} else {
|
||||
env
|
||||
}
|
||||
}
|
||||
|
||||
impl QuadletUnit {
|
||||
/// Build a backend-flavour QuadletUnit from a parsed AppManifest.
|
||||
/// Wired through `prod_orchestrator::install_via_quadlet`, gated by
|
||||
@@ -778,6 +795,22 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quote_environment_quotes_values_with_spaces() {
|
||||
assert_eq!(
|
||||
quote_environment("BITCOIN_RPC_PASS=secret"),
|
||||
"BITCOIN_RPC_PASS=secret"
|
||||
);
|
||||
assert_eq!(
|
||||
quote_environment("RELAY_NAME=Archipelago Nostr Relay"),
|
||||
"\"RELAY_NAME=Archipelago Nostr Relay\""
|
||||
);
|
||||
assert_eq!(
|
||||
quote_environment("GREETING=say \"hi\""),
|
||||
"\"GREETING=say \\\"hi\\\"\""
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn restart_policy_emits_correct_systemd_string() {
|
||||
assert_eq!(RestartPolicy::Always.as_systemd(), "always");
|
||||
@@ -797,6 +830,7 @@ mod tests {
|
||||
environment: vec![
|
||||
"BITCOIN_RPC_USER=archipelago".into(),
|
||||
"BITCOIN_RPC_PASS=secret".into(),
|
||||
"RELAY_NAME=Archipelago Nostr Relay".into(),
|
||||
],
|
||||
devices: vec!["/dev/kvm".into()],
|
||||
add_hosts: vec![("host.archipelago".into(), "10.89.0.1".into())],
|
||||
@@ -813,6 +847,7 @@ mod tests {
|
||||
assert!(s.contains("PublishPort=8333:8333/tcp"));
|
||||
assert!(s.contains("Environment=BITCOIN_RPC_USER=archipelago"));
|
||||
assert!(s.contains("Environment=BITCOIN_RPC_PASS=secret"));
|
||||
assert!(s.contains("Environment=\"RELAY_NAME=Archipelago Nostr Relay\""));
|
||||
assert!(s.contains("AddDevice=/dev/kvm"));
|
||||
assert!(s.contains("AddHost=host.archipelago:10.89.0.1"));
|
||||
assert!(s.contains("ReadOnly=true"));
|
||||
|
||||
@@ -8,7 +8,7 @@ use crate::data_model::{Notification, NotificationLevel, PackageState};
|
||||
use crate::state::StateManager;
|
||||
use crate::webhooks::{self, WebhookEvent};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
@@ -420,6 +420,9 @@ async fn check_containers() -> Vec<ContainerHealth> {
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let containers: Vec<serde_json::Value> = serde_json::from_str(&stdout).unwrap_or_default();
|
||||
|
||||
let live_container_ids = live_container_ids(&containers);
|
||||
cleanup_stale_podman_healthcheck_units(&live_container_ids).await;
|
||||
|
||||
// Monitor ALL long-running containers for health — backend services (databases,
|
||||
// nbxplorer, mempool-api) and UI containers need auto-restart too.
|
||||
// Only skip ephemeral containers (build infrastructure, init one-shots).
|
||||
@@ -462,6 +465,161 @@ async fn check_containers() -> Vec<ContainerHealth> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn live_container_ids(containers: &[serde_json::Value]) -> HashSet<String> {
|
||||
containers
|
||||
.iter()
|
||||
.filter_map(|c| {
|
||||
c.get("Id")
|
||||
.or_else(|| c.get("ID"))
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string())
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
async fn cleanup_stale_podman_healthcheck_units(live_container_ids: &HashSet<String>) {
|
||||
if live_container_ids.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut units = stale_healthcheck_units_from_systemd(live_container_ids).await;
|
||||
if units.is_empty() {
|
||||
return;
|
||||
}
|
||||
units.sort();
|
||||
units.dedup();
|
||||
|
||||
let mut cleaned = 0;
|
||||
for unit in units {
|
||||
let Some(container_id) = parse_podman_healthcheck_unit(&unit) else {
|
||||
continue;
|
||||
};
|
||||
let service = format!("{}.service", unit.trim_end_matches(".timer"));
|
||||
if stop_user_unit(&unit).await {
|
||||
cleaned += 1;
|
||||
}
|
||||
let _ = stop_user_unit(&service).await;
|
||||
let _ = reset_failed_user_unit(&service).await;
|
||||
debug!(
|
||||
"Stopped stale Podman healthcheck unit {} for removed container {}",
|
||||
unit, container_id
|
||||
);
|
||||
}
|
||||
|
||||
if cleaned > 0 {
|
||||
info!("Cleaned {} stale Podman healthcheck timer(s)", cleaned);
|
||||
}
|
||||
}
|
||||
|
||||
async fn stale_healthcheck_units_from_systemd(live_container_ids: &HashSet<String>) -> Vec<String> {
|
||||
let mut units = Vec::new();
|
||||
for args in [
|
||||
[
|
||||
"--user",
|
||||
"list-timers",
|
||||
"--all",
|
||||
"--no-legend",
|
||||
"--no-pager",
|
||||
]
|
||||
.as_slice(),
|
||||
["--user", "list-units", "--all", "--no-legend", "--no-pager"].as_slice(),
|
||||
] {
|
||||
let output = match tokio::time::timeout(
|
||||
std::time::Duration::from_secs(20),
|
||||
tokio::process::Command::new("systemctl")
|
||||
.args(args.iter().copied())
|
||||
.output(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(Ok(output)) if output.status.success() => output,
|
||||
Ok(Ok(output)) => {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
debug!("systemctl {} failed: {}", args.join(" "), stderr.trim());
|
||||
continue;
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
debug!("Failed to run systemctl {}: {}", args.join(" "), e);
|
||||
continue;
|
||||
}
|
||||
Err(_) => {
|
||||
debug!("systemctl {} timed out", args.join(" "));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
units.extend(stale_healthcheck_units(&stdout, live_container_ids));
|
||||
}
|
||||
units
|
||||
}
|
||||
|
||||
fn stale_healthcheck_units(output: &str, live_container_ids: &HashSet<String>) -> Vec<String> {
|
||||
output
|
||||
.lines()
|
||||
.flat_map(|line| line.split_whitespace())
|
||||
.filter_map(|token| {
|
||||
let unit = token.trim_start_matches('●');
|
||||
let id = parse_podman_healthcheck_unit(unit)?;
|
||||
(!live_container_ids.contains(id)).then(|| unit.to_string())
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn parse_podman_healthcheck_unit(unit: &str) -> Option<&str> {
|
||||
let unit = unit
|
||||
.strip_suffix(".timer")
|
||||
.or_else(|| unit.strip_suffix(".service"))?;
|
||||
let (container_id, _suffix) = unit.split_once('-')?;
|
||||
if container_id.len() == 64 && container_id.bytes().all(|b| b.is_ascii_hexdigit()) {
|
||||
Some(container_id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
async fn stop_user_unit(unit: &str) -> bool {
|
||||
run_systemctl_user(["stop", unit]).await
|
||||
}
|
||||
|
||||
async fn reset_failed_user_unit(unit: &str) -> bool {
|
||||
run_systemctl_user(["reset-failed", unit]).await
|
||||
}
|
||||
|
||||
async fn run_systemctl_user<const N: usize>(args: [&str; N]) -> bool {
|
||||
let output = match tokio::time::timeout(
|
||||
std::time::Duration::from_secs(10),
|
||||
tokio::process::Command::new("systemctl")
|
||||
.arg("--user")
|
||||
.args(args.iter().copied())
|
||||
.output(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(Ok(output)) => output,
|
||||
Ok(Err(e)) => {
|
||||
debug!("Failed to run systemctl --user {}: {}", args.join(" "), e);
|
||||
return false;
|
||||
}
|
||||
Err(_) => {
|
||||
debug!("systemctl --user {} timed out", args.join(" "));
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
if output.status.success() {
|
||||
true
|
||||
} else {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
debug!(
|
||||
"systemctl --user {} failed: {}",
|
||||
args.join(" "),
|
||||
stderr.trim()
|
||||
);
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_podman_health(c: &serde_json::Value, state: &str) -> Option<String> {
|
||||
c.get("Status")
|
||||
.and_then(|v| v.as_str())
|
||||
@@ -625,6 +783,14 @@ pub fn spawn_health_monitor(state: Arc<StateManager>, data_dir: PathBuf) {
|
||||
}
|
||||
|
||||
if container.healthy {
|
||||
let before = data.notifications.len();
|
||||
data.notifications.retain(|n| {
|
||||
n.app_id.as_deref() != Some(&container.app_id)
|
||||
&& !n.title.contains(&container.app_id)
|
||||
});
|
||||
if data.notifications.len() != before {
|
||||
state_changed = true;
|
||||
}
|
||||
if tracker.attempt_count(&container.name) > 0 {
|
||||
info!(
|
||||
"Container {} is healthy again after restart",
|
||||
@@ -674,6 +840,21 @@ pub fn spawn_health_monitor(state: Arc<StateManager>, data_dir: PathBuf) {
|
||||
}
|
||||
}
|
||||
|
||||
let unhealthy_app_ids: HashSet<&str> = unhealthy
|
||||
.iter()
|
||||
.map(|container| container.app_id.as_str())
|
||||
.collect();
|
||||
let before = data.notifications.len();
|
||||
data.notifications.retain(|n| {
|
||||
!n.id.starts_with("health-")
|
||||
|| n.app_id
|
||||
.as_deref()
|
||||
.is_some_and(|app_id| unhealthy_app_ids.contains(app_id))
|
||||
});
|
||||
if data.notifications.len() != before {
|
||||
state_changed = true;
|
||||
}
|
||||
|
||||
// Sort by startup tier: databases first, then core, then dependent, then apps, then UIs
|
||||
unhealthy.sort_by_key(|c| container_tier(&c.name));
|
||||
|
||||
@@ -1173,4 +1354,49 @@ mod tests {
|
||||
Some("unhealthy")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_podman_healthcheck_systemd_units() {
|
||||
let id = "c1f44a6369c91d65f9e9f6134a5591aa02792cff2f1a4e0f689b5a6c03b6c77c";
|
||||
assert_eq!(
|
||||
parse_podman_healthcheck_unit(&format!("{}-15c66ddfefa8a763.timer", id)),
|
||||
Some(id)
|
||||
);
|
||||
assert_eq!(
|
||||
parse_podman_healthcheck_unit(&format!("{}-15c66ddfefa8a763.service", id)),
|
||||
Some(id)
|
||||
);
|
||||
assert_eq!(parse_podman_healthcheck_unit("grafana.service"), None);
|
||||
assert_eq!(
|
||||
parse_podman_healthcheck_unit(
|
||||
"nothexzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz-x.timer"
|
||||
),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stale_healthcheck_units_filters_only_removed_container_ids() {
|
||||
let live = "6467e25fd87d791a63fe9dbf6e2fabc7bf26533aa2c402b1089effeacf7ebbba";
|
||||
let stale = "c1f44a6369c91d65f9e9f6134a5591aa02792cff2f1a4e0f689b5a6c03b6c77c";
|
||||
let mut live_ids = HashSet::new();
|
||||
live_ids.insert(live.to_string());
|
||||
|
||||
let output = format!(
|
||||
" {live}-6fdc497fd3ba3b62.timer loaded active waiting\n\
|
||||
● {stale}-15c66ddfefa8a763.service loaded failed failed\n\
|
||||
grafana.service loaded active running\n\
|
||||
{stale}-1898d85de0bb707f.timer loaded active waiting\n"
|
||||
);
|
||||
|
||||
let mut units = stale_healthcheck_units(&output, &live_ids);
|
||||
units.sort();
|
||||
assert_eq!(
|
||||
units,
|
||||
vec![
|
||||
format!("{stale}-15c66ddfefa8a763.service"),
|
||||
format!("{stale}-1898d85de0bb707f.timer"),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1100,12 +1100,15 @@ if [ "$BACKEND_CAPTURED" = "0" ]; then
|
||||
FROM rust:1.93-trixie as builder
|
||||
WORKDIR /build
|
||||
COPY core ./core
|
||||
COPY scripts ./scripts
|
||||
COPY image-recipe/configs ./image-recipe/configs
|
||||
RUN cd core && cargo build --release --bin archipelago
|
||||
BACKENDFILE
|
||||
|
||||
if $CONTAINER_CMD build --platform $CONTAINER_PLATFORM -t archipelago-backend -f "$BACKEND_DOCKERFILE" "$SCRIPT_DIR/.." 2>&1 | tail -20; then
|
||||
BACKEND_IMAGE="localhost/archipelago-backend:iso"
|
||||
if $CONTAINER_CMD build --platform $CONTAINER_PLATFORM -t "$BACKEND_IMAGE" -f "$BACKEND_DOCKERFILE" "$SCRIPT_DIR/.."; then
|
||||
echo " Extracting backend binary..."
|
||||
BACKEND_CONTAINER=$($CONTAINER_CMD create --platform $CONTAINER_PLATFORM archipelago-backend)
|
||||
BACKEND_CONTAINER=$($CONTAINER_CMD create --platform $CONTAINER_PLATFORM "$BACKEND_IMAGE")
|
||||
$CONTAINER_CMD cp "$BACKEND_CONTAINER:/build/core/target/release/archipelago" "$ARCH_DIR/bin/" && \
|
||||
echo " ✅ Backend binary built ($(du -h "$ARCH_DIR/bin/archipelago" | cut -f1))"
|
||||
$CONTAINER_CMD rm "$BACKEND_CONTAINER"
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "neode-ui",
|
||||
"version": "1.7.55-alpha",
|
||||
"version": "1.7.56-alpha",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "neode-ui",
|
||||
"version": "1.7.55-alpha",
|
||||
"version": "1.7.56-alpha",
|
||||
"dependencies": {
|
||||
"@types/dompurify": "^3.0.5",
|
||||
"@vue-leaflet/vue-leaflet": "^0.10.1",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "neode-ui",
|
||||
"private": true,
|
||||
"version": "1.7.55-alpha",
|
||||
"version": "1.7.56-alpha",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "./start-dev.sh",
|
||||
|
||||
@@ -140,6 +140,7 @@ import { rpcClient } from '@/api/rpc-client'
|
||||
import AppHeroSection from './appDetails/AppHeroSection.vue'
|
||||
import AppContentSection from './appDetails/AppContentSection.vue'
|
||||
import AppSidebar from './appDetails/AppSidebar.vue'
|
||||
import { resolveAppUrl } from './appSession/appSessionConfig'
|
||||
import {
|
||||
WEB_ONLY_APP_URLS,
|
||||
PACKAGE_ALIASES,
|
||||
@@ -266,7 +267,9 @@ const backButtonText = computed(() => {
|
||||
const canLaunch = computed(() => {
|
||||
if (!pkg.value) return false
|
||||
if (isWebOnly.value) return true
|
||||
const hasUI = !!(pkg.value.manifest.interfaces?.main?.ui || pkg.value.installed?.['interface-addresses']?.main)
|
||||
const hasRuntimeAddress = !!pkg.value.installed?.['interface-addresses']?.main?.['lan-address']
|
||||
const hasKnownLaunchUrl = typeof window !== 'undefined' && !!resolveAppUrl(pkg.value.manifest.id)
|
||||
const hasUI = !!(pkg.value.manifest.interfaces?.main?.ui || hasRuntimeAddress || hasKnownLaunchUrl)
|
||||
return hasUI && pkg.value.state === 'running' && pkg.value.health !== 'starting' && pkg.value.health !== 'unhealthy'
|
||||
})
|
||||
|
||||
|
||||
@@ -160,7 +160,7 @@ export function getStatusLabel(state: PackageState, health?: string | null, exit
|
||||
if (state === PackageState.Running && health === 'unhealthy') return 'unhealthy'
|
||||
if (state === PackageState.Running && health === 'healthy') return 'healthy'
|
||||
if (state === PackageState.Exited) {
|
||||
if (exitCode === 137) return 'killed (OOM)'
|
||||
if (exitCode === 137) return 'killed (SIGKILL)'
|
||||
if (exitCode != null && exitCode !== 0) return 'crashed'
|
||||
return 'stopped'
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import type { Ref } from 'vue'
|
||||
import { computed } from 'vue'
|
||||
import { PackageState, type PackageDataEntry } from '@/types/api'
|
||||
import { resolveAppUrl } from '../appSession/appSessionConfig'
|
||||
|
||||
// Service container name patterns (backend/infra, not user-facing)
|
||||
export const SERVICE_NAMES = new Set([
|
||||
@@ -144,7 +145,9 @@ export function resolveAppIcon(id: string, pkg: PackageDataEntry, curatedIcon?:
|
||||
|
||||
export function canLaunch(pkg: PackageDataEntry): boolean {
|
||||
if (isWebOnlyApp(pkg.manifest.id)) return true
|
||||
const hasUI = pkg.manifest.interfaces?.main?.ui || pkg.installed?.['interface-addresses']?.main
|
||||
const hasRuntimeAddress = !!pkg.installed?.['interface-addresses']?.main?.['lan-address']
|
||||
const hasKnownLaunchUrl = typeof window !== 'undefined' && !!resolveAppUrl(pkg.manifest.id)
|
||||
const hasUI = pkg.manifest.interfaces?.main?.ui || hasRuntimeAddress || hasKnownLaunchUrl
|
||||
return !!hasUI && pkg.state === 'running' && pkg.health !== 'starting' && pkg.health !== 'unhealthy'
|
||||
}
|
||||
|
||||
@@ -181,7 +184,7 @@ export function getStatusLabel(state: PackageState, health?: string | null, exit
|
||||
if (state === PackageState.Updating) return 'updating...'
|
||||
if (state === PackageState.Running) return 'running'
|
||||
if (state === PackageState.Exited || state === PackageState.Stopped) {
|
||||
if (exitCode === 137) return 'killed (OOM)'
|
||||
if (exitCode === 137) return 'killed (SIGKILL)'
|
||||
if (exitCode != null && exitCode !== 0) return 'crashed'
|
||||
return 'stopped'
|
||||
}
|
||||
|
||||
+17
-16
@@ -1,27 +1,28 @@
|
||||
{
|
||||
"version": "1.7.55-alpha",
|
||||
"release_date": "2026-05-13",
|
||||
"version": "1.7.56-alpha",
|
||||
"release_date": "2026-05-14",
|
||||
"changelog": [
|
||||
"Container reconcile now force-recreates Podman records stuck in `Stopping`, preserving bind-mounted app data while recovering wedged containers automatically.",
|
||||
"`.198` is green after the container-layer hardening pass: focused and broad non-destructive lifecycle audits pass, raw Podman health/state sweep is clean, and direct app probes return healthy responses.",
|
||||
"Release-candidate artifacts are staged separately from live update publishing while Gitea artifact hosting is repaired."
|
||||
"Health notifications now clear when an app is no longer unhealthy, including stale alerts for removed containers such as Portainer.",
|
||||
"Quadlet environment values with spaces or shell metacharacters are quoted consistently, preventing env drift recreate loops for apps like nostr-rs-relay and Grafana.",
|
||||
"Boot/bootstrap reconcile avoids restarting running Bitcoin containers while repairing RPC config, preserving IBD progress on active nodes.",
|
||||
"Exit code 137 is labeled as SIGKILL instead of assuming OOM, avoiding false OOM alerts for orchestrator-managed recreates."
|
||||
],
|
||||
"components": [
|
||||
{
|
||||
"name": "archipelago",
|
||||
"current_version": "1.7.55-alpha",
|
||||
"new_version": "1.7.55-alpha",
|
||||
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.55-alpha/archipelago",
|
||||
"sha256": "f2caba778f63c7435431fb1b95cf6470bd43c4769ebe6adee2cbd2721707a663",
|
||||
"size_bytes": 42580880
|
||||
"current_version": "1.7.56-alpha",
|
||||
"new_version": "1.7.56-alpha",
|
||||
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.56-alpha/archipelago",
|
||||
"sha256": "11217a0e40c1704ee9f5bbcdeb59e38c9efc5e00ea7c673c5c3f0f69de720109",
|
||||
"size_bytes": 42647960
|
||||
},
|
||||
{
|
||||
"name": "archipelago-frontend-1.7.55-alpha.tar.gz",
|
||||
"current_version": "1.7.55-alpha",
|
||||
"new_version": "1.7.55-alpha",
|
||||
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.55-alpha/archipelago-frontend-1.7.55-alpha.tar.gz",
|
||||
"sha256": "fe37425aad25724db49ec2be8d602342cfdc5fb99f08b4a3f04709751a3ed560",
|
||||
"size_bytes": 166464949
|
||||
"name": "archipelago-frontend-1.7.56-alpha.tar.gz",
|
||||
"current_version": "1.7.56-alpha",
|
||||
"new_version": "1.7.56-alpha",
|
||||
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.56-alpha/archipelago-frontend-1.7.56-alpha.tar.gz",
|
||||
"sha256": "9cee503e260891267fed33e4c97f067a4a592b10695ecea4cbf730d517ff6c8b",
|
||||
"size_bytes": 166493666
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+17
-16
@@ -1,27 +1,28 @@
|
||||
{
|
||||
"version": "1.7.55-alpha",
|
||||
"release_date": "2026-05-13",
|
||||
"version": "1.7.56-alpha",
|
||||
"release_date": "2026-05-14",
|
||||
"changelog": [
|
||||
"Container reconcile now force-recreates Podman records stuck in `Stopping`, preserving bind-mounted app data while recovering wedged containers automatically.",
|
||||
"`.198` is green after the container-layer hardening pass: focused and broad non-destructive lifecycle audits pass, raw Podman health/state sweep is clean, and direct app probes return healthy responses.",
|
||||
"Release-candidate artifacts are staged separately from live update publishing while Gitea artifact hosting is repaired."
|
||||
"Health notifications now clear when an app is no longer unhealthy, including stale alerts for removed containers such as Portainer.",
|
||||
"Quadlet environment values with spaces or shell metacharacters are quoted consistently, preventing env drift recreate loops for apps like nostr-rs-relay and Grafana.",
|
||||
"Boot/bootstrap reconcile avoids restarting running Bitcoin containers while repairing RPC config, preserving IBD progress on active nodes.",
|
||||
"Exit code 137 is labeled as SIGKILL instead of assuming OOM, avoiding false OOM alerts for orchestrator-managed recreates."
|
||||
],
|
||||
"components": [
|
||||
{
|
||||
"name": "archipelago",
|
||||
"current_version": "1.7.55-alpha",
|
||||
"new_version": "1.7.55-alpha",
|
||||
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.55-alpha/archipelago",
|
||||
"sha256": "f2caba778f63c7435431fb1b95cf6470bd43c4769ebe6adee2cbd2721707a663",
|
||||
"size_bytes": 42580880
|
||||
"current_version": "1.7.56-alpha",
|
||||
"new_version": "1.7.56-alpha",
|
||||
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.56-alpha/archipelago",
|
||||
"sha256": "11217a0e40c1704ee9f5bbcdeb59e38c9efc5e00ea7c673c5c3f0f69de720109",
|
||||
"size_bytes": 42647960
|
||||
},
|
||||
{
|
||||
"name": "archipelago-frontend-1.7.55-alpha.tar.gz",
|
||||
"current_version": "1.7.55-alpha",
|
||||
"new_version": "1.7.55-alpha",
|
||||
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.55-alpha/archipelago-frontend-1.7.55-alpha.tar.gz",
|
||||
"sha256": "fe37425aad25724db49ec2be8d602342cfdc5fb99f08b4a3f04709751a3ed560",
|
||||
"size_bytes": 166464949
|
||||
"name": "archipelago-frontend-1.7.56-alpha.tar.gz",
|
||||
"current_version": "1.7.56-alpha",
|
||||
"new_version": "1.7.56-alpha",
|
||||
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.56-alpha/archipelago-frontend-1.7.56-alpha.tar.gz",
|
||||
"sha256": "9cee503e260891267fed33e4c97f067a4a592b10695ecea4cbf730d517ff6c8b",
|
||||
"size_bytes": 166493666
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -173,9 +173,9 @@ load_spec_bitcoin-knots() {
|
||||
SPEC_DATA_UID="100101:100101"
|
||||
# Dynamic: prune on small disk
|
||||
if [ "${DISK_GB:-0}" -lt 1000 ]; then
|
||||
SPEC_CUSTOM_ARGS="-server=1 -prune=550 -rpcallowip=0.0.0.0/0 -rpcbind=0.0.0.0:8332 -listen=1 -bind=0.0.0.0:8333 -dbcache=512"
|
||||
SPEC_CUSTOM_ARGS="-server=1 -prune=550 -rpcallowip=0.0.0.0/0 -rpcbind=0.0.0.0:8332 -listen=1 -bind=0.0.0.0:8333 -dbcache=2048 -par=0 -maxconnections=125"
|
||||
else
|
||||
SPEC_CUSTOM_ARGS="-server=1 -txindex=1 -rpcallowip=0.0.0.0/0 -rpcbind=0.0.0.0:8332 -listen=1 -bind=0.0.0.0:8333 -dbcache=4096"
|
||||
SPEC_CUSTOM_ARGS="-server=1 -txindex=1 -rpcallowip=0.0.0.0/0 -rpcbind=0.0.0.0:8332 -listen=1 -bind=0.0.0.0:8333 -dbcache=4096 -par=0 -maxconnections=125"
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
@@ -557,11 +557,11 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -qE 'bitcoin-knots|arch
|
||||
[ -z "$DISK_GB" ] && DISK_GB=$(df --output=size -BG / 2>/dev/null | tail -1 | tr -dc '0-9')
|
||||
if [ "${DISK_GB:-0}" -lt 1000 ]; then
|
||||
BTC_EXTRA_ARGS="-prune=550"
|
||||
BTC_DBCACHE=512
|
||||
BTC_DBCACHE=2048
|
||||
log " Small disk (${DISK_GB}GB) — enabling pruning"
|
||||
else
|
||||
BTC_EXTRA_ARGS="-txindex=1"
|
||||
BTC_DBCACHE=2048
|
||||
BTC_DBCACHE=4096
|
||||
log " Large disk (${DISK_GB}GB) — enabling txindex"
|
||||
fi
|
||||
if $DOCKER run -d --name bitcoin-knots --restart unless-stopped \
|
||||
@@ -574,7 +574,7 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -qE 'bitcoin-knots|arch
|
||||
-v /var/lib/archipelago/bitcoin:/home/bitcoin/.bitcoin \
|
||||
"${BITCOIN_KNOTS_IMAGE}" \
|
||||
$BTC_EXTRA_ARGS \
|
||||
-printtoconsole=1 -dbcache=$BTC_DBCACHE 2>>"$LOG"; then
|
||||
-printtoconsole=1 -dbcache=$BTC_DBCACHE -par=0 -maxconnections=125 2>>"$LOG"; then
|
||||
log "Bitcoin Knots started"
|
||||
else
|
||||
log "Bitcoin Knots failed (may already exist)"
|
||||
|
||||
Reference in New Issue
Block a user