feat: container orchestration, branding overhaul, onboarding logging
Container orchestration: - Health monitor with crash recovery and auto-restart - Doctor service (periodic health checks via systemd timer) - Reconcile service (desired-state convergence) - Stack-aware install/uninstall with dependency tracking Branding: - Custom GRUB background (designer artwork, 1024x768) - ISOLINUX boot menu: centered, orange accents, clean labels - Terminal banners: adaptive width, basic ANSI colors, fits 80-col - Removed auto-generated splash scripts (designer provides assets) - GRUB theme: lowercase branding Frontend: - 401 handler clears localStorage immediately (prevents cascade) Backend: - Onboarding/auth logging ([onboarding] tag in journalctl) - Cookie Secure flag logging for debugging HTTP/HTTPS issues ISO fixes: - Install log saved before unmount (was silently failing) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6311aa563e
commit
99400a7165
15
core/Cargo.lock
generated
15
core/Cargo.lock
generated
@ -84,7 +84,6 @@ version = "1.2.0-alpha"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"archipelago-container",
|
"archipelago-container",
|
||||||
"archipelago-parmanode",
|
|
||||||
"archipelago-performance",
|
"archipelago-performance",
|
||||||
"archipelago-security",
|
"archipelago-security",
|
||||||
"argon2",
|
"argon2",
|
||||||
@ -160,20 +159,6 @@ dependencies = [
|
|||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "archipelago-parmanode"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"anyhow",
|
|
||||||
"archipelago-container",
|
|
||||||
"log",
|
|
||||||
"serde",
|
|
||||||
"serde_yaml",
|
|
||||||
"thiserror 1.0.69",
|
|
||||||
"tokio",
|
|
||||||
"tracing",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "archipelago-performance"
|
name = "archipelago-performance"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|||||||
@ -16,8 +16,10 @@ impl RpcHandler {
|
|||||||
if !is_setup {
|
if !is_setup {
|
||||||
// Dev mode: allow default password so UI can log in without running setup
|
// Dev mode: allow default password so UI can log in without running setup
|
||||||
if self.config.dev_mode && password == DEV_DEFAULT_PASSWORD {
|
if self.config.dev_mode && password == DEV_DEFAULT_PASSWORD {
|
||||||
|
tracing::info!("[onboarding] login via dev default password");
|
||||||
return Ok(serde_json::Value::Null);
|
return Ok(serde_json::Value::Null);
|
||||||
}
|
}
|
||||||
|
tracing::warn!("[onboarding] login attempt before setup complete");
|
||||||
return Err(anyhow::anyhow!(
|
return Err(anyhow::anyhow!(
|
||||||
"User not set up. Please complete setup first."
|
"User not set up. Please complete setup first."
|
||||||
));
|
));
|
||||||
@ -25,13 +27,16 @@ impl RpcHandler {
|
|||||||
|
|
||||||
let valid = self.auth_manager.verify_password(password).await?;
|
let valid = self.auth_manager.verify_password(password).await?;
|
||||||
if !valid {
|
if !valid {
|
||||||
|
tracing::warn!("[onboarding] login failed — wrong password");
|
||||||
return Err(anyhow::anyhow!("Password Incorrect"));
|
return Err(anyhow::anyhow!("Password Incorrect"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tracing::info!("[onboarding] login successful");
|
||||||
Ok(serde_json::Value::Null)
|
Ok(serde_json::Value::Null)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) async fn handle_auth_logout(&self) -> Result<serde_json::Value> {
|
pub(super) async fn handle_auth_logout(&self) -> Result<serde_json::Value> {
|
||||||
|
tracing::info!("[onboarding] logout");
|
||||||
Ok(serde_json::Value::Null)
|
Ok(serde_json::Value::Null)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -78,6 +83,7 @@ impl RpcHandler {
|
|||||||
// Prevent re-setup if already set up
|
// Prevent re-setup if already set up
|
||||||
let is_setup = self.auth_manager.is_setup().await?;
|
let is_setup = self.auth_manager.is_setup().await?;
|
||||||
if is_setup {
|
if is_setup {
|
||||||
|
tracing::warn!("[onboarding] setup rejected — already set up");
|
||||||
return Err(anyhow::anyhow!("Already set up. Use auth.changePassword to change."));
|
return Err(anyhow::anyhow!("Already set up. Use auth.changePassword to change."));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -88,20 +94,24 @@ impl RpcHandler {
|
|||||||
.ok_or_else(|| anyhow::anyhow!("Missing password"))?;
|
.ok_or_else(|| anyhow::anyhow!("Missing password"))?;
|
||||||
|
|
||||||
if password.len() < 8 {
|
if password.len() < 8 {
|
||||||
|
tracing::warn!("[onboarding] setup rejected — password too short");
|
||||||
return Err(anyhow::anyhow!("Password must be at least 8 characters"));
|
return Err(anyhow::anyhow!("Password must be at least 8 characters"));
|
||||||
}
|
}
|
||||||
|
|
||||||
self.auth_manager.setup_user(password).await?;
|
self.auth_manager.setup_user(password).await?;
|
||||||
|
tracing::info!("[onboarding] user setup complete");
|
||||||
Ok(serde_json::json!(true))
|
Ok(serde_json::json!(true))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) async fn handle_auth_onboarding_complete(&self) -> Result<serde_json::Value> {
|
pub(super) async fn handle_auth_onboarding_complete(&self) -> Result<serde_json::Value> {
|
||||||
self.auth_manager.complete_onboarding().await?;
|
self.auth_manager.complete_onboarding().await?;
|
||||||
|
tracing::info!("[onboarding] onboarding marked complete");
|
||||||
Ok(serde_json::json!(true))
|
Ok(serde_json::json!(true))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) async fn handle_auth_is_onboarding_complete(&self) -> Result<serde_json::Value> {
|
pub(super) async fn handle_auth_is_onboarding_complete(&self) -> Result<serde_json::Value> {
|
||||||
let complete = self.auth_manager.is_onboarding_complete().await?;
|
let complete = self.auth_manager.is_onboarding_complete().await?;
|
||||||
|
tracing::debug!("[onboarding] isOnboardingComplete={}", complete);
|
||||||
Ok(serde_json::json!(complete))
|
Ok(serde_json::json!(complete))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -117,10 +127,12 @@ impl RpcHandler {
|
|||||||
|
|
||||||
let valid = self.auth_manager.verify_password(password).await?;
|
let valid = self.auth_manager.verify_password(password).await?;
|
||||||
if !valid {
|
if !valid {
|
||||||
|
tracing::warn!("[onboarding] reset rejected — wrong password");
|
||||||
return Err(anyhow::anyhow!("Password Incorrect"));
|
return Err(anyhow::anyhow!("Password Incorrect"));
|
||||||
}
|
}
|
||||||
|
|
||||||
self.auth_manager.reset_onboarding().await?;
|
self.auth_manager.reset_onboarding().await?;
|
||||||
|
tracing::info!("[onboarding] onboarding reset");
|
||||||
Ok(serde_json::json!(true))
|
Ok(serde_json::json!(true))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -153,9 +153,11 @@ impl RpcHandler {
|
|||||||
}
|
}
|
||||||
if let Some(proto) = headers.get("x-forwarded-proto") {
|
if let Some(proto) = headers.get("x-forwarded-proto") {
|
||||||
if proto.as_bytes() == b"https" {
|
if proto.as_bytes() == b"https" {
|
||||||
|
tracing::debug!("[onboarding] cookie: Secure (X-Forwarded-Proto: https)");
|
||||||
return "; Secure";
|
return "; Secure";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
tracing::debug!("[onboarding] cookie: no Secure flag (HTTP or no X-Forwarded-Proto)");
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -228,6 +228,11 @@ impl RpcHandler {
|
|||||||
|
|
||||||
if !run_output.status.success() {
|
if !run_output.status.success() {
|
||||||
let stderr = String::from_utf8_lossy(&run_output.stderr);
|
let stderr = String::from_utf8_lossy(&run_output.stderr);
|
||||||
|
// Rollback: remove partially created container
|
||||||
|
let _ = tokio::process::Command::new("podman")
|
||||||
|
.args(["rm", "-f", container_name])
|
||||||
|
.output()
|
||||||
|
.await;
|
||||||
return Err(anyhow::anyhow!("Failed to start container: {}", stderr));
|
return Err(anyhow::anyhow!("Failed to start container: {}", stderr));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -235,6 +240,43 @@ impl RpcHandler {
|
|||||||
.trim()
|
.trim()
|
||||||
.to_string();
|
.to_string();
|
||||||
|
|
||||||
|
// Post-start health verification: wait up to 30s for container to be running
|
||||||
|
for i in 0..6u32 {
|
||||||
|
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
|
||||||
|
let status = tokio::process::Command::new("podman")
|
||||||
|
.args(["inspect", container_name, "--format", "{{.State.Status}}"])
|
||||||
|
.output()
|
||||||
|
.await;
|
||||||
|
if let Ok(o) = status {
|
||||||
|
let state = String::from_utf8_lossy(&o.stdout).trim().to_string();
|
||||||
|
if state == "running" {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if state == "exited" {
|
||||||
|
// Container crashed immediately — get logs for diagnosis
|
||||||
|
let logs = tokio::process::Command::new("podman")
|
||||||
|
.args(["logs", "--tail", "20", container_name])
|
||||||
|
.output()
|
||||||
|
.await;
|
||||||
|
let log_output = logs
|
||||||
|
.map(|o| String::from_utf8_lossy(&o.stderr).to_string())
|
||||||
|
.unwrap_or_default();
|
||||||
|
let _ = tokio::process::Command::new("podman")
|
||||||
|
.args(["rm", "-f", container_name])
|
||||||
|
.output()
|
||||||
|
.await;
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"Container {} exited immediately after start. Logs: {}",
|
||||||
|
container_name,
|
||||||
|
log_output.chars().take(500).collect::<String>()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if i == 5 {
|
||||||
|
debug!("Container {} health check timeout (30s) — continuing anyway", container_name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Post-install hooks
|
// Post-install hooks
|
||||||
self.run_post_install_hooks(package_id).await;
|
self.run_post_install_hooks(package_id).await;
|
||||||
|
|
||||||
@ -301,11 +343,43 @@ impl RpcHandler {
|
|||||||
Ok(has_local_fallback)
|
Ok(has_local_fallback)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stream `podman pull` while updating install progress state.
|
/// Pull image with retry and exponential backoff (3 attempts: 5s, 15s, 45s).
|
||||||
async fn pull_image_with_progress(
|
async fn pull_image_with_progress(
|
||||||
&self,
|
&self,
|
||||||
package_id: &str,
|
package_id: &str,
|
||||||
docker_image: &str,
|
docker_image: &str,
|
||||||
|
) -> Result<()> {
|
||||||
|
const MAX_ATTEMPTS: u32 = 3;
|
||||||
|
const BACKOFF_SECS: [u64; 3] = [5, 15, 45];
|
||||||
|
|
||||||
|
for attempt in 1..=MAX_ATTEMPTS {
|
||||||
|
match self.do_pull_image(package_id, docker_image).await {
|
||||||
|
Ok(()) => return Ok(()),
|
||||||
|
Err(e) if attempt < MAX_ATTEMPTS => {
|
||||||
|
let delay = BACKOFF_SECS[(attempt - 1) as usize];
|
||||||
|
tracing::warn!(
|
||||||
|
"Image pull failed for {} (attempt {}/{}): {}. Retrying in {}s...",
|
||||||
|
docker_image, attempt, MAX_ATTEMPTS, e, delay
|
||||||
|
);
|
||||||
|
tokio::time::sleep(std::time::Duration::from_secs(delay)).await;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
self.clear_install_progress(package_id).await;
|
||||||
|
return Err(e.context(format!(
|
||||||
|
"Failed to pull {} after {} attempts",
|
||||||
|
docker_image, MAX_ATTEMPTS
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Single image pull attempt with progress streaming.
|
||||||
|
async fn do_pull_image(
|
||||||
|
&self,
|
||||||
|
package_id: &str,
|
||||||
|
docker_image: &str,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
debug!("Pulling image: {}", docker_image);
|
debug!("Pulling image: {}", docker_image);
|
||||||
self.set_install_progress(package_id, 0, 0).await;
|
self.set_install_progress(package_id, 0, 0).await;
|
||||||
@ -336,8 +410,20 @@ impl RpcHandler {
|
|||||||
.await
|
.await
|
||||||
.context("Failed to wait for image pull")?;
|
.context("Failed to wait for image pull")?;
|
||||||
if !status.success() {
|
if !status.success() {
|
||||||
self.clear_install_progress(package_id).await;
|
return Err(anyhow::anyhow!("podman pull exited with non-zero status"));
|
||||||
return Err(anyhow::anyhow!("Failed to pull image"));
|
}
|
||||||
|
|
||||||
|
// Verify image exists locally after pull
|
||||||
|
let verify = tokio::process::Command::new("podman")
|
||||||
|
.args(["images", "-q", docker_image])
|
||||||
|
.output()
|
||||||
|
.await
|
||||||
|
.context("Failed to verify pulled image")?;
|
||||||
|
if String::from_utf8_lossy(&verify.stdout).trim().is_empty() {
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"Image {} not found locally after pull",
|
||||||
|
docker_image
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
self.set_install_progress(package_id, 100, 100).await;
|
self.set_install_progress(package_id, 100, 100).await;
|
||||||
|
|||||||
@ -4,6 +4,22 @@ use super::validation::validate_app_id;
|
|||||||
use crate::api::rpc::RpcHandler;
|
use crate::api::rpc::RpcHandler;
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
|
|
||||||
|
/// Per-container graceful shutdown timeout in seconds.
|
||||||
|
/// Bitcoin Core needs 600s to flush UTXO set, LND 330s for channel state,
|
||||||
|
/// indexers 300s for index flush, databases 120s for WAL/transaction commit.
|
||||||
|
fn stop_timeout_secs(container_name: &str) -> &'static str {
|
||||||
|
let id = container_name.strip_prefix("archy-").unwrap_or(container_name);
|
||||||
|
match id {
|
||||||
|
"bitcoin-knots" | "bitcoin-core" | "bitcoin" => "600",
|
||||||
|
"lnd" => "330",
|
||||||
|
"electrumx" | "electrs" | "mempool-electrs" => "300",
|
||||||
|
"btcpay-db" | "mempool-db" | "penpot-postgres" | "immich_postgres"
|
||||||
|
| "nextcloud-db" | "endurain-db" => "120",
|
||||||
|
"btcpay-server" | "nbxplorer" | "fedimint" | "fedimint-gateway" => "60",
|
||||||
|
_ => "30",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl RpcHandler {
|
impl RpcHandler {
|
||||||
/// Start a package: start all containers in dependency order.
|
/// Start a package: start all containers in dependency order.
|
||||||
pub(in crate::api::rpc) async fn handle_package_start(
|
pub(in crate::api::rpc) async fn handle_package_start(
|
||||||
@ -56,7 +72,7 @@ impl RpcHandler {
|
|||||||
crate::crash_recovery::mark_user_stopped(&self.config.data_dir, &container_name)
|
crate::crash_recovery::mark_user_stopped(&self.config.data_dir, &container_name)
|
||||||
.await;
|
.await;
|
||||||
let _ = tokio::process::Command::new("podman")
|
let _ = tokio::process::Command::new("podman")
|
||||||
.args(["stop", &container_name])
|
.args(["stop", "-t", stop_timeout_secs(&container_name), &container_name])
|
||||||
.output()
|
.output()
|
||||||
.await;
|
.await;
|
||||||
return Ok(serde_json::Value::Null);
|
return Ok(serde_json::Value::Null);
|
||||||
@ -67,7 +83,7 @@ impl RpcHandler {
|
|||||||
}
|
}
|
||||||
for name in containers {
|
for name in containers {
|
||||||
let _ = tokio::process::Command::new("podman")
|
let _ = tokio::process::Command::new("podman")
|
||||||
.args(["stop", &name])
|
.args(["stop", "-t", stop_timeout_secs(&name), &name])
|
||||||
.output()
|
.output()
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
@ -135,7 +151,7 @@ impl RpcHandler {
|
|||||||
for name in &containers_to_remove {
|
for name in &containers_to_remove {
|
||||||
tracing::info!("Uninstall {}: stopping container {}", package_id, name);
|
tracing::info!("Uninstall {}: stopping container {}", package_id, name);
|
||||||
let stop_out = tokio::process::Command::new("podman")
|
let stop_out = tokio::process::Command::new("podman")
|
||||||
.args(["stop", "-t", "10", name])
|
.args(["stop", "-t", stop_timeout_secs(name), name])
|
||||||
.output()
|
.output()
|
||||||
.await;
|
.await;
|
||||||
match stop_out {
|
match stop_out {
|
||||||
@ -344,7 +360,7 @@ impl RpcHandler {
|
|||||||
validate_app_id(app_id)?;
|
validate_app_id(app_id)?;
|
||||||
|
|
||||||
let output = tokio::process::Command::new("podman")
|
let output = tokio::process::Command::new("podman")
|
||||||
.args(["stop", app_id])
|
.args(["stop", "-t", stop_timeout_secs(app_id), app_id])
|
||||||
.output()
|
.output()
|
||||||
.await
|
.await
|
||||||
.context("Failed to stop container")?;
|
.context("Failed to stop container")?;
|
||||||
|
|||||||
@ -7,6 +7,41 @@ use crate::api::rpc::RpcHandler;
|
|||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
|
|
||||||
|
/// Pull an image with retry and exponential backoff (3 attempts).
|
||||||
|
async fn pull_image_with_retry(image: &str) -> Result<()> {
|
||||||
|
const MAX_ATTEMPTS: u32 = 3;
|
||||||
|
const BACKOFF_SECS: [u64; 3] = [5, 15, 45];
|
||||||
|
|
||||||
|
for attempt in 1..=MAX_ATTEMPTS {
|
||||||
|
let output = tokio::process::Command::new("podman")
|
||||||
|
.args(["pull", image])
|
||||||
|
.output()
|
||||||
|
.await
|
||||||
|
.context("Failed to execute podman pull")?;
|
||||||
|
|
||||||
|
if output.status.success() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
if attempt < MAX_ATTEMPTS {
|
||||||
|
let delay = BACKOFF_SECS[(attempt - 1) as usize];
|
||||||
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
|
tracing::warn!(
|
||||||
|
"Image pull failed for {} (attempt {}/{}): {}. Retrying in {}s...",
|
||||||
|
image, attempt, MAX_ATTEMPTS, stderr.trim(), delay
|
||||||
|
);
|
||||||
|
tokio::time::sleep(std::time::Duration::from_secs(delay)).await;
|
||||||
|
} else {
|
||||||
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"Failed to pull {} after {} attempts: {}",
|
||||||
|
image, MAX_ATTEMPTS, stderr.trim()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
|
||||||
impl RpcHandler {
|
impl RpcHandler {
|
||||||
/// Install Immich stack (postgres + redis + server).
|
/// Install Immich stack (postgres + redis + server).
|
||||||
pub(super) async fn install_immich_stack(&self) -> Result<serde_json::Value> {
|
pub(super) async fn install_immich_stack(&self) -> Result<serde_json::Value> {
|
||||||
@ -38,10 +73,7 @@ impl RpcHandler {
|
|||||||
"80.71.235.15:3000/archipelago/immich-server:release",
|
"80.71.235.15:3000/archipelago/immich-server:release",
|
||||||
];
|
];
|
||||||
for img in &images {
|
for img in &images {
|
||||||
let _ = tokio::process::Command::new("podman")
|
pull_image_with_retry(img).await?;
|
||||||
.args(["pull", img])
|
|
||||||
.output()
|
|
||||||
.await;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let _ = tokio::process::Command::new("sudo")
|
let _ = tokio::process::Command::new("sudo")
|
||||||
@ -168,10 +200,7 @@ impl RpcHandler {
|
|||||||
"80.71.235.15:3000/archipelago/penpot-frontend:2.4",
|
"80.71.235.15:3000/archipelago/penpot-frontend:2.4",
|
||||||
];
|
];
|
||||||
for img in &images {
|
for img in &images {
|
||||||
let _ = tokio::process::Command::new("podman")
|
pull_image_with_retry(img).await?;
|
||||||
.args(["pull", img])
|
|
||||||
.output()
|
|
||||||
.await;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let _ = tokio::process::Command::new("sudo")
|
let _ = tokio::process::Command::new("sudo")
|
||||||
|
|||||||
@ -384,6 +384,36 @@ fn container_boot_tier(name: &str) -> u8 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Run the reconciliation script after boot to fix any config drift.
|
||||||
|
/// Ensures all containers match their canonical specs from container-specs.sh.
|
||||||
|
pub async fn run_boot_reconciliation() {
|
||||||
|
let script = "/home/archipelago/archy/scripts/reconcile-containers.sh";
|
||||||
|
if !std::path::Path::new(script).exists() {
|
||||||
|
info!("Reconciliation script not found (dev mode?) — skipping boot reconciliation");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
info!("Running boot reconciliation...");
|
||||||
|
let result = tokio::time::timeout(
|
||||||
|
std::time::Duration::from_secs(300),
|
||||||
|
tokio::process::Command::new(script).output(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
match result {
|
||||||
|
Ok(Ok(output)) if output.status.success() => {
|
||||||
|
info!("Boot reconciliation complete");
|
||||||
|
}
|
||||||
|
Ok(Ok(output)) => {
|
||||||
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
|
warn!(
|
||||||
|
"Boot reconciliation had failures: {}",
|
||||||
|
stderr.chars().take(500).collect::<String>()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(Err(e)) => warn!("Boot reconciliation failed to run: {}", e),
|
||||||
|
Err(_) => warn!("Boot reconciliation timed out (300s)"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Spawn a background task that periodically saves the container snapshot.
|
/// Spawn a background task that periodically saves the container snapshot.
|
||||||
pub fn spawn_snapshot_task(data_dir: PathBuf) {
|
pub fn spawn_snapshot_task(data_dir: PathBuf) {
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
|
|||||||
@ -6,8 +6,9 @@
|
|||||||
use crate::data_model::{Notification, NotificationLevel};
|
use crate::data_model::{Notification, NotificationLevel};
|
||||||
use crate::state::StateManager;
|
use crate::state::StateManager;
|
||||||
use crate::webhooks::{self, WebhookEvent};
|
use crate::webhooks::{self, WebhookEvent};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::path::PathBuf;
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
use tracing::{debug, info, warn};
|
use tracing::{debug, info, warn};
|
||||||
@ -177,6 +178,69 @@ impl MemoryTracker {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Persistent restart tracking ────────────────────────────────────────
|
||||||
|
// Survives process restarts so a container can't loop infinitely by
|
||||||
|
// crashing 3 times → triggering process restart → resetting counter → repeat.
|
||||||
|
|
||||||
|
const RESTART_HISTORY_FILE: &str = "restart-tracker.json";
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Default)]
|
||||||
|
struct RestartHistory {
|
||||||
|
containers: HashMap<String, ContainerRestartRecord>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
|
struct ContainerRestartRecord {
|
||||||
|
attempts: u32,
|
||||||
|
last_failure_epoch: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RestartHistory {
|
||||||
|
async fn load(data_dir: &Path) -> Self {
|
||||||
|
let path = data_dir.join(RESTART_HISTORY_FILE);
|
||||||
|
match tokio::fs::read_to_string(&path).await {
|
||||||
|
Ok(content) => serde_json::from_str(&content).unwrap_or_default(),
|
||||||
|
Err(_) => Self::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn save(&self, data_dir: &Path) {
|
||||||
|
let path = data_dir.join(RESTART_HISTORY_FILE);
|
||||||
|
if let Ok(json) = serde_json::to_string(self) {
|
||||||
|
let _ = tokio::fs::write(&path, json).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Seed the in-memory RestartTracker from persisted history.
|
||||||
|
fn seed_tracker(&self, tracker: &mut RestartTracker) {
|
||||||
|
let now_epoch = chrono::Utc::now().timestamp();
|
||||||
|
for (name, record) in &self.containers {
|
||||||
|
// Only seed if last failure was within the stability window
|
||||||
|
let secs_since_failure = now_epoch - record.last_failure_epoch;
|
||||||
|
if secs_since_failure < STABILITY_RESET_SECS as i64 && record.attempts > 0 {
|
||||||
|
tracker.attempts.insert(name.clone(), record.attempts);
|
||||||
|
info!(
|
||||||
|
"Restored restart counter for {}: {} attempts ({}s ago)",
|
||||||
|
name, record.attempts, secs_since_failure
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn record_attempt(&mut self, name: &str) {
|
||||||
|
let entry = self.containers.entry(name.to_string()).or_insert(ContainerRestartRecord {
|
||||||
|
attempts: 0,
|
||||||
|
last_failure_epoch: 0,
|
||||||
|
});
|
||||||
|
entry.attempts += 1;
|
||||||
|
entry.last_failure_epoch = chrono::Utc::now().timestamp();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clear(&mut self, name: &str) {
|
||||||
|
self.containers.remove(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Query container memory stats from podman.
|
/// Query container memory stats from podman.
|
||||||
async fn check_container_memory() -> HashMap<String, u64> {
|
async fn check_container_memory() -> HashMap<String, u64> {
|
||||||
let output = match tokio::time::timeout(
|
let output = match tokio::time::timeout(
|
||||||
@ -373,6 +437,11 @@ pub fn spawn_health_monitor(state: Arc<StateManager>, data_dir: PathBuf) {
|
|||||||
let mut mem_check_counter: u32 = 0;
|
let mut mem_check_counter: u32 = 0;
|
||||||
let mut interval = tokio::time::interval(std::time::Duration::from_secs(CHECK_INTERVAL_SECS));
|
let mut interval = tokio::time::interval(std::time::Duration::from_secs(CHECK_INTERVAL_SECS));
|
||||||
|
|
||||||
|
// Load persistent restart history and seed the in-memory tracker
|
||||||
|
let mut restart_history = RestartHistory::load(&data_dir).await;
|
||||||
|
restart_history.seed_tracker(&mut tracker);
|
||||||
|
let mut history_dirty = false;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
interval.tick().await;
|
interval.tick().await;
|
||||||
mem_check_counter += 1;
|
mem_check_counter += 1;
|
||||||
@ -406,6 +475,8 @@ pub fn spawn_health_monitor(state: Arc<StateManager>, data_dir: PathBuf) {
|
|||||||
if tracker.attempt_count(&container.name) > 0 {
|
if tracker.attempt_count(&container.name) > 0 {
|
||||||
info!("Container {} is healthy again after restart", container.name);
|
info!("Container {} is healthy again after restart", container.name);
|
||||||
tracker.clear(&container.name);
|
tracker.clear(&container.name);
|
||||||
|
restart_history.clear(&container.name);
|
||||||
|
history_dirty = true;
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -430,6 +501,8 @@ pub fn spawn_health_monitor(state: Arc<StateManager>, data_dir: PathBuf) {
|
|||||||
if tracker.should_reset_failed(&container.name) {
|
if tracker.should_reset_failed(&container.name) {
|
||||||
info!("Resetting restart counter for {} after {}s stability window", container.name, STABILITY_RESET_SECS);
|
info!("Resetting restart counter for {} after {}s stability window", container.name, STABILITY_RESET_SECS);
|
||||||
tracker.clear(&container.name);
|
tracker.clear(&container.name);
|
||||||
|
restart_history.clear(&container.name);
|
||||||
|
history_dirty = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if tracker.attempt_count(&container.name) >= MAX_RESTART_ATTEMPTS {
|
if tracker.attempt_count(&container.name) >= MAX_RESTART_ATTEMPTS {
|
||||||
@ -453,6 +526,8 @@ pub fn spawn_health_monitor(state: Arc<StateManager>, data_dir: PathBuf) {
|
|||||||
prev_tier = Some(tier);
|
prev_tier = Some(tier);
|
||||||
|
|
||||||
if tracker.record_attempt(&container.name) {
|
if tracker.record_attempt(&container.name) {
|
||||||
|
restart_history.record_attempt(&container.name);
|
||||||
|
history_dirty = true;
|
||||||
let attempt = tracker.attempt_count(&container.name);
|
let attempt = tracker.attempt_count(&container.name);
|
||||||
info!("Restarting {} (tier {:?}, attempt {}/{}, backoff {}s)",
|
info!("Restarting {} (tier {:?}, attempt {}/{}, backoff {}s)",
|
||||||
container.name, tier, attempt, MAX_RESTART_ATTEMPTS,
|
container.name, tier, attempt, MAX_RESTART_ATTEMPTS,
|
||||||
@ -509,6 +584,12 @@ pub fn spawn_health_monitor(state: Arc<StateManager>, data_dir: PathBuf) {
|
|||||||
state.update_data(data).await;
|
state.update_data(data).await;
|
||||||
debug!("Health monitor: state updated with notifications");
|
debug!("Health monitor: state updated with notifications");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Persist restart history to disk (debounced: once per check cycle)
|
||||||
|
if history_dirty {
|
||||||
|
restart_history.save(&data_dir).await;
|
||||||
|
history_dirty = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -106,6 +106,9 @@ async fn main() -> Result<()> {
|
|||||||
|
|
||||||
// Signal to health monitor that boot recovery is done
|
// Signal to health monitor that boot recovery is done
|
||||||
crash_recovery::mark_recovery_complete();
|
crash_recovery::mark_recovery_complete();
|
||||||
|
|
||||||
|
// Reconcile containers against canonical specs (fixes config drift)
|
||||||
|
crash_recovery::run_boot_reconciliation().await;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -137,7 +137,12 @@ impl HealthMonitor {
|
|||||||
if consecutive_failures >= max_failures {
|
if consecutive_failures >= max_failures {
|
||||||
error!("Container {} is unhealthy after {} failures",
|
error!("Container {} is unhealthy after {} failures",
|
||||||
self.container_name, consecutive_failures);
|
self.container_name, consecutive_failures);
|
||||||
// TODO: Trigger auto-restart or alert
|
// Auto-restart is handled by the orchestrator-level health monitor
|
||||||
|
// (core/archipelago/src/health_monitor.rs) which runs every 60s,
|
||||||
|
// checks all container states via `podman ps`, and restarts
|
||||||
|
// exited containers with exponential backoff (10s/30s/90s).
|
||||||
|
// This per-container monitor is for manifest-driven health
|
||||||
|
// tracking and status change callbacks only.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
|
|||||||
@ -1,18 +1,39 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
#
|
#
|
||||||
# Archipelago Main Menu
|
# archipelago main menu
|
||||||
# Interactive setup wizard for Archipelago Bitcoin Node OS
|
# interactive setup for archipelago bitcoin node os
|
||||||
#
|
#
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
|
||||||
|
# Colors (256-color — works on Linux console with fbcon)
|
||||||
|
O=$'\033[38;5;208m' # Orange
|
||||||
|
W=$'\033[1;37m' # Bold white
|
||||||
|
D=$'\033[38;5;242m' # Dim
|
||||||
|
C=$'\033[38;5;37m' # Cyan
|
||||||
|
G=$'\033[38;5;35m' # Green
|
||||||
|
R=$'\033[38;5;196m' # Red
|
||||||
|
Y=$'\033[38;5;220m' # Yellow
|
||||||
|
N=$'\033[0m' # Reset
|
||||||
|
|
||||||
|
# Adaptive centering
|
||||||
|
get_width() { TW=$(tput cols 2>/dev/null || echo 60); [ "$TW" -gt 120 ] && TW=120; }
|
||||||
|
get_width
|
||||||
|
cc() { local s=$(echo -e "$1" | sed 's/\x1b\[[0-9;]*m//g'); local p=$(( (TW - ${#s}) / 2 )); [ $p -lt 0 ] && p=0; printf "%*s" "$p" ""; echo -e "$1"; }
|
||||||
|
|
||||||
|
# Box helpers (Claude-style rounded corners)
|
||||||
|
bw() { echo $((TW > 52 ? 52 : TW - 4)); }
|
||||||
|
btop() { local w=$(bw); local t="╭"; for i in $(seq 1 $((w-2))); do t="${t}─"; done; cc "${D}${t}╮${N}"; }
|
||||||
|
bbox() { local w=$(bw); local s=$(echo -e "$1" | sed 's/\x1b\[[0-9;]*m//g'); local pad=$((w - 2 - ${#s})); [ $pad -lt 0 ] && pad=0; local r=""; for i in $(seq 1 $pad); do r="${r} "; done; cc "${D}│${N} $1${r}${D}│${N}"; }
|
||||||
|
bbot() { local w=$(bw); local b="╰"; for i in $(seq 1 $((w-2))); do b="${b}─"; done; cc "${D}${b}╯${N}"; }
|
||||||
|
hrule() { local len=$((TW > 50 ? 50 : TW - 4)); local hr=""; for i in $(seq 1 $len); do hr="${hr}─"; done; cc "${D}${hr}${N}"; }
|
||||||
|
|
||||||
# Install required tools on first run (for live mode)
|
# Install required tools on first run (for live mode)
|
||||||
install_required_tools() {
|
install_required_tools() {
|
||||||
if [ -f /tmp/.archipelago-tools-installed ]; then
|
if [ -f /tmp/.archipelago-tools-installed ]; then
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Check if we need to install tools
|
|
||||||
local NEED_TOOLS=0
|
local NEED_TOOLS=0
|
||||||
for tool in parted debootstrap mkfs.ext4 mkfs.vfat; do
|
for tool in parted debootstrap mkfs.ext4 mkfs.vfat; do
|
||||||
if ! command -v $tool >/dev/null 2>&1; then
|
if ! command -v $tool >/dev/null 2>&1; then
|
||||||
@ -23,11 +44,11 @@ install_required_tools() {
|
|||||||
|
|
||||||
if [ $NEED_TOOLS -eq 1 ]; then
|
if [ $NEED_TOOLS -eq 1 ]; then
|
||||||
echo ""
|
echo ""
|
||||||
echo " 📦 Installing required tools (first run)..."
|
cc "${D}installing required tools...${N}"
|
||||||
echo ""
|
echo ""
|
||||||
sudo apt-get update -qq 2>/dev/null
|
sudo apt-get update -qq 2>/dev/null
|
||||||
sudo apt-get install -y parted debootstrap dosfstools e2fsprogs 2>/dev/null
|
sudo apt-get install -y parted debootstrap dosfstools e2fsprogs 2>/dev/null
|
||||||
echo " ✅ Tools installed"
|
cc "${G}tools installed${N}"
|
||||||
echo ""
|
echo ""
|
||||||
sleep 1
|
sleep 1
|
||||||
fi
|
fi
|
||||||
@ -35,57 +56,44 @@ install_required_tools() {
|
|||||||
touch /tmp/.archipelago-tools-installed
|
touch /tmp/.archipelago-tools-installed
|
||||||
}
|
}
|
||||||
|
|
||||||
# Run tool installation at startup
|
|
||||||
install_required_tools
|
install_required_tools
|
||||||
|
|
||||||
show_banner() {
|
show_banner() {
|
||||||
|
get_width
|
||||||
clear
|
clear
|
||||||
echo ""
|
echo ""
|
||||||
echo " ╔═══════════════════════════════════════════════════════════╗"
|
btop
|
||||||
echo " ║ ║"
|
bbox ""
|
||||||
echo " ║ 🏝️ ARCHIPELAGO BITCOIN NODE OS ║"
|
bbox "${W}a r c h i p e l a g o${N}"
|
||||||
echo " ║ ║"
|
bbox "${O}━━━━━━━━━━━━━━━━━━━━━${N}"
|
||||||
echo " ║ Your sovereign Bitcoin infrastructure ║"
|
bbox "${D}bitcoin node os${N}"
|
||||||
echo " ║ ║"
|
bbox ""
|
||||||
echo " ╚═══════════════════════════════════════════════════════════╝"
|
bbot
|
||||||
echo ""
|
echo ""
|
||||||
}
|
}
|
||||||
|
|
||||||
show_status() {
|
show_status() {
|
||||||
echo " System Status:"
|
|
||||||
echo " ─────────────────────────────────────────────────────────────"
|
|
||||||
|
|
||||||
# Check if we're in live mode
|
|
||||||
if [ -d /run/live ]; then
|
if [ -d /run/live ]; then
|
||||||
echo " Mode: 🔴 Live (changes won't persist)"
|
cc "${R}live mode${N} ${D}(changes won't persist)${N}"
|
||||||
else
|
else
|
||||||
echo " Mode: 🟢 Installed"
|
cc "${G}installed${N}"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
local podman_ok=0
|
||||||
|
command -v podman >/dev/null 2>&1 && podman_ok=1
|
||||||
|
|
||||||
|
if [ $podman_ok -eq 1 ] && podman ps 2>/dev/null | grep -q bitcoind; then
|
||||||
|
local blocks=$(podman exec bitcoind bitcoin-cli getblockcount 2>/dev/null || echo "syncing")
|
||||||
|
cc "${G}bitcoin${N} ${D}running ($blocks blocks)${N}"
|
||||||
|
elif [ $podman_ok -eq 1 ] && podman ps -a 2>/dev/null | grep -q bitcoind; then
|
||||||
|
cc "${Y}bitcoin${N} ${D}stopped${N}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Check Podman
|
if [ $podman_ok -eq 1 ] && podman ps 2>/dev/null | grep -q lnd; then
|
||||||
if command -v podman >/dev/null 2>&1; then
|
cc "${G}lightning${N} ${D}running${N}"
|
||||||
echo " Podman: 🟢 Installed"
|
elif [ $podman_ok -eq 1 ] && podman ps -a 2>/dev/null | grep -q lnd; then
|
||||||
else
|
cc "${Y}lightning${N} ${D}stopped${N}"
|
||||||
echo " Podman: 🔴 Not installed"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check Bitcoin Core
|
|
||||||
if podman ps 2>/dev/null | grep -q bitcoind; then
|
|
||||||
BLOCKS=$(podman exec bitcoind bitcoin-cli getblockcount 2>/dev/null || echo "syncing")
|
|
||||||
echo " Bitcoin: 🟢 Running (blocks: $BLOCKS)"
|
|
||||||
elif podman ps -a 2>/dev/null | grep -q bitcoind; then
|
|
||||||
echo " Bitcoin: 🟡 Stopped"
|
|
||||||
else
|
|
||||||
echo " Bitcoin: ⚪ Not configured"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check LND
|
|
||||||
if podman ps 2>/dev/null | grep -q lnd; then
|
|
||||||
echo " Lightning: 🟢 Running"
|
|
||||||
elif podman ps -a 2>/dev/null | grep -q lnd; then
|
|
||||||
echo " Lightning: 🟡 Stopped"
|
|
||||||
else
|
|
||||||
echo " Lightning: ⚪ Not configured"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
@ -96,125 +104,111 @@ main_menu() {
|
|||||||
show_banner
|
show_banner
|
||||||
show_status
|
show_status
|
||||||
|
|
||||||
# Show Web UI URL prominently
|
# Connection info
|
||||||
IP=$(hostname -I 2>/dev/null | awk '{print $1}')
|
IP=$(hostname -I 2>/dev/null | awk '{print $1}')
|
||||||
[ -z "$IP" ] && IP=$(ip -4 addr show | grep -oP '(?<=inet\s)\d+(\.\d+){3}' | grep -v '127.0.0.1' | head -1)
|
[ -z "$IP" ] && IP=$(ip -4 addr show | grep -oP '(?<=inet\s)\d+(\.\d+){3}' | grep -v '127.0.0.1' | head -1)
|
||||||
|
|
||||||
echo " ┌─────────────────────────────────────────────────────────────┐"
|
|
||||||
if [ -n "$IP" ]; then
|
if [ -n "$IP" ]; then
|
||||||
# Check if backend is running
|
|
||||||
if pgrep -f "archipelago" >/dev/null 2>&1; then
|
if pgrep -f "archipelago" >/dev/null 2>&1; then
|
||||||
echo " │ 🌐 Web UI: http://$IP:5678 (running) │"
|
cc "${C}web ui${N} ${W}http://$IP${N}"
|
||||||
else
|
else
|
||||||
echo " │ 🌐 Web UI: http://$IP:5678 (not started) │"
|
cc "${C}web ui${N} ${D}http://$IP${N} ${Y}(not started)${N}"
|
||||||
fi
|
fi
|
||||||
echo " │ 📡 SSH: ssh user@$IP (password: archipelago) │"
|
cc "${C}ssh${N} ${D}archipelago@$IP${N}"
|
||||||
else
|
else
|
||||||
echo " │ 🌐 Web UI: (no network) │"
|
cc "${D}no network detected${N}"
|
||||||
fi
|
fi
|
||||||
echo " └─────────────────────────────────────────────────────────────┘"
|
|
||||||
|
echo ""
|
||||||
|
hrule
|
||||||
|
echo ""
|
||||||
|
cc "${D}r${N} refresh status ${D}w${N} start web ui"
|
||||||
|
echo ""
|
||||||
|
cc "${O}1${N} install to disk ${O}5${N} view logs"
|
||||||
|
cc "${O}2${N} setup bitcoin core ${O}6${N} network settings"
|
||||||
|
cc "${O}3${N} setup lightning ${O}7${N} system info"
|
||||||
|
cc "${O}4${N} setup btcpay server"
|
||||||
|
echo ""
|
||||||
|
cc "${D}q quit${N}"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
echo " Main Menu:"
|
local pad=$(( (TW - 18) / 2 ))
|
||||||
echo " ─────────────────────────────────────────────────────────────"
|
[ $pad -lt 0 ] && pad=0
|
||||||
echo ""
|
printf "%*s" "$pad" ""
|
||||||
echo " r) Refresh - Update IP/status (no restart needed)"
|
read -p "select option: " choice
|
||||||
echo " w) Open Web UI - Launch graphical interface"
|
|
||||||
echo ""
|
|
||||||
echo " 1) Install to Disk - Permanently install Archipelago"
|
|
||||||
echo " 2) Setup Bitcoin Core - Configure Bitcoin full node"
|
|
||||||
echo " 3) Setup Lightning (LND) - Configure Lightning Network"
|
|
||||||
echo " 4) Setup BTCPay Server - Bitcoin payment processor"
|
|
||||||
echo " 5) View Logs - Monitor running services"
|
|
||||||
echo " 6) Network Settings - Configure networking"
|
|
||||||
echo " 7) System Info - View system information"
|
|
||||||
echo ""
|
|
||||||
echo " q) Quit"
|
|
||||||
echo ""
|
|
||||||
read -p " Select option: " choice
|
|
||||||
|
|
||||||
case $choice in
|
case $choice in
|
||||||
r|R)
|
r|R)
|
||||||
# Refresh - just loop again to show updated IP/status
|
|
||||||
;;
|
;;
|
||||||
w|W)
|
w|W)
|
||||||
echo ""
|
echo ""
|
||||||
# Start the real backend on port 5678
|
|
||||||
if command -v archipelago >/dev/null 2>&1; then
|
if command -v archipelago >/dev/null 2>&1; then
|
||||||
if pgrep -f "archipelago" >/dev/null 2>&1; then
|
if pgrep -f "archipelago" >/dev/null 2>&1; then
|
||||||
echo " ✅ Archipelago backend already running on port 5678"
|
cc "${G}backend already running${N}"
|
||||||
else
|
else
|
||||||
echo " 🚀 Starting Archipelago backend on port 5678..."
|
cc "${D}starting backend on port 5678...${N}"
|
||||||
nohup archipelago >/tmp/archipelago.log 2>&1 &
|
nohup archipelago >/tmp/archipelago.log 2>&1 &
|
||||||
sleep 2
|
sleep 2
|
||||||
if pgrep -f "archipelago" >/dev/null 2>&1; then
|
if pgrep -f "archipelago" >/dev/null 2>&1; then
|
||||||
echo " ✅ Backend started!"
|
cc "${G}backend started${N}"
|
||||||
else
|
else
|
||||||
echo " ⚠️ Failed to start backend. Check /tmp/archipelago.log"
|
cc "${R}failed — see /tmp/archipelago.log${N}"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
IP=$(hostname -I 2>/dev/null | awk '{print $1}')
|
IP=$(hostname -I 2>/dev/null | awk '{print $1}')
|
||||||
echo ""
|
echo ""
|
||||||
echo " ┌─────────────────────────────────────────────────────────────┐"
|
cc "open in browser: ${W}http://$IP${N}"
|
||||||
echo " │ 🌐 Open in browser: http://$IP:5678 │"
|
|
||||||
echo " └─────────────────────────────────────────────────────────────┘"
|
|
||||||
else
|
else
|
||||||
echo " ⚠️ Archipelago binary not found at /usr/local/bin/archipelago"
|
cc "${R}binary not found at /usr/local/bin/archipelago${N}"
|
||||||
echo ""
|
|
||||||
echo " Try running:"
|
|
||||||
echo " sudo cp /run/live/medium/archipelago/bin/archipelago /usr/local/bin/"
|
|
||||||
fi
|
fi
|
||||||
echo ""
|
echo ""
|
||||||
read -p " Press Enter to continue..."
|
read -sp " press enter to continue..."
|
||||||
;;
|
;;
|
||||||
1)
|
1)
|
||||||
if [ -f "$SCRIPT_DIR/install-to-disk.sh" ]; then
|
if [ -f "$SCRIPT_DIR/install-to-disk.sh" ]; then
|
||||||
sudo bash "$SCRIPT_DIR/install-to-disk.sh"
|
sudo bash "$SCRIPT_DIR/install-to-disk.sh"
|
||||||
else
|
else
|
||||||
echo "Installer not found. Running from: $SCRIPT_DIR"
|
echo " installer not found at: $SCRIPT_DIR"
|
||||||
fi
|
fi
|
||||||
read -p "Press Enter to continue..."
|
read -sp " press enter to continue..."
|
||||||
;;
|
;;
|
||||||
2)
|
2)
|
||||||
if [ -f "$SCRIPT_DIR/setup-bitcoin.sh" ]; then
|
if [ -f "$SCRIPT_DIR/setup-bitcoin.sh" ]; then
|
||||||
bash "$SCRIPT_DIR/setup-bitcoin.sh"
|
bash "$SCRIPT_DIR/setup-bitcoin.sh"
|
||||||
else
|
else
|
||||||
echo "Bitcoin setup script not found."
|
echo " bitcoin setup script not found."
|
||||||
fi
|
fi
|
||||||
read -p "Press Enter to continue..."
|
read -sp " press enter to continue..."
|
||||||
;;
|
;;
|
||||||
3)
|
3)
|
||||||
if [ -f "$SCRIPT_DIR/setup-lnd.sh" ]; then
|
if [ -f "$SCRIPT_DIR/setup-lnd.sh" ]; then
|
||||||
bash "$SCRIPT_DIR/setup-lnd.sh"
|
bash "$SCRIPT_DIR/setup-lnd.sh"
|
||||||
else
|
else
|
||||||
echo "LND setup script not found."
|
echo " lnd setup script not found."
|
||||||
fi
|
fi
|
||||||
read -p "Press Enter to continue..."
|
read -sp " press enter to continue..."
|
||||||
;;
|
;;
|
||||||
4)
|
4)
|
||||||
setup_btcpay
|
setup_btcpay
|
||||||
read -p "Press Enter to continue..."
|
read -sp " press enter to continue..."
|
||||||
;;
|
;;
|
||||||
5)
|
5)
|
||||||
view_logs
|
view_logs
|
||||||
;;
|
;;
|
||||||
6)
|
6)
|
||||||
network_settings
|
network_settings
|
||||||
read -p "Press Enter to continue..."
|
read -sp " press enter to continue..."
|
||||||
;;
|
;;
|
||||||
7)
|
7)
|
||||||
system_info
|
system_info
|
||||||
read -p "Press Enter to continue..."
|
read -sp " press enter to continue..."
|
||||||
;;
|
;;
|
||||||
q|Q)
|
q|Q)
|
||||||
echo ""
|
|
||||||
echo " Goodbye! 🏝️"
|
|
||||||
echo ""
|
echo ""
|
||||||
exit 0
|
exit 0
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
echo "Invalid option"
|
sleep 0.5
|
||||||
sleep 1
|
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
@ -222,62 +216,63 @@ main_menu() {
|
|||||||
|
|
||||||
setup_btcpay() {
|
setup_btcpay() {
|
||||||
show_banner
|
show_banner
|
||||||
echo " BTCPay Server Setup"
|
cc "${W}btcpay server setup${N}"
|
||||||
echo " ─────────────────────────────────────────────────────────────"
|
cc "${D}self-hosted bitcoin payment processor${N}"
|
||||||
echo ""
|
|
||||||
echo " BTCPay Server is a self-hosted Bitcoin payment processor."
|
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
if ! podman ps | grep -q bitcoind; then
|
if ! podman ps | grep -q bitcoind; then
|
||||||
echo " ⚠️ Bitcoin Core must be running first."
|
cc "${R}bitcoin core must be running first${N}"
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
read -p " Setup BTCPay Server? [y/N]: " SETUP
|
local pad=$(( (TW - 30) / 2 ))
|
||||||
|
[ $pad -lt 0 ] && pad=0
|
||||||
|
printf "%*s" "$pad" ""
|
||||||
|
read -p "setup btcpay server? [y/N]: " SETUP
|
||||||
if [[ ! "$SETUP" =~ ^[Yy]$ ]]; then
|
if [[ ! "$SETUP" =~ ^[Yy]$ ]]; then
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo " 🐳 Pulling BTCPay Server image..."
|
cc "${D}pulling btcpay server image...${N}"
|
||||||
podman pull "${BTCPAY_IMAGE}"
|
podman pull "${BTCPAY_IMAGE}"
|
||||||
|
|
||||||
# Create data directory
|
|
||||||
mkdir -p ~/.btcpay
|
mkdir -p ~/.btcpay
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo " BTCPay Server setup is more complex and typically uses docker-compose."
|
cc "${D}full setup: https://docs.btcpayserver.org${N}"
|
||||||
echo " For a full setup, visit: https://docs.btcpayserver.org"
|
|
||||||
echo ""
|
echo ""
|
||||||
}
|
}
|
||||||
|
|
||||||
view_logs() {
|
view_logs() {
|
||||||
show_banner
|
show_banner
|
||||||
echo " View Logs"
|
cc "${W}view logs${N}"
|
||||||
echo " ─────────────────────────────────────────────────────────────"
|
|
||||||
echo ""
|
echo ""
|
||||||
echo " 1) Bitcoin Core logs"
|
cc "${O}1${N} ${D}bitcoin core${N}"
|
||||||
echo " 2) LND logs"
|
cc "${O}2${N} ${D}lnd${N}"
|
||||||
echo " 3) System logs"
|
cc "${O}3${N} ${D}system journal${N}"
|
||||||
echo " b) Back"
|
cc "${D}b back${N}"
|
||||||
echo ""
|
echo ""
|
||||||
read -p " Select: " choice
|
|
||||||
|
local pad=$(( (TW - 10) / 2 ))
|
||||||
|
[ $pad -lt 0 ] && pad=0
|
||||||
|
printf "%*s" "$pad" ""
|
||||||
|
read -p "select: " choice
|
||||||
|
|
||||||
case $choice in
|
case $choice in
|
||||||
1)
|
1)
|
||||||
if podman ps -a | grep -q bitcoind; then
|
if podman ps -a | grep -q bitcoind; then
|
||||||
podman logs -f --tail 50 bitcoind
|
podman logs -f --tail 50 bitcoind
|
||||||
else
|
else
|
||||||
echo "Bitcoin Core not running"
|
cc "${D}bitcoin core not running${N}"
|
||||||
read -p "Press Enter..."
|
read -sp " press enter..."
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
2)
|
2)
|
||||||
if podman ps -a | grep -q lnd; then
|
if podman ps -a | grep -q lnd; then
|
||||||
podman logs -f --tail 50 lnd
|
podman logs -f --tail 50 lnd
|
||||||
else
|
else
|
||||||
echo "LND not running"
|
cc "${D}lnd not running${N}"
|
||||||
read -p "Press Enter..."
|
read -sp " press enter..."
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
3)
|
3)
|
||||||
@ -288,57 +283,61 @@ view_logs() {
|
|||||||
|
|
||||||
network_settings() {
|
network_settings() {
|
||||||
show_banner
|
show_banner
|
||||||
echo " Network Settings"
|
cc "${W}network settings${N}"
|
||||||
echo " ─────────────────────────────────────────────────────────────"
|
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Show current IP
|
|
||||||
IP=$(hostname -I | awk '{print $1}')
|
IP=$(hostname -I | awk '{print $1}')
|
||||||
echo " Current IP: $IP"
|
cc "${C}ip${N} ${W}$IP${N}"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Show network interfaces
|
cc "${D}interfaces:${N}"
|
||||||
echo " Network Interfaces:"
|
|
||||||
ip -br addr | grep -v "^lo" | while read line; do
|
ip -br addr | grep -v "^lo" | while read line; do
|
||||||
echo " $line"
|
cc " ${D}$line${N}"
|
||||||
done
|
done
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
echo " Ports in use:"
|
cc "${D}service ports:${N}"
|
||||||
echo " 8332 - Bitcoin RPC"
|
cc " ${D}8332 bitcoin rpc 9735 lightning p2p${N}"
|
||||||
echo " 8333 - Bitcoin P2P"
|
cc " ${D}8333 bitcoin p2p 10009 lightning grpc${N}"
|
||||||
echo " 9735 - Lightning P2P"
|
|
||||||
echo " 10009 - Lightning gRPC"
|
|
||||||
echo " 8080 - Lightning REST"
|
|
||||||
echo ""
|
echo ""
|
||||||
}
|
}
|
||||||
|
|
||||||
system_info() {
|
system_info() {
|
||||||
show_banner
|
show_banner
|
||||||
echo " System Information"
|
cc "${W}system information${N}"
|
||||||
echo " ─────────────────────────────────────────────────────────────"
|
|
||||||
echo ""
|
echo ""
|
||||||
echo " Hostname: $(hostname)"
|
|
||||||
echo " Kernel: $(uname -r)"
|
cc "${C}host${N} ${D}$(hostname)${N}"
|
||||||
echo " Uptime: $(uptime -p)"
|
cc "${C}kernel${N} ${D}$(uname -r)${N}"
|
||||||
|
cc "${C}uptime${N} ${D}$(uptime -p 2>/dev/null || echo 'unknown')${N}"
|
||||||
echo ""
|
echo ""
|
||||||
echo " CPU: $(grep "model name" /proc/cpuinfo | head -1 | cut -d: -f2 | xargs)"
|
|
||||||
echo " Memory: $(free -h | grep Mem | awk '{print $2}') total, $(free -h | grep Mem | awk '{print $3}') used"
|
local cpu=$(grep "model name" /proc/cpuinfo 2>/dev/null | head -1 | cut -d: -f2 | xargs)
|
||||||
|
[ -n "$cpu" ] && cc "${C}cpu${N} ${D}${cpu}${N}"
|
||||||
|
|
||||||
|
local mem_total=$(free -h 2>/dev/null | grep Mem | awk '{print $2}')
|
||||||
|
local mem_used=$(free -h 2>/dev/null | grep Mem | awk '{print $3}')
|
||||||
|
[ -n "$mem_total" ] && cc "${C}memory${N} ${D}${mem_used} / ${mem_total}${N}"
|
||||||
echo ""
|
echo ""
|
||||||
echo " Disk Usage:"
|
|
||||||
df -h / | tail -1 | awk '{print " Root: " $3 " / " $2 " (" $5 " used)"}'
|
cc "${D}disk:${N}"
|
||||||
|
df -h / | tail -1 | awk '{printf " root: %s / %s (%s used)\n", $3, $2, $5}' | while read line; do
|
||||||
|
cc "${D}${line}${N}"
|
||||||
|
done
|
||||||
|
|
||||||
if [ -d ~/.bitcoin ]; then
|
if [ -d ~/.bitcoin ]; then
|
||||||
echo " Bitcoin: $(du -sh ~/.bitcoin 2>/dev/null | cut -f1)"
|
local btc_size=$(du -sh ~/.bitcoin 2>/dev/null | cut -f1)
|
||||||
|
cc " ${D}bitcoin: $btc_size${N}"
|
||||||
fi
|
fi
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Container status
|
|
||||||
echo " Containers:"
|
|
||||||
if command -v podman >/dev/null 2>&1; then
|
if command -v podman >/dev/null 2>&1; then
|
||||||
podman ps --format " {{.Names}}: {{.Status}}" 2>/dev/null || echo " No containers running"
|
cc "${D}containers:${N}"
|
||||||
|
podman ps --format " {{.Names}}: {{.Status}}" 2>/dev/null | while read line; do
|
||||||
|
cc "${D}${line}${N}"
|
||||||
|
done
|
||||||
fi
|
fi
|
||||||
echo ""
|
echo ""
|
||||||
}
|
}
|
||||||
|
|
||||||
# Run main menu
|
|
||||||
main_menu
|
main_menu
|
||||||
|
|||||||
@ -1,332 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Generate Archipelago GRUB boot background — 80s pixel cyberpunk aesthetic.
|
|
||||||
|
|
||||||
Outputs a 1024x768 PNG with:
|
|
||||||
- Near-black background with subtle radial gradient
|
|
||||||
- Scanline overlay (CRT effect)
|
|
||||||
- Pixel-art "A" logo (from Archipelago SVG) rendered in neon orange
|
|
||||||
- Neon glow effect around the logo
|
|
||||||
- Retro grid lines at the bottom (Tron-style horizon)
|
|
||||||
- Subtle vignette
|
|
||||||
|
|
||||||
Uses only PIL (Pillow) — no external dependencies.
|
|
||||||
"""
|
|
||||||
import struct
|
|
||||||
import zlib
|
|
||||||
import math
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
|
|
||||||
W, H = 1024, 768
|
|
||||||
|
|
||||||
# Archipelago brand colors
|
|
||||||
BG_DARK = (5, 5, 10) # Near-black with blue tint
|
|
||||||
BG_MID = (10, 10, 18) # Slightly lighter center
|
|
||||||
ORANGE = (251, 146, 60) # #fb923c — primary accent
|
|
||||||
ORANGE_DIM = (180, 100, 30) # Dimmed orange for glow
|
|
||||||
CYAN = (60, 200, 220) # Cyberpunk accent
|
|
||||||
MAGENTA = (180, 60, 180) # Cyberpunk accent 2
|
|
||||||
GRID_COLOR = (30, 60, 80) # Subtle teal grid
|
|
||||||
SCANLINE = (0, 0, 0) # Black scanlines
|
|
||||||
|
|
||||||
# The pixel-art "a" (lowercase) from the Archipelago SVG favicon
|
|
||||||
# Matched to the actual SVG path — it's a blocky pixel-art lowercase "a"
|
|
||||||
LOGO_A = [
|
|
||||||
[0, 1, 1, 1, 1, 0],
|
|
||||||
[0, 0, 0, 0, 0, 1],
|
|
||||||
[0, 1, 1, 1, 1, 1],
|
|
||||||
[1, 0, 0, 0, 0, 1],
|
|
||||||
[1, 0, 0, 0, 0, 1],
|
|
||||||
[0, 1, 1, 1, 1, 1],
|
|
||||||
]
|
|
||||||
|
|
||||||
# Pixel-art text: "archipelago" — 5-pixel-high bitmap font (lowercase)
|
|
||||||
PIXEL_CHARS = {
|
|
||||||
'a': [[0,1,1,0],[0,0,0,1],[0,1,1,1],[1,0,0,1],[0,1,1,1]],
|
|
||||||
'r': [[1,0,1,1],[1,1,0,0],[1,0,0,0],[1,0,0,0],[1,0,0,0]],
|
|
||||||
'c': [[0,1,1,0],[1,0,0,0],[1,0,0,0],[1,0,0,0],[0,1,1,0]],
|
|
||||||
'h': [[1,0,0,0],[1,0,0,0],[1,1,1,0],[1,0,0,1],[1,0,0,1]],
|
|
||||||
'i': [[0,1],[0,0],[0,1],[0,1],[0,1]],
|
|
||||||
'p': [[1,1,1,0],[1,0,0,1],[1,1,1,0],[1,0,0,0],[1,0,0,0]],
|
|
||||||
'e': [[0,1,1,0],[1,0,0,1],[1,1,1,0],[1,0,0,0],[0,1,1,0]],
|
|
||||||
'l': [[1,0],[1,0],[1,0],[1,0],[1,1]],
|
|
||||||
'g': [[0,1,1,1],[1,0,0,1],[0,1,1,1],[0,0,0,1],[1,1,1,0]],
|
|
||||||
'o': [[0,1,1,0],[1,0,0,1],[1,0,0,1],[1,0,0,1],[0,1,1,0]],
|
|
||||||
' ': [[0,0],[0,0],[0,0],[0,0],[0,0]],
|
|
||||||
}
|
|
||||||
LOGO_TEXT = "archipelago"
|
|
||||||
|
|
||||||
|
|
||||||
def lerp_color(c1, c2, t):
|
|
||||||
"""Linearly interpolate between two RGB colors."""
|
|
||||||
return tuple(int(a + (b - a) * t) for a, b in zip(c1, c2))
|
|
||||||
|
|
||||||
|
|
||||||
def distance(x1, y1, x2, y2):
|
|
||||||
return math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2)
|
|
||||||
|
|
||||||
|
|
||||||
def create_png(width, height, pixels):
|
|
||||||
"""Create a PNG file from raw RGB pixel data without PIL."""
|
|
||||||
|
|
||||||
def make_chunk(chunk_type, data):
|
|
||||||
chunk = chunk_type + data
|
|
||||||
return struct.pack('>I', len(data)) + chunk + struct.pack('>I', zlib.crc32(chunk) & 0xFFFFFFFF)
|
|
||||||
|
|
||||||
# PNG signature
|
|
||||||
signature = b'\x89PNG\r\n\x1a\n'
|
|
||||||
|
|
||||||
# IHDR
|
|
||||||
ihdr_data = struct.pack('>IIBBBBB', width, height, 8, 2, 0, 0, 0) # 8-bit RGB
|
|
||||||
ihdr = make_chunk(b'IHDR', ihdr_data)
|
|
||||||
|
|
||||||
# IDAT — raw pixel data with filter byte per row
|
|
||||||
raw_data = bytearray()
|
|
||||||
for y in range(height):
|
|
||||||
raw_data.append(0) # filter: none
|
|
||||||
offset = y * width * 3
|
|
||||||
raw_data.extend(pixels[offset:offset + width * 3])
|
|
||||||
|
|
||||||
compressed = zlib.compress(bytes(raw_data), 9)
|
|
||||||
idat = make_chunk(b'IDAT', compressed)
|
|
||||||
|
|
||||||
# IEND
|
|
||||||
iend = make_chunk(b'IEND', b'')
|
|
||||||
|
|
||||||
return signature + ihdr + idat + iend
|
|
||||||
|
|
||||||
|
|
||||||
def generate():
|
|
||||||
# Create pixel buffer (RGB, row-major)
|
|
||||||
pixels = bytearray(W * H * 3)
|
|
||||||
|
|
||||||
cx, cy = W // 2, H // 2 - 40 # Center, slightly above middle
|
|
||||||
|
|
||||||
# --- Background: radial gradient ---
|
|
||||||
max_dist = distance(0, 0, cx, cy)
|
|
||||||
for y in range(H):
|
|
||||||
for x in range(W):
|
|
||||||
d = distance(x, y, cx, cy) / max_dist
|
|
||||||
d = min(d, 1.0)
|
|
||||||
bg = lerp_color(BG_MID, BG_DARK, d * d) # Quadratic falloff
|
|
||||||
|
|
||||||
# Vignette — darken edges
|
|
||||||
vx = abs(x - cx) / (W / 2)
|
|
||||||
vy = abs(y - cy) / (H / 2)
|
|
||||||
vignette = max(0, 1.0 - (vx * vx + vy * vy) * 0.4)
|
|
||||||
r = int(bg[0] * vignette)
|
|
||||||
g = int(bg[1] * vignette)
|
|
||||||
b = int(bg[2] * vignette)
|
|
||||||
|
|
||||||
idx = (y * W + x) * 3
|
|
||||||
pixels[idx] = max(0, min(255, r))
|
|
||||||
pixels[idx + 1] = max(0, min(255, g))
|
|
||||||
pixels[idx + 2] = max(0, min(255, b))
|
|
||||||
|
|
||||||
# --- Retro grid (bottom third) ---
|
|
||||||
horizon_y = H * 2 // 3
|
|
||||||
for y in range(horizon_y, H):
|
|
||||||
depth = (y - horizon_y) / (H - horizon_y) # 0 at horizon, 1 at bottom
|
|
||||||
|
|
||||||
# Horizontal grid lines — spacing decreases with perspective
|
|
||||||
grid_spacing = max(4, int(40 * (1.0 - depth * 0.8)))
|
|
||||||
is_hline = ((y - horizon_y) % grid_spacing) < 1
|
|
||||||
|
|
||||||
for x in range(W):
|
|
||||||
# Vertical grid lines — converge toward center
|
|
||||||
spread = 0.3 + depth * 0.7 # Lines spread more toward bottom
|
|
||||||
grid_x = (x - cx) / (spread * W / 2) * 12
|
|
||||||
is_vline = abs(grid_x - round(grid_x)) < 0.04
|
|
||||||
|
|
||||||
if is_hline or is_vline:
|
|
||||||
alpha = 0.15 + depth * 0.25 # Brighter closer to viewer
|
|
||||||
idx = (y * W + x) * 3
|
|
||||||
for c in range(3):
|
|
||||||
old = pixels[idx + c]
|
|
||||||
pixels[idx + c] = min(255, int(old + GRID_COLOR[c] * alpha))
|
|
||||||
|
|
||||||
# --- Horizon glow line ---
|
|
||||||
for x in range(W):
|
|
||||||
dx = abs(x - cx) / (W / 2)
|
|
||||||
intensity = max(0, 1.0 - dx * 1.5) * 0.4
|
|
||||||
for dy in range(-2, 3):
|
|
||||||
y = horizon_y + dy
|
|
||||||
if 0 <= y < H:
|
|
||||||
falloff = 1.0 - abs(dy) / 3.0
|
|
||||||
idx = (y * W + x) * 3
|
|
||||||
pixels[idx] = min(255, int(pixels[idx] + CYAN[0] * intensity * falloff))
|
|
||||||
pixels[idx + 1] = min(255, int(pixels[idx + 1] + CYAN[1] * intensity * falloff))
|
|
||||||
pixels[idx + 2] = min(255, int(pixels[idx + 2] + CYAN[2] * intensity * falloff))
|
|
||||||
|
|
||||||
# --- Pixel-art "A" logo ---
|
|
||||||
logo_rows = len(LOGO_A)
|
|
||||||
logo_cols = len(LOGO_A[0])
|
|
||||||
pixel_size = 14
|
|
||||||
logo_w = logo_cols * pixel_size
|
|
||||||
logo_h = logo_rows * pixel_size
|
|
||||||
logo_x = cx - logo_w // 2
|
|
||||||
logo_y = 80
|
|
||||||
|
|
||||||
# Glow behind logo
|
|
||||||
glow_radius = 90
|
|
||||||
for y in range(max(0, logo_y - glow_radius), min(H, logo_y + logo_h + glow_radius)):
|
|
||||||
for x in range(max(0, logo_x - glow_radius), min(W, logo_x + logo_w + glow_radius)):
|
|
||||||
# Distance to logo bounding box
|
|
||||||
dx = max(0, logo_x - x, x - (logo_x + logo_w))
|
|
||||||
dy = max(0, logo_y - y, y - (logo_y + logo_h))
|
|
||||||
d = math.sqrt(dx * dx + dy * dy)
|
|
||||||
if d < glow_radius:
|
|
||||||
alpha = (1.0 - d / glow_radius) ** 2 * 0.15
|
|
||||||
idx = (y * W + x) * 3
|
|
||||||
pixels[idx] = min(255, int(pixels[idx] + ORANGE[0] * alpha))
|
|
||||||
pixels[idx + 1] = min(255, int(pixels[idx + 1] + ORANGE[1] * alpha))
|
|
||||||
pixels[idx + 2] = min(255, int(pixels[idx + 2] + ORANGE[2] * alpha))
|
|
||||||
|
|
||||||
# Draw logo pixels with 3D depth/shadow effect
|
|
||||||
shadow_offset = 3 # Pixel offset for 3D shadow
|
|
||||||
for row in range(logo_rows):
|
|
||||||
for col in range(logo_cols):
|
|
||||||
if LOGO_A[row][col]:
|
|
||||||
px = logo_x + col * pixel_size
|
|
||||||
py = logo_y + row * pixel_size
|
|
||||||
# Shadow layer (dark, offset down-right)
|
|
||||||
for dy in range(pixel_size - 1):
|
|
||||||
for dx in range(pixel_size - 1):
|
|
||||||
x = px + dx + shadow_offset
|
|
||||||
y = py + dy + shadow_offset
|
|
||||||
if 0 <= x < W and 0 <= y < H:
|
|
||||||
idx = (y * W + x) * 3
|
|
||||||
pixels[idx] = max(0, pixels[idx] - 5)
|
|
||||||
pixels[idx + 1] = min(255, pixels[idx + 1] + 15)
|
|
||||||
pixels[idx + 2] = min(255, pixels[idx + 2] + 20)
|
|
||||||
# Main pixel with highlight gradient (brighter at top-left)
|
|
||||||
for dy in range(pixel_size - 1):
|
|
||||||
for dx in range(pixel_size - 1):
|
|
||||||
x, y = px + dx, py + dy
|
|
||||||
if 0 <= x < W and 0 <= y < H:
|
|
||||||
# Gradient: top-left bright, bottom-right darker
|
|
||||||
t = (dx + dy) / (2 * pixel_size)
|
|
||||||
r = int(ORANGE[0] * (1.0 - t * 0.3))
|
|
||||||
g = int(ORANGE[1] * (1.0 - t * 0.3))
|
|
||||||
b = int(ORANGE[2] * (1.0 - t * 0.3))
|
|
||||||
# Top-left highlight for 3D bevel
|
|
||||||
if dx < 2 or dy < 2:
|
|
||||||
r = min(255, r + 40)
|
|
||||||
g = min(255, g + 30)
|
|
||||||
b = min(255, b + 10)
|
|
||||||
idx = (y * W + x) * 3
|
|
||||||
pixels[idx] = r
|
|
||||||
pixels[idx + 1] = g
|
|
||||||
pixels[idx + 2] = b
|
|
||||||
|
|
||||||
# --- Pixel-art text "archipelago" below logo ---
|
|
||||||
text_pixel = 4 # Smaller pixels for text
|
|
||||||
text_gap = 2 # Gap between characters in pixels
|
|
||||||
# Calculate total text width
|
|
||||||
total_w = 0
|
|
||||||
for ch in LOGO_TEXT:
|
|
||||||
char_data = PIXEL_CHARS.get(ch, PIXEL_CHARS[' '])
|
|
||||||
total_w += len(char_data[0]) * text_pixel + text_gap
|
|
||||||
total_w -= text_gap # No gap after last char
|
|
||||||
|
|
||||||
text_x = cx - total_w // 2
|
|
||||||
text_y = logo_y + logo_h + 20 # Below the logo
|
|
||||||
|
|
||||||
cursor_x = text_x
|
|
||||||
for ch in LOGO_TEXT:
|
|
||||||
char_data = PIXEL_CHARS.get(ch, PIXEL_CHARS[' '])
|
|
||||||
char_h = len(char_data)
|
|
||||||
char_w = len(char_data[0])
|
|
||||||
for row in range(char_h):
|
|
||||||
for col in range(char_w):
|
|
||||||
if char_data[row][col]:
|
|
||||||
px = cursor_x + col * text_pixel
|
|
||||||
py = text_y + row * text_pixel
|
|
||||||
for dy in range(text_pixel - 1):
|
|
||||||
for dx in range(text_pixel - 1):
|
|
||||||
x, y = px + dx, py + dy
|
|
||||||
if 0 <= x < W and 0 <= y < H:
|
|
||||||
idx = (y * W + x) * 3
|
|
||||||
# Dimmer orange for text
|
|
||||||
pixels[idx] = ORANGE_DIM[0]
|
|
||||||
pixels[idx + 1] = ORANGE_DIM[1]
|
|
||||||
pixels[idx + 2] = ORANGE_DIM[2]
|
|
||||||
cursor_x += char_w * text_pixel + text_gap
|
|
||||||
|
|
||||||
# --- Decorative neon lines flanking the text ---
|
|
||||||
line_y = text_y + 5 * text_pixel + 12
|
|
||||||
line_w = total_w + 40
|
|
||||||
line_x1 = cx - line_w // 2
|
|
||||||
line_x2 = cx + line_w // 2
|
|
||||||
for x in range(line_x1, line_x2):
|
|
||||||
if 0 <= x < W:
|
|
||||||
# Fade at edges
|
|
||||||
edge_dist = min(x - line_x1, line_x2 - x)
|
|
||||||
alpha = min(1.0, edge_dist / 30.0) * 0.5
|
|
||||||
for dy in range(2):
|
|
||||||
y = line_y + dy
|
|
||||||
if 0 <= y < H:
|
|
||||||
idx = (y * W + x) * 3
|
|
||||||
pixels[idx] = min(255, int(pixels[idx] + CYAN[0] * alpha * 0.3))
|
|
||||||
pixels[idx + 1] = min(255, int(pixels[idx + 1] + CYAN[1] * alpha * 0.3))
|
|
||||||
pixels[idx + 2] = min(255, int(pixels[idx + 2] + CYAN[2] * alpha * 0.3))
|
|
||||||
|
|
||||||
# --- "self-sovereign bitcoin infrastructure" tagline ---
|
|
||||||
TAG_CHARS = {
|
|
||||||
's': [[0,1,1],[1,0,0],[0,1,0],[0,0,1],[1,1,0]],
|
|
||||||
'f': [[0,1,1],[1,0,0],[1,1,0],[1,0,0],[1,0,0]],
|
|
||||||
'-': [[0,0,0],[0,0,0],[1,1,1],[0,0,0],[0,0,0]],
|
|
||||||
'v': [[1,0,1],[1,0,1],[1,0,1],[0,1,0],[0,1,0]],
|
|
||||||
'n': [[1,0,0,0],[1,1,1,0],[1,0,0,1],[1,0,0,1],[1,0,0,1]],
|
|
||||||
'b': [[1,0,0,0],[1,0,0,0],[1,1,1,0],[1,0,0,1],[1,1,1,0]],
|
|
||||||
't': [[0,1,0],[1,1,1],[0,1,0],[0,1,0],[0,0,1]],
|
|
||||||
'd': [[0,0,0,1],[0,0,0,1],[0,1,1,1],[1,0,0,1],[0,1,1,1]],
|
|
||||||
'u': [[1,0,0,1],[1,0,0,1],[1,0,0,1],[1,0,0,1],[0,1,1,1]],
|
|
||||||
}
|
|
||||||
# Merge with existing chars
|
|
||||||
all_chars = {**PIXEL_CHARS, **TAG_CHARS}
|
|
||||||
tagline = "self-sovereign bitcoin node"
|
|
||||||
tag_pixel = 3
|
|
||||||
tag_gap = 2
|
|
||||||
tag_total = sum(len(all_chars.get(c, all_chars[' '])[0]) * tag_pixel + tag_gap for c in tagline) - tag_gap
|
|
||||||
tag_x = cx - tag_total // 2
|
|
||||||
tag_y = line_y + 8
|
|
||||||
tag_cursor = tag_x
|
|
||||||
for ch in tagline:
|
|
||||||
char_data = all_chars.get(ch, all_chars[' '])
|
|
||||||
char_h = len(char_data)
|
|
||||||
char_w = len(char_data[0])
|
|
||||||
for row in range(char_h):
|
|
||||||
for col in range(char_w):
|
|
||||||
if char_data[row][col]:
|
|
||||||
px = tag_cursor + col * tag_pixel
|
|
||||||
py = tag_y + row * tag_pixel
|
|
||||||
for dy in range(tag_pixel - 1):
|
|
||||||
for dx in range(tag_pixel - 1):
|
|
||||||
x, y = px + dx, py + dy
|
|
||||||
if 0 <= x < W and 0 <= y < H:
|
|
||||||
idx = (y * W + x) * 3
|
|
||||||
pixels[idx] = min(255, pixels[idx] + 40)
|
|
||||||
pixels[idx + 1] = min(255, pixels[idx + 1] + 50)
|
|
||||||
pixels[idx + 2] = min(255, pixels[idx + 2] + 55)
|
|
||||||
tag_cursor += char_w * tag_pixel + tag_gap
|
|
||||||
|
|
||||||
# --- Scanlines (every other row, subtle) ---
|
|
||||||
for y in range(0, H, 2):
|
|
||||||
for x in range(W):
|
|
||||||
idx = (y * W + x) * 3
|
|
||||||
pixels[idx] = int(pixels[idx] * 0.92)
|
|
||||||
pixels[idx + 1] = int(pixels[idx + 1] * 0.92)
|
|
||||||
pixels[idx + 2] = int(pixels[idx + 2] * 0.92)
|
|
||||||
|
|
||||||
# --- Generate PNG ---
|
|
||||||
png_data = create_png(W, H, bytes(pixels))
|
|
||||||
return png_data
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
out_path = sys.argv[1] if len(sys.argv) > 1 else 'background.png'
|
|
||||||
png_data = generate()
|
|
||||||
with open(out_path, 'wb') as f:
|
|
||||||
f.write(png_data)
|
|
||||||
print(f'Generated {out_path} ({len(png_data)} bytes)')
|
|
||||||
@ -1,149 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Generate the Archipelago Plymouth boot logo — pixel-art 'a' with neon glow.
|
|
||||||
|
|
||||||
Outputs a 256x256 PNG with transparent background (RGBA).
|
|
||||||
"""
|
|
||||||
import struct
|
|
||||||
import zlib
|
|
||||||
import math
|
|
||||||
import sys
|
|
||||||
|
|
||||||
W, H = 256, 256
|
|
||||||
ORANGE = (251, 146, 60)
|
|
||||||
ORANGE_BRIGHT = (255, 180, 100)
|
|
||||||
|
|
||||||
# Lowercase pixel-art "a" — 6x6 grid
|
|
||||||
LOGO_A = [
|
|
||||||
[0, 1, 1, 1, 1, 0],
|
|
||||||
[0, 0, 0, 0, 0, 1],
|
|
||||||
[0, 1, 1, 1, 1, 1],
|
|
||||||
[1, 0, 0, 0, 0, 1],
|
|
||||||
[1, 0, 0, 0, 0, 1],
|
|
||||||
[0, 1, 1, 1, 1, 1],
|
|
||||||
]
|
|
||||||
|
|
||||||
# "archipelago" text — same pixel font as GRUB generator
|
|
||||||
PIXEL_CHARS = {
|
|
||||||
'a': [[0,1,1,0],[0,0,0,1],[0,1,1,1],[1,0,0,1],[0,1,1,1]],
|
|
||||||
'r': [[1,0,1,1],[1,1,0,0],[1,0,0,0],[1,0,0,0],[1,0,0,0]],
|
|
||||||
'c': [[0,1,1,0],[1,0,0,0],[1,0,0,0],[1,0,0,0],[0,1,1,0]],
|
|
||||||
'h': [[1,0,0,0],[1,0,0,0],[1,1,1,0],[1,0,0,1],[1,0,0,1]],
|
|
||||||
'i': [[0,1],[0,0],[0,1],[0,1],[0,1]],
|
|
||||||
'p': [[1,1,1,0],[1,0,0,1],[1,1,1,0],[1,0,0,0],[1,0,0,0]],
|
|
||||||
'e': [[0,1,1,0],[1,0,0,1],[1,1,1,0],[1,0,0,0],[0,1,1,0]],
|
|
||||||
'l': [[1,0],[1,0],[1,0],[1,0],[1,1]],
|
|
||||||
'g': [[0,1,1,1],[1,0,0,1],[0,1,1,1],[0,0,0,1],[1,1,1,0]],
|
|
||||||
'o': [[0,1,1,0],[1,0,0,1],[1,0,0,1],[1,0,0,1],[0,1,1,0]],
|
|
||||||
' ': [[0,0],[0,0],[0,0],[0,0],[0,0]],
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def create_png_rgba(width, height, pixels):
|
|
||||||
"""Create a PNG with RGBA pixel data."""
|
|
||||||
def make_chunk(chunk_type, data):
|
|
||||||
chunk = chunk_type + data
|
|
||||||
return struct.pack('>I', len(data)) + chunk + struct.pack('>I', zlib.crc32(chunk) & 0xFFFFFFFF)
|
|
||||||
|
|
||||||
signature = b'\x89PNG\r\n\x1a\n'
|
|
||||||
ihdr_data = struct.pack('>IIBBBBB', width, height, 8, 6, 0, 0, 0) # 8-bit RGBA
|
|
||||||
ihdr = make_chunk(b'IHDR', ihdr_data)
|
|
||||||
|
|
||||||
raw_data = bytearray()
|
|
||||||
for y in range(height):
|
|
||||||
raw_data.append(0)
|
|
||||||
offset = y * width * 4
|
|
||||||
raw_data.extend(pixels[offset:offset + width * 4])
|
|
||||||
|
|
||||||
compressed = zlib.compress(bytes(raw_data), 9)
|
|
||||||
idat = make_chunk(b'IDAT', compressed)
|
|
||||||
iend = make_chunk(b'IEND', b'')
|
|
||||||
|
|
||||||
return signature + ihdr + idat + iend
|
|
||||||
|
|
||||||
|
|
||||||
def generate():
|
|
||||||
pixels = bytearray(W * H * 4) # RGBA
|
|
||||||
|
|
||||||
cx, cy = W // 2, W // 2 - 30
|
|
||||||
|
|
||||||
logo_rows = len(LOGO_A)
|
|
||||||
logo_cols = len(LOGO_A[0])
|
|
||||||
pixel_size = 18
|
|
||||||
logo_w = logo_cols * pixel_size
|
|
||||||
logo_h = logo_rows * pixel_size
|
|
||||||
logo_x = cx - logo_w // 2
|
|
||||||
logo_y = 30
|
|
||||||
|
|
||||||
# Glow
|
|
||||||
glow_radius = 60
|
|
||||||
for y in range(H):
|
|
||||||
for x in range(W):
|
|
||||||
dx = max(0, logo_x - x, x - (logo_x + logo_w))
|
|
||||||
dy = max(0, logo_y - y, y - (logo_y + logo_h))
|
|
||||||
d = math.sqrt(dx * dx + dy * dy)
|
|
||||||
if d < glow_radius:
|
|
||||||
alpha = (1.0 - d / glow_radius) ** 2 * 0.25
|
|
||||||
idx = (y * W + x) * 4
|
|
||||||
pixels[idx] = ORANGE[0]
|
|
||||||
pixels[idx + 1] = ORANGE[1]
|
|
||||||
pixels[idx + 2] = ORANGE[2]
|
|
||||||
pixels[idx + 3] = int(alpha * 255)
|
|
||||||
|
|
||||||
# Logo pixels with 3D bevel
|
|
||||||
for row in range(logo_rows):
|
|
||||||
for col in range(logo_cols):
|
|
||||||
if LOGO_A[row][col]:
|
|
||||||
px = logo_x + col * pixel_size
|
|
||||||
py = logo_y + row * pixel_size
|
|
||||||
for dy in range(pixel_size - 1):
|
|
||||||
for dx in range(pixel_size - 1):
|
|
||||||
x, y = px + dx, py + dy
|
|
||||||
if 0 <= x < W and 0 <= y < H:
|
|
||||||
t = (dx + dy) / (2 * pixel_size)
|
|
||||||
r = int(ORANGE[0] * (1.0 - t * 0.3))
|
|
||||||
g = int(ORANGE[1] * (1.0 - t * 0.3))
|
|
||||||
b = int(ORANGE[2] * (1.0 - t * 0.3))
|
|
||||||
if dx < 2 or dy < 2:
|
|
||||||
r = min(255, r + 40)
|
|
||||||
g = min(255, g + 30)
|
|
||||||
b = min(255, b + 10)
|
|
||||||
idx = (y * W + x) * 4
|
|
||||||
pixels[idx] = r
|
|
||||||
pixels[idx + 1] = g
|
|
||||||
pixels[idx + 2] = b
|
|
||||||
pixels[idx + 3] = 255
|
|
||||||
|
|
||||||
# Text "archipelago" below logo
|
|
||||||
text = "archipelago"
|
|
||||||
text_pixel = 3
|
|
||||||
text_gap = 2
|
|
||||||
total_w = sum(len(PIXEL_CHARS.get(c, PIXEL_CHARS[' '])[0]) * text_pixel + text_gap for c in text) - text_gap
|
|
||||||
text_x = cx - total_w // 2
|
|
||||||
text_y = logo_y + logo_h + 16
|
|
||||||
cursor = text_x
|
|
||||||
for ch in text:
|
|
||||||
char_data = PIXEL_CHARS.get(ch, PIXEL_CHARS[' '])
|
|
||||||
for row in range(len(char_data)):
|
|
||||||
for col in range(len(char_data[0])):
|
|
||||||
if char_data[row][col]:
|
|
||||||
for dy in range(text_pixel - 1):
|
|
||||||
for dx in range(text_pixel - 1):
|
|
||||||
x = cursor + col * text_pixel + dx
|
|
||||||
y = text_y + row * text_pixel + dy
|
|
||||||
if 0 <= x < W and 0 <= y < H:
|
|
||||||
idx = (y * W + x) * 4
|
|
||||||
pixels[idx] = 180
|
|
||||||
pixels[idx + 1] = 100
|
|
||||||
pixels[idx + 2] = 30
|
|
||||||
pixels[idx + 3] = 200
|
|
||||||
cursor += len(char_data[0]) * text_pixel + text_gap
|
|
||||||
|
|
||||||
return create_png_rgba(W, H, bytes(pixels))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
out_path = sys.argv[1] if len(sys.argv) > 1 else 'logo.png'
|
|
||||||
data = generate()
|
|
||||||
with open(out_path, 'wb') as f:
|
|
||||||
f.write(data)
|
|
||||||
print(f'Generated {out_path} ({len(data)} bytes)')
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 675 KiB After Width: | Height: | Size: 837 KiB |
@ -23,7 +23,7 @@ desktop-image: "background.png"
|
|||||||
left = 25%
|
left = 25%
|
||||||
top = 20%
|
top = 20%
|
||||||
width = 50%
|
width = 50%
|
||||||
text = "A R C H I P E L A G O"
|
text = "a r c h i p e l a g o"
|
||||||
color = "#f7931a"
|
color = "#f7931a"
|
||||||
align = "center"
|
align = "center"
|
||||||
}
|
}
|
||||||
@ -32,7 +32,7 @@ desktop-image: "background.png"
|
|||||||
left = 25%
|
left = 25%
|
||||||
top = 28%
|
top = 28%
|
||||||
width = 50%
|
width = 50%
|
||||||
text = "Bitcoin Node OS"
|
text = "bitcoin node os"
|
||||||
color = "#888888"
|
color = "#888888"
|
||||||
align = "center"
|
align = "center"
|
||||||
}
|
}
|
||||||
@ -41,7 +41,7 @@ desktop-image: "background.png"
|
|||||||
left = 25%
|
left = 25%
|
||||||
top = 90%
|
top = 90%
|
||||||
width = 50%
|
width = 50%
|
||||||
text = "Use arrow keys to select, Enter to boot"
|
text = "use arrow keys to select, enter to boot"
|
||||||
color = "#555555"
|
color = "#555555"
|
||||||
align = "center"
|
align = "center"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -324,6 +324,10 @@ RUN mkdir -p /etc/archipelago/ssl && \
|
|||||||
COPY archipelago.service /etc/systemd/system/archipelago.service
|
COPY archipelago.service /etc/systemd/system/archipelago.service
|
||||||
COPY archipelago-update.service /etc/systemd/system/archipelago-update.service
|
COPY archipelago-update.service /etc/systemd/system/archipelago-update.service
|
||||||
COPY archipelago-update.timer /etc/systemd/system/archipelago-update.timer
|
COPY archipelago-update.timer /etc/systemd/system/archipelago-update.timer
|
||||||
|
COPY archipelago-doctor.service /etc/systemd/system/archipelago-doctor.service
|
||||||
|
COPY archipelago-doctor.timer /etc/systemd/system/archipelago-doctor.timer
|
||||||
|
COPY archipelago-reconcile.service /etc/systemd/system/archipelago-reconcile.service
|
||||||
|
COPY archipelago-reconcile.timer /etc/systemd/system/archipelago-reconcile.timer
|
||||||
|
|
||||||
# Enable services
|
# Enable services
|
||||||
RUN systemctl enable NetworkManager || true && \
|
RUN systemctl enable NetworkManager || true && \
|
||||||
@ -333,7 +337,9 @@ RUN systemctl enable NetworkManager || true && \
|
|||||||
systemctl enable tor || true && \
|
systemctl enable tor || true && \
|
||||||
systemctl enable tailscaled || true && \
|
systemctl enable tailscaled || true && \
|
||||||
systemctl enable chrony || true && \
|
systemctl enable chrony || true && \
|
||||||
systemctl enable archipelago-update.timer || true
|
systemctl enable archipelago-update.timer || true && \
|
||||||
|
systemctl enable archipelago-doctor.timer || true && \
|
||||||
|
systemctl enable archipelago-reconcile.timer || true
|
||||||
|
|
||||||
# Remove policy-rc.d so services can start on first boot
|
# Remove policy-rc.d so services can start on first boot
|
||||||
RUN rm -f /usr/sbin/policy-rc.d
|
RUN rm -f /usr/sbin/policy-rc.d
|
||||||
@ -393,6 +399,15 @@ NGINXCONF
|
|||||||
echo " Using archipelago-update.service + timer from configs/"
|
echo " Using archipelago-update.service + timer from configs/"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Copy container doctor and reconciliation timers
|
||||||
|
if [ -f "$SCRIPT_DIR/configs/archipelago-doctor.service" ]; then
|
||||||
|
cp "$SCRIPT_DIR/configs/archipelago-doctor.service" "$WORK_DIR/archipelago-doctor.service"
|
||||||
|
cp "$SCRIPT_DIR/configs/archipelago-doctor.timer" "$WORK_DIR/archipelago-doctor.timer"
|
||||||
|
cp "$SCRIPT_DIR/configs/archipelago-reconcile.service" "$WORK_DIR/archipelago-reconcile.service"
|
||||||
|
cp "$SCRIPT_DIR/configs/archipelago-reconcile.timer" "$WORK_DIR/archipelago-reconcile.timer"
|
||||||
|
echo " Using container doctor + reconcile timers from configs/"
|
||||||
|
fi
|
||||||
|
|
||||||
# Use archipelago.service from configs/ (User=root for Podman container access)
|
# Use archipelago.service from configs/ (User=root for Podman container access)
|
||||||
if [ -f "$SCRIPT_DIR/configs/archipelago.service" ]; then
|
if [ -f "$SCRIPT_DIR/configs/archipelago.service" ]; then
|
||||||
cp "$SCRIPT_DIR/configs/archipelago.service" "$WORK_DIR/archipelago.service"
|
cp "$SCRIPT_DIR/configs/archipelago.service" "$WORK_DIR/archipelago.service"
|
||||||
@ -519,11 +534,23 @@ if [ -n "\$INSTALLER_STARTED" ]; then
|
|||||||
fi
|
fi
|
||||||
export INSTALLER_STARTED=1
|
export INSTALLER_STARTED=1
|
||||||
|
|
||||||
|
# Colors
|
||||||
|
O=\$'\\033[1;33m' # Bold yellow (orange accent)
|
||||||
|
W=\$'\\033[1;37m' # Bold white
|
||||||
|
D=\$'\\033[37m' # Dim
|
||||||
|
N=\$'\\033[0m' # Reset
|
||||||
|
|
||||||
|
# Center-print
|
||||||
|
TW=\$(tput cols 2>/dev/null || echo 60)
|
||||||
|
[ "\$TW" -gt 120 ] && TW=120
|
||||||
|
cc() { local s=\$(echo -e "\$1" | sed 's/\\x1b\\[[0-9;]*m//g'); local p=\$(( (TW - \${#s}) / 2 )); [ \$p -lt 0 ] && p=0; printf "%*s" "\$p" ""; echo -e "\$1"; }
|
||||||
|
|
||||||
sleep 1
|
sleep 1
|
||||||
clear
|
clear
|
||||||
echo ""
|
echo ""
|
||||||
echo " ARCHIPELAGO BITCOIN NODE OS"
|
cc "\${W}a r c h i p e l a g o\${N}"
|
||||||
echo " Automatic Installer"
|
cc "\${O}━━━━━━━━━━━━━━━━━━━━━\${N}"
|
||||||
|
cc "\${D}Automatic Installer\${N}"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
BOOT_MEDIA=""
|
BOOT_MEDIA=""
|
||||||
@ -535,17 +562,14 @@ for dev in /run/live/medium /lib/live/mount/medium /run/archiso /cdrom /media/cd
|
|||||||
done
|
done
|
||||||
|
|
||||||
if [ -n "\$BOOT_MEDIA" ]; then
|
if [ -n "\$BOOT_MEDIA" ]; then
|
||||||
echo " Found installer at: \$BOOT_MEDIA"
|
cc "\${D}Found installer at: \$BOOT_MEDIA\${N}"
|
||||||
echo ""
|
echo ""
|
||||||
echo " Press Enter to start installation, or Ctrl+C for shell..."
|
cc "Press Enter to install | \${W}Ctrl+C\${N} for shell"
|
||||||
read
|
read -s
|
||||||
bash "\$BOOT_MEDIA/archipelago/auto-install.sh"
|
bash "\$BOOT_MEDIA/archipelago/auto-install.sh"
|
||||||
else
|
else
|
||||||
echo " Installer not found on boot media."
|
cc "\${D}Installer not found on boot media.\${N}"
|
||||||
echo " Checked: /run/live/medium, /run/archiso, /cdrom, /media/cdrom"
|
cc "\${D}Try: sudo bash /path/to/archipelago/auto-install.sh\${N}"
|
||||||
echo ""
|
|
||||||
echo " You can try manually:"
|
|
||||||
echo " sudo bash /path/to/archipelago/auto-install.sh"
|
|
||||||
echo ""
|
echo ""
|
||||||
fi
|
fi
|
||||||
PROFILE
|
PROFILE
|
||||||
@ -1349,26 +1373,62 @@ case "$(uname -m)" in
|
|||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
# Colors (use $'...' syntax for reliable escape code interpretation)
|
# Colors (basic ANSI — works on bare-metal Linux console)
|
||||||
RED=$'\033[0;31m'
|
RED=$'\033[31m'
|
||||||
GREEN=$'\033[0;32m'
|
GREEN=$'\033[32m'
|
||||||
YELLOW=$'\033[1;33m'
|
YELLOW=$'\033[1;33m'
|
||||||
BLUE=$'\033[0;34m'
|
ORANGE=$'\033[1;33m'
|
||||||
|
DIM=$'\033[37m'
|
||||||
|
CYAN=$'\033[36m'
|
||||||
|
WHITE=$'\033[1;37m'
|
||||||
NC=$'\033[0m'
|
NC=$'\033[0m'
|
||||||
|
|
||||||
|
# Adaptive centering
|
||||||
|
TW=$(tput cols 2>/dev/null || echo 60)
|
||||||
|
[ "$TW" -gt 120 ] && TW=120
|
||||||
|
cc() { local s=$(echo -e "$1" | sed 's/\x1b\[[0-9;]*m//g'); local p=$(( (TW - ${#s}) / 2 )); [ $p -lt 0 ] && p=0; printf "%*s" "$p" ""; echo -e "$1"; }
|
||||||
|
hrule() { local len=$((TW > 50 ? 50 : TW - 4)); local hr=""; for i in $(seq 1 $len); do hr="${hr}─"; done; cc "${DIM}${hr}${NC}"; }
|
||||||
|
|
||||||
|
box() {
|
||||||
|
local bw=$((TW > 52 ? 52 : TW - 4))
|
||||||
|
local inner=$((bw - 2))
|
||||||
|
local top="╭"; local bot="╰"
|
||||||
|
for i in $(seq 1 $inner); do top="${top}─"; bot="${bot}─"; done
|
||||||
|
top="${top}╮"; bot="${bot}╯"
|
||||||
|
cc "${DIM}${top}${NC}"
|
||||||
|
}
|
||||||
|
boxend() {
|
||||||
|
local bw=$((TW > 52 ? 52 : TW - 4))
|
||||||
|
local inner=$((bw - 2))
|
||||||
|
local bot="╰"
|
||||||
|
for i in $(seq 1 $inner); do bot="${bot}─"; done
|
||||||
|
bot="${bot}╯"
|
||||||
|
cc "${DIM}${bot}${NC}"
|
||||||
|
}
|
||||||
|
boxline() {
|
||||||
|
local bw=$((TW > 52 ? 52 : TW - 4))
|
||||||
|
local inner=$((bw - 2))
|
||||||
|
local stripped=$(echo -e "$1" | sed 's/\x1b\[[0-9;]*m//g')
|
||||||
|
local pad=$((inner - ${#stripped}))
|
||||||
|
[ $pad -lt 0 ] && pad=0
|
||||||
|
local right=""
|
||||||
|
for i in $(seq 1 $pad); do right="${right} "; done
|
||||||
|
cc "${DIM}│${NC} $1${right}${DIM}│${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
clear
|
clear
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${BLUE}╔═══════════════════════════════════════════════════════════════════╗${NC}"
|
box
|
||||||
echo -e "${BLUE}║ ║${NC}"
|
boxline ""
|
||||||
echo -e "${BLUE}║ ${GREEN}🏝️ ARCHIPELAGO BITCOIN NODE OS${BLUE} ║${NC}"
|
boxline "${WHITE}A R C H I P E L A G O${NC}"
|
||||||
echo -e "${BLUE}║ ║${NC}"
|
boxline "${ORANGE}━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||||
echo -e "${BLUE}║ ${NC}Automatic Installation${BLUE} ║${NC}"
|
boxline "${DIM}Automatic Installation${NC}"
|
||||||
echo -e "${BLUE}║ ║${NC}"
|
boxline ""
|
||||||
echo -e "${BLUE}╚═══════════════════════════════════════════════════════════════════╝${NC}"
|
boxend
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Check required tools are present (should be bundled in ISO)
|
# Check required tools are present (should be bundled in ISO)
|
||||||
echo -e "${YELLOW}🔧 Checking installer tools...${NC}"
|
cc "${DIM}Checking installer tools...${NC}"
|
||||||
MISSING=""
|
MISSING=""
|
||||||
command -v parted >/dev/null 2>&1 || MISSING="parted $MISSING"
|
command -v parted >/dev/null 2>&1 || MISSING="parted $MISSING"
|
||||||
command -v mkfs.vfat >/dev/null 2>&1 || MISSING="mkfs.vfat $MISSING"
|
command -v mkfs.vfat >/dev/null 2>&1 || MISSING="mkfs.vfat $MISSING"
|
||||||
@ -1467,15 +1527,15 @@ if [ -z "$TARGET_DISK" ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${GREEN}✅ Target disk: $TARGET_DISK ($TARGET_SIZE)${NC}"
|
cc "${GREEN}target: ${WHITE}$TARGET_DISK ($TARGET_SIZE)${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${RED}⚠️ WARNING: ALL DATA ON $TARGET_DISK WILL BE ERASED${NC}"
|
cc "${RED}all data on $TARGET_DISK will be erased${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Press Enter to install Archipelago, or Ctrl+C to cancel..."
|
cc "${DIM}Press Enter to install | Ctrl+C to cancel${NC}"
|
||||||
read
|
read -s
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${YELLOW}🔧 Installing Archipelago...${NC}"
|
cc "${DIM}Installing Archipelago...${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Unmount any existing partitions
|
# Unmount any existing partitions
|
||||||
@ -2402,6 +2462,9 @@ date=$(date -u '+%Y-%m-%d %H:%M:%S UTC')
|
|||||||
type=unbundled
|
type=unbundled
|
||||||
BUILDINFO
|
BUILDINFO
|
||||||
|
|
||||||
|
# Save install log BEFORE unmounting target
|
||||||
|
cp "$INSTALL_LOG" /mnt/target/var/log/archipelago-install.log 2>/dev/null || true
|
||||||
|
|
||||||
# Cleanup
|
# Cleanup
|
||||||
sync
|
sync
|
||||||
umount /mnt/target/run 2>/dev/null || true
|
umount /mnt/target/run 2>/dev/null || true
|
||||||
@ -2415,63 +2478,34 @@ cryptsetup close archipelago-data 2>/dev/null || true
|
|||||||
umount /mnt/target 2>/dev/null || true
|
umount /mnt/target 2>/dev/null || true
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${GREEN} _${NC}"
|
|
||||||
echo -e "${GREEN} ,--.\\\`-. __${NC}"
|
|
||||||
echo -e "${GREEN} _,.\\\`. \\:/,\" \\\`-._${NC}"
|
|
||||||
echo -e "${GREEN} ,-*\" _,.-;-*\\\`-.+\"*._ )${NC}"
|
|
||||||
echo -e "${GREEN} ( ,.\"* ,-\" / \\\`. \\\\. \\\`.${NC}"
|
|
||||||
echo -e "${GREEN} ,\" ,;\" ,\"\\../\\ \\: \\${NC}"
|
|
||||||
echo -e "${GREEN} ( ,\"/ / \\\\.,' : )) /${NC}"
|
|
||||||
echo -e "${GREEN} \\ |/ / \\\\.,' / // ,'${NC}"
|
|
||||||
echo -e "${GREEN} \\_)\\ ,' \\\\.,' ( / )/${NC}"
|
|
||||||
echo -e "${GREEN} \\\` \\._,' \\\`\"${NC}"
|
|
||||||
echo -e "${GREEN} \\../${NC}"
|
|
||||||
echo -e "${GREEN} \\../${NC}"
|
|
||||||
echo -e "${GREEN} ~ ~\\../ ~~ ~~${NC}"
|
|
||||||
echo -e "${GREEN} ~~ ~~ \\../ ~~ ~ ~~${NC}"
|
|
||||||
echo -e "${GREEN} ~~ ~ ~~ __...---\\../-...__ ~~~ ~~${NC}"
|
|
||||||
echo -e "${GREEN} ~~~~ ~_,--' \\../ \\\`--.__ ~~ ~~${NC}"
|
|
||||||
echo -e "${GREEN} ~~~ __,--' \\\`\" \\\`--.__ ~~~${NC}"
|
|
||||||
echo -e "${GREEN}~~ ,--' \\\`--.${NC}"
|
|
||||||
echo -e "${GREEN} '------......______ ______......------\\\` ~~${NC}"
|
|
||||||
echo -e "${GREEN} ~~~ ~ ~~ ~ \\\`\\\`\\\`\\\`\\\`---\"\"\"\"\" ~~ ~ ~~${NC}"
|
|
||||||
echo -e "${GREEN} ~~~~ ~~ ~~~~ ~~~~~~ ~ ~~ ~~ ~~~ ~${NC}"
|
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${GREEN} █████╗ ██████╗ ██████╗██╗ ██╗██╗██████╗ ███████╗██╗ █████╗ ██████╗ ██████╗ ${NC}"
|
box
|
||||||
echo -e "${GREEN} ██╔══██╗██╔══██╗██╔════╝██║ ██║██║██╔══██╗██╔════╝██║ ██╔══██╗██╔════╝ ██╔═══██╗${NC}"
|
boxline ""
|
||||||
echo -e "${GREEN} ███████║██████╔╝██║ ███████║██║██████╔╝█████╗ ██║ ███████║██║ ███╗██║ ██║${NC}"
|
boxline "${WHITE}A R C H I P E L A G O${NC}"
|
||||||
echo -e "${GREEN} ██╔══██║██╔══██╗██║ ██╔══██║██║██╔═══╝ ██╔══╝ ██║ ██╔══██║██║ ██║██║ ██║${NC}"
|
boxline "${GREEN}━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||||
echo -e "${GREEN} ██║ ██║██║ ██║╚██████╗██║ ██║██║██║ ███████╗███████╗██║ ██║╚██████╔╝╚██████╔╝${NC}"
|
boxline "${GREEN}Installation Complete${NC}"
|
||||||
echo -e "${GREEN} ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝╚═╝╚═╝ ╚══════╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ${NC}"
|
boxline ""
|
||||||
|
boxend
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${GREEN} 🏝️ BITCOIN NODE OS 🏝️${NC}"
|
cc "${DIM}After reboot, open the Web UI from any device on your network.${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${GREEN}╔═══════════════════════════════════════════════════════════════════╗${NC}"
|
cc "${DIM}Web UI:${NC} ${WHITE}http://<this machine's IP>${NC}"
|
||||||
echo -e "${GREEN}║ ✅ INSTALLATION COMPLETE! ║${NC}"
|
cc "${DIM}SSH:${NC} ${DIM}ssh archipelago@<IP>${NC}"
|
||||||
echo -e "${GREEN}║ ║${NC}"
|
cc "${DIM}Password:${NC} ${DIM}archipelago${NC}"
|
||||||
echo -e "${GREEN}║ Remove the USB drive and press Enter to reboot. ║${NC}"
|
cc "${DIM}Web Login:${NC} ${DIM}password123${NC}"
|
||||||
echo -e "${GREEN}║ ║${NC}"
|
|
||||||
echo -e "${GREEN}║ After reboot: ║${NC}"
|
|
||||||
echo -e "${GREEN}║ • Web UI: http://<IP> ║${NC}"
|
|
||||||
echo -e "${GREEN}║ • SSH: ssh archipelago@<IP> ║${NC}"
|
|
||||||
echo -e "${GREEN}║ • SSH Password: archipelago ║${NC}"
|
|
||||||
echo -e "${GREEN}║ • Web Password: password123 ║${NC}"
|
|
||||||
echo -e "${GREEN}║ ║${NC}"
|
|
||||||
echo -e "${GREEN}║ Pre-loaded apps (ready to start via Web UI): ║${NC}"
|
|
||||||
echo -e "${GREEN}║ • Bitcoin Knots • LND • Home Assistant ║${NC}"
|
|
||||||
echo -e "${GREEN}║ • BTCPay Server • Mempool • Nostr Relays ║${NC}"
|
|
||||||
echo -e "${GREEN}║ ║${NC}"
|
|
||||||
echo -e "${GREEN}║ Validate: bash /opt/archipelago/scripts/run-e2e-tests.sh ║${NC}"
|
|
||||||
echo -e "${GREEN}║ ║${NC}"
|
|
||||||
echo -e "${GREEN}╚═══════════════════════════════════════════════════════════════════╝${NC}"
|
|
||||||
echo ""
|
echo ""
|
||||||
|
cc "${DIM}Pre-loaded apps (start via Web UI):${NC}"
|
||||||
|
cc "${DIM}Bitcoin Knots, LND, Home Assistant,${NC}"
|
||||||
|
cc "${DIM}BTCPay Server, Mempool, Nostr Relay${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${YELLOW} >>> REMOVE THE USB DRIVE NOW <<<${NC}"
|
hrule
|
||||||
echo ""
|
echo ""
|
||||||
# Save install log to target disk for post-install debugging
|
cc "${YELLOW}>>> REMOVE THE USB DRIVE NOW <<<${NC}"
|
||||||
cp "$INSTALL_LOG" /mnt/target/var/log/archipelago-install.log 2>/dev/null || true
|
echo ""
|
||||||
|
# Install log already saved before unmount (above)
|
||||||
|
|
||||||
read -p "Press Enter to reboot (make sure USB is removed)..."
|
cc "${DIM}Press Enter to reboot${NC}"
|
||||||
|
read -s
|
||||||
|
|
||||||
# Suppress all error output during cleanup and reboot
|
# Suppress all error output during cleanup and reboot
|
||||||
exec 2>/dev/null
|
exec 2>/dev/null
|
||||||
@ -2590,22 +2624,25 @@ UI vesamenu.c32
|
|||||||
PROMPT 0
|
PROMPT 0
|
||||||
TIMEOUT 0
|
TIMEOUT 0
|
||||||
|
|
||||||
MENU TITLE ARCHIPELAGO - Bitcoin Node OS
|
MENU TITLE Bitcoin Node OS
|
||||||
MENU BACKGROUND splash.png
|
MENU BACKGROUND splash.png
|
||||||
MENU RESOLUTION 1024 768
|
MENU RESOLUTION 1024 768
|
||||||
MENU VSHIFT 14
|
MENU VSHIFT 15
|
||||||
MENU HSHIFT 6
|
MENU HSHIFT 28
|
||||||
MENU WIDTH 40
|
MENU WIDTH 26
|
||||||
|
MENU MARGIN 2
|
||||||
|
MENU ROWS 5
|
||||||
|
MENU TABMSG Press TAB to edit | https://archipelago.sh
|
||||||
MENU COLOR screen 37;40 #00000000 #00000000 none
|
MENU COLOR screen 37;40 #00000000 #00000000 none
|
||||||
MENU COLOR border 30;40 #00000000 #00000000 none
|
MENU COLOR border 30;40 #00000000 #00000000 none
|
||||||
MENU COLOR title 1;37;40 #fffb923c #00000000 none
|
MENU COLOR title 1;37;40 #80888888 #00000000 none
|
||||||
MENU COLOR sel 7;37;40 #ffffffff #80333333 std
|
MENU COLOR sel 7;37;40 #ffffffff #c0181818 std
|
||||||
MENU COLOR unsel 37;40 #ff999999 #00000000 none
|
MENU COLOR unsel 37;40 #ffaaaaaa #00000000 none
|
||||||
MENU COLOR hotkey 1;37;40 #fffb923c #00000000 none
|
MENU COLOR hotkey 1;37;40 #fffb923c #00000000 none
|
||||||
MENU COLOR hotsel 1;37;40 #fffb923c #80333333 std
|
MENU COLOR hotsel 1;37;40 #fffb923c #c0181818 std
|
||||||
MENU COLOR timeout_msg 37;40 #ff666666 #00000000 none
|
MENU COLOR timeout_msg 37;40 #ff555555 #00000000 none
|
||||||
MENU COLOR timeout 1;37;40 #fffb923c #00000000 none
|
MENU COLOR timeout 1;37;40 #fffb923c #00000000 none
|
||||||
MENU COLOR tabmsg 37;40 #ff666666 #00000000 none
|
MENU COLOR tabmsg 37;40 #ff444444 #00000000 none
|
||||||
MENU COLOR cmdmark 37;40 #00000000 #00000000 none
|
MENU COLOR cmdmark 37;40 #00000000 #00000000 none
|
||||||
MENU COLOR cmdline 37;40 #00000000 #00000000 none
|
MENU COLOR cmdline 37;40 #00000000 #00000000 none
|
||||||
|
|
||||||
@ -2618,7 +2655,7 @@ LABEL install
|
|||||||
MENU DEFAULT
|
MENU DEFAULT
|
||||||
|
|
||||||
LABEL install-verbose
|
LABEL install-verbose
|
||||||
MENU LABEL Install Archipelago (verbose)
|
MENU LABEL Install (verbose output)
|
||||||
KERNEL /live/vmlinuz
|
KERNEL /live/vmlinuz
|
||||||
APPEND initrd=/live/initrd.img boot=live components
|
APPEND initrd=/live/initrd.img boot=live components
|
||||||
|
|
||||||
|
|||||||
12
image-recipe/configs/archipelago-doctor.service
Normal file
12
image-recipe/configs/archipelago-doctor.service
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Archipelago Container Doctor
|
||||||
|
After=archipelago.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
# Runs as root: needs to kill orphaned conmon processes, fix permissions
|
||||||
|
User=root
|
||||||
|
ExecStart=/home/archipelago/archy/scripts/container-doctor.sh --local
|
||||||
|
TimeoutStartSec=120
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
12
image-recipe/configs/archipelago-doctor.timer
Normal file
12
image-recipe/configs/archipelago-doctor.timer
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Archipelago container doctor (periodic)
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
# First run 5 minutes after boot, then every 30 minutes
|
||||||
|
OnBootSec=5min
|
||||||
|
OnUnitActiveSec=30min
|
||||||
|
# Jitter to avoid load spikes
|
||||||
|
RandomizedDelaySec=60
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
14
image-recipe/configs/archipelago-reconcile.service
Normal file
14
image-recipe/configs/archipelago-reconcile.service
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Archipelago Container Reconciliation
|
||||||
|
After=archipelago.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
User=archipelago
|
||||||
|
Environment="XDG_RUNTIME_DIR=/run/user/1000"
|
||||||
|
Environment="HOME=/home/archipelago"
|
||||||
|
Environment="PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
||||||
|
ExecStart=/home/archipelago/archy/scripts/reconcile-containers.sh
|
||||||
|
TimeoutStartSec=600
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
14
image-recipe/configs/archipelago-reconcile.timer
Normal file
14
image-recipe/configs/archipelago-reconcile.timer
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Archipelago container reconciliation (periodic)
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
# First run 10 minutes after boot, then every 6 hours
|
||||||
|
OnBootSec=10min
|
||||||
|
OnUnitActiveSec=6h
|
||||||
|
# Jitter to avoid load spikes
|
||||||
|
RandomizedDelaySec=300
|
||||||
|
# Run missed checks on boot
|
||||||
|
Persistent=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
@ -16,6 +16,8 @@ Restart=on-failure
|
|||||||
RestartSec=5
|
RestartSec=5
|
||||||
WatchdogSec=300
|
WatchdogSec=300
|
||||||
TimeoutStartSec=300
|
TimeoutStartSec=300
|
||||||
|
# Bitcoin Core needs up to 600s to flush UTXO set on shutdown
|
||||||
|
TimeoutStopSec=660
|
||||||
|
|
||||||
# Filesystem protection
|
# Filesystem protection
|
||||||
ProtectSystem=strict
|
ProtectSystem=strict
|
||||||
|
|||||||
@ -62,6 +62,11 @@ class RPCClient {
|
|||||||
// Use a single shared timeout to prevent redirect storms when
|
// Use a single shared timeout to prevent redirect storms when
|
||||||
// multiple parallel requests all get 401 at once
|
// multiple parallel requests all get 401 at once
|
||||||
if (response.status === 401 && method !== 'auth.login') {
|
if (response.status === 401 && method !== 'auth.login') {
|
||||||
|
// Clear stale auth immediately — stops App.vue watcher from
|
||||||
|
// firing more requests and prevents the router from
|
||||||
|
// optimistically navigating to /dashboard
|
||||||
|
try { localStorage.removeItem('neode-auth') } catch { /* noop */ }
|
||||||
|
|
||||||
const isOnboarding = window.location.pathname.startsWith('/onboarding')
|
const isOnboarding = window.location.pathname.startsWith('/onboarding')
|
||||||
console.warn(`[RPC] 401 on ${method} | path=${window.location.pathname} | onboarding=${isOnboarding} | redirecting=${RPCClient._sessionExpiredRedirecting}`)
|
console.warn(`[RPC] 401 on ${method} | path=${window.location.pathname} | onboarding=${isOnboarding} | redirecting=${RPCClient._sessionExpiredRedirecting}`)
|
||||||
if (!isOnboarding && !RPCClient._sessionExpiredRedirecting) {
|
if (!isOnboarding && !RPCClient._sessionExpiredRedirecting) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user