fix: container installs, Tor, kiosk, GRUB, LUKS display, error messages

Critical:
- fix: container installs fail with "statfs: no such file or directory"
  Root cause: NoNewPrivileges=yes in systemd blocks sudo inside backend.
  Fix: use std::fs::create_dir_all + podman unshare chown (no sudo needed)
- fix: Tor services.json never written — \$ARCHY_TOR_DIR escaping bug
- fix: kiosk white screen — increase health wait to 60s, add --disable-gpu

Improvements:
- feat: LUKS encryption badge in Server disk stats (backend detects dm-crypt)
- fix: GRUB theme text scaling on 4:3 monitors — explicit fonts, wider menu
- fix: suppress default Debian MOTD (custom profile.d welcome is enough)
- fix: install error messages now show "Failed to pull/start" instead of
  generic "Operation failed" (middleware.rs allowlist expanded)
- fix: container-tests CI — source cargo env before running tests
- docs: interactive container architecture diagram (HTML)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dorian 2026-03-30 16:35:06 +01:00
parent e65b039914
commit 031b3c34f4
9 changed files with 522 additions and 37 deletions

View File

@ -29,6 +29,7 @@ jobs:
- name: Run orchestration unit tests
working-directory: core
run: |
source $HOME/.cargo/env 2>/dev/null || true
echo "=== Container crate tests ==="
cargo test -p archipelago-container --no-fail-fast 2>&1
@ -38,7 +39,9 @@ jobs:
- name: Verify cargo check (full crate)
working-directory: core
run: cargo check --release 2>&1
run: |
source $HOME/.cargo/env 2>/dev/null || true
cargo check --release 2>&1
smoke-tests:
runs-on: ubuntu-latest

View File

@ -56,6 +56,10 @@ pub(super) fn sanitize_error_message(msg: &str) -> String {
"cannot",
"Password",
"Session",
"Failed to pull",
"Failed to start",
"Container",
"Image",
];
for prefix in &user_facing_prefixes {
if msg.starts_with(prefix) {

View File

@ -472,18 +472,31 @@ impl RpcHandler {
if let Some(host_path) = volume.split(':').next() {
if host_path.starts_with("/var/lib/archipelago/") {
debug!("Creating directory: {} (owner: {})", host_path, uid_str);
let create_dir = tokio::process::Command::new("sudo")
.args(["mkdir", "-p", host_path])
.output()
.await;
if let Err(e) = create_dir {
debug!("Failed to create directory {}: {}", host_path, e);
// Create directory directly (service has ReadWritePaths access).
// sudo is blocked by NoNewPrivileges=yes in the systemd service.
if let Err(e) = std::fs::create_dir_all(host_path) {
tracing::warn!("Failed to create directory {}: {}", host_path, e);
}
// Set ownership to the mapped UID for rootless podman
let _ = tokio::process::Command::new("sudo")
.args(["chown", "-R", &uid_str, host_path])
// Set ownership to the mapped UID for rootless podman.
// This needs elevated privileges — use podman unshare to run
// chown inside the user namespace where UIDs are mapped.
let chown_result = tokio::process::Command::new("podman")
.args(["unshare", "chown", "-R", &uid_str, host_path])
.output()
.await;
match chown_result {
Ok(out) if !out.status.success() => {
tracing::warn!(
"podman unshare chown failed for {}: {}",
host_path,
String::from_utf8_lossy(&out.stderr)
);
}
Err(e) => tracing::warn!("Failed to chown {}: {}", host_path, e),
_ => {}
}
}
}
}

View File

@ -98,7 +98,11 @@ impl RpcHandler {
/// system.disk-status — Disk usage with warning/critical thresholds.
pub(in crate::api::rpc) async fn handle_system_disk_status(&self) -> Result<serde_json::Value> {
let (used, total) = read_disk_usage().await.unwrap_or((0, 0));
// Prefer the encrypted data partition if it exists
let data_path = std::path::Path::new("/var/lib/archipelago");
let df_target = if data_path.exists() { "/var/lib/archipelago" } else { "/" };
let (used, total) = read_disk_usage_path(df_target).await.unwrap_or((0, 0));
let percent = if total > 0 {
(used as f64 / total as f64) * 100.0
} else {
@ -114,12 +118,19 @@ impl RpcHandler {
"ok"
};
// Detect LUKS encryption (device name varies by install)
let encrypted = std::path::Path::new("/dev/mapper/archipelago_crypt").exists()
|| std::path::Path::new("/dev/mapper/archipelago-data").exists()
|| std::path::Path::new("/dev/mapper/archipelago_data").exists();
Ok(serde_json::json!({
"used_bytes": used,
"total_bytes": total,
"free_bytes": total.saturating_sub(used),
"used_percent": percent_rounded,
"level": level,
"encrypted": encrypted,
"partition": df_target,
}))
}

View File

@ -157,8 +157,13 @@ pub(super) fn parse_meminfo_kb(val: &str) -> Result<u64> {
/// Read disk usage via `df` for the root filesystem.
/// Returns (used_bytes, total_bytes).
pub(super) async fn read_disk_usage() -> Result<(u64, u64)> {
read_disk_usage_path("/").await
}
/// Read disk usage via `df` for a given path.
pub(super) async fn read_disk_usage_path(path: &str) -> Result<(u64, u64)> {
let output = tokio::process::Command::new("df")
.args(["--block-size=1", "--output=used,size", "/"])
.args(["--block-size=1", "--output=used,size", path])
.output()
.await
.context("Failed to run df")?;

View File

@ -0,0 +1,428 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Archipelago Container Architecture</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'SF Pro', system-ui, sans-serif; background: #0a0e1a; color: #e0e0e0; min-height: 100vh; }
.header { padding: 24px 32px; background: linear-gradient(135deg, #0f1629 0%, #1a1040 100%); border-bottom: 1px solid rgba(99,102,241,0.2); }
.header h1 { font-size: 24px; font-weight: 600; color: #fff; }
.header p { font-size: 13px; color: #8b8fa3; margin-top: 4px; }
.controls { padding: 16px 32px; display: flex; gap: 8px; flex-wrap: wrap; background: #0d1120; border-bottom: 1px solid rgba(255,255,255,0.06); }
.filter-btn { padding: 6px 14px; border-radius: 6px; border: 1px solid rgba(255,255,255,0.1); background: transparent; color: #8b8fa3; cursor: pointer; font-size: 12px; transition: all 0.2s; }
.filter-btn:hover { border-color: rgba(99,102,241,0.4); color: #c4c8e0; }
.filter-btn.active { background: rgba(99,102,241,0.15); border-color: rgba(99,102,241,0.5); color: #a5b4fc; }
.grid { padding: 24px 32px; display: grid; gap: 16px; }
.tier-section { margin-bottom: 8px; }
.tier-label { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 1.5px; padding: 8px 0; display: flex; align-items: center; gap: 8px; }
.tier-label::after { content: ''; flex: 1; height: 1px; background: rgba(255,255,255,0.06); }
.tier-0 .tier-label { color: #f59e0b; }
.tier-1 .tier-label { color: #ef4444; }
.tier-2 .tier-label { color: #8b5cf6; }
.tier-3 .tier-label { color: #10b981; }
.cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 10px; }
.card { background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.06); border-radius: 10px; padding: 14px 16px; cursor: pointer; transition: all 0.2s; position: relative; overflow: hidden; }
.card:hover { background: rgba(255,255,255,0.06); border-color: rgba(255,255,255,0.12); transform: translateY(-1px); }
.card.expanded { background: rgba(255,255,255,0.05); border-color: rgba(99,102,241,0.3); }
.card.highlight { animation: pulse 1s ease-in-out; }
@keyframes pulse { 0%,100% { box-shadow: none; } 50% { box-shadow: 0 0 20px rgba(99,102,241,0.3); } }
.card-header { display: flex; justify-content: space-between; align-items: center; }
.card-name { font-size: 14px; font-weight: 600; color: #fff; }
.card-network { font-size: 10px; padding: 2px 6px; border-radius: 4px; font-weight: 500; }
.net-archy { background: rgba(239,68,68,0.15); color: #fca5a5; }
.net-bridge { background: rgba(16,185,129,0.15); color: #6ee7b7; }
.net-host { background: rgba(245,158,11,0.15); color: #fcd34d; }
.card-image { font-size: 11px; color: #6b7280; margin-top: 4px; font-family: 'SF Mono', monospace; }
.card-ports { font-size: 11px; color: #8b8fa3; margin-top: 6px; }
.card-ports span { display: inline-block; background: rgba(255,255,255,0.06); padding: 1px 6px; border-radius: 3px; margin: 1px 2px; font-family: 'SF Mono', monospace; }
.card-details { display: none; margin-top: 10px; padding-top: 10px; border-top: 1px solid rgba(255,255,255,0.06); font-size: 12px; line-height: 1.6; }
.card.expanded .card-details { display: block; }
.detail-row { display: flex; gap: 8px; }
.detail-label { color: #6b7280; min-width: 80px; }
.detail-value { color: #c4c8e0; }
.dep-tag { display: inline-block; background: rgba(139,92,246,0.15); color: #c4b5fd; padding: 1px 6px; border-radius: 3px; margin: 1px 2px; font-size: 11px; cursor: pointer; transition: all 0.15s; }
.dep-tag:hover { background: rgba(139,92,246,0.3); }
.tab-badge { display: inline-block; font-size: 10px; padding: 1px 5px; border-radius: 3px; margin-left: 6px; }
.tab-apps { background: rgba(99,102,241,0.15); color: #a5b4fc; }
.tab-services { background: rgba(245,158,11,0.15); color: #fcd34d; }
.legend { padding: 16px 32px; display: flex; gap: 20px; flex-wrap: wrap; font-size: 12px; color: #6b7280; border-top: 1px solid rgba(255,255,255,0.04); }
.legend-item { display: flex; align-items: center; gap: 6px; }
.legend-dot { width: 10px; height: 10px; border-radius: 3px; }
.dep-graph { padding: 24px 32px; }
.dep-graph h2 { font-size: 16px; font-weight: 600; margin-bottom: 16px; color: #fff; }
.dep-chain { font-family: 'SF Mono', Menlo, monospace; font-size: 13px; line-height: 1.8; color: #8b8fa3; background: rgba(255,255,255,0.02); border-radius: 10px; padding: 20px 24px; border: 1px solid rgba(255,255,255,0.04); white-space: pre; overflow-x: auto; }
.dep-chain .hl { color: #a5b4fc; font-weight: 600; cursor: pointer; }
.dep-chain .hl:hover { text-decoration: underline; }
.dep-chain .arrow { color: #4b5563; }
.dep-chain .comment { color: #4b5563; }
.stats { padding: 16px 32px; display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 10px; }
.stat { background: rgba(255,255,255,0.03); border-radius: 8px; padding: 12px 16px; text-align: center; border: 1px solid rgba(255,255,255,0.04); }
.stat-value { font-size: 24px; font-weight: 700; color: #fff; }
.stat-label { font-size: 11px; color: #6b7280; margin-top: 2px; }
.hidden { display: none !important; }
</style>
</head>
<body>
<div class="header">
<h1>Archipelago Container Architecture</h1>
<p>Interactive map of all containers, dependencies, and networks &middot; Click any container for details</p>
</div>
<div class="stats">
<div class="stat"><div class="stat-value">28</div><div class="stat-label">Total Containers</div></div>
<div class="stat"><div class="stat-value">4</div><div class="stat-label">Tier 0 &middot; DB</div></div>
<div class="stat"><div class="stat-value">2</div><div class="stat-label">Tier 1 &middot; Core</div></div>
<div class="stat"><div class="stat-value">8</div><div class="stat-label">Tier 2 &middot; Service</div></div>
<div class="stat"><div class="stat-value">14</div><div class="stat-label">Tier 3 &middot; App</div></div>
</div>
<div class="controls">
<button class="filter-btn active" data-filter="all">All</button>
<button class="filter-btn" data-filter="tier-0">Databases</button>
<button class="filter-btn" data-filter="tier-1">Core</button>
<button class="filter-btn" data-filter="tier-2">Services</button>
<button class="filter-btn" data-filter="tier-3">Apps</button>
<button class="filter-btn" data-filter="archy-net">archy-net</button>
<button class="filter-btn" data-filter="bridge">bridge</button>
</div>
<div class="grid">
<div class="tier-section tier-0" data-tier="tier-0">
<div class="tier-label">Tier 0 &mdash; Databases</div>
<div class="cards">
<div class="card" data-name="archy-mempool-db" data-tier="tier-0" data-net="archy-net" onclick="toggle(this)">
<div class="card-header">
<span class="card-name">archy-mempool-db</span>
<span class="card-network net-archy">archy-net</span>
</div>
<div class="card-image">mariadb:11.4.10</div>
<div class="card-ports">No ports exposed</div>
<div class="card-details">
<div class="detail-row"><span class="detail-label">UID</span><span class="detail-value">100999:100999</span></div>
<div class="detail-row"><span class="detail-label">Health</span><span class="detail-value">mariadb -uroot -e 'SELECT 1'</span></div>
<div class="detail-row"><span class="detail-label">UI Tab</span><span class="tab-badge tab-services">Services</span></div>
<div class="detail-row"><span class="detail-label">Data</span><span class="detail-value">/var/lib/archipelago/mysql-mempool</span></div>
<div class="detail-row"><span class="detail-label">Deps</span><span class="detail-value">None</span></div>
<div class="detail-row"><span class="detail-label">Needed by</span><span class="detail-value"><span class="dep-tag" onclick="find('mempool-api')">mempool-api</span></span></div>
</div>
</div>
<div class="card" data-name="archy-btcpay-db" data-tier="tier-0" data-net="archy-net" onclick="toggle(this)">
<div class="card-header">
<span class="card-name">archy-btcpay-db</span>
<span class="card-network net-archy">archy-net</span>
</div>
<div class="card-image">postgres:15.17</div>
<div class="card-ports">No ports exposed</div>
<div class="card-details">
<div class="detail-row"><span class="detail-label">UID</span><span class="detail-value">100070:100070</span></div>
<div class="detail-row"><span class="detail-label">Health</span><span class="detail-value">pg_isready -U postgres</span></div>
<div class="detail-row"><span class="detail-label">UI Tab</span><span class="tab-badge tab-services">Services</span></div>
<div class="detail-row"><span class="detail-label">Needed by</span><span class="detail-value"><span class="dep-tag" onclick="find('archy-nbxplorer')">nbxplorer</span> <span class="dep-tag" onclick="find('btcpay-server')">btcpay</span></span></div>
</div>
</div>
<div class="card" data-name="immich_postgres" data-tier="tier-0" data-net="bridge" onclick="toggle(this)" style="opacity:0.5">
<div class="card-header">
<span class="card-name">immich_postgres</span>
<span class="card-network net-bridge">bridge</span>
</div>
<div class="card-image">immich-postgres:14 (optional)</div>
<div class="card-details"><div class="detail-row"><span class="detail-label">Needed by</span><span class="detail-value"><span class="dep-tag" onclick="find('immich_server')">immich</span></span></div></div>
</div>
<div class="card" data-name="immich_redis" data-tier="tier-0" data-net="bridge" onclick="toggle(this)" style="opacity:0.5">
<div class="card-header">
<span class="card-name">immich_redis</span>
<span class="card-network net-bridge">bridge</span>
</div>
<div class="card-image">valkey:8.1.6 (optional)</div>
<div class="card-details"><div class="detail-row"><span class="detail-label">Needed by</span><span class="detail-value"><span class="dep-tag" onclick="find('immich_server')">immich</span></span></div></div>
</div>
</div>
</div>
<div class="tier-section tier-1" data-tier="tier-1">
<div class="tier-label">Tier 1 &mdash; Core Infrastructure</div>
<div class="cards">
<div class="card" data-name="bitcoin-knots" data-tier="tier-1" data-net="archy-net" onclick="toggle(this)">
<div class="card-header">
<span class="card-name">bitcoin-knots</span>
<span class="card-network net-archy">archy-net</span>
</div>
<div class="card-image">bitcoin-knots:latest</div>
<div class="card-ports">Ports: <span>8332</span> <span>8333</span></div>
<div class="card-details">
<div class="detail-row"><span class="detail-label">UID</span><span class="detail-value">100101:100101</span></div>
<div class="detail-row"><span class="detail-label">Health</span><span class="detail-value">bitcoin-cli getblockchaininfo</span></div>
<div class="detail-row"><span class="detail-label">UI Tab</span><span class="tab-badge tab-apps">Apps</span></div>
<div class="detail-row"><span class="detail-label">Memory</span><span class="detail-value">2g (1g low-mem)</span></div>
<div class="detail-row"><span class="detail-label">Disk</span><span class="detail-value">Prunes if &lt;1TB, txindex if &ge;1TB</span></div>
<div class="detail-row"><span class="detail-label">Deps</span><span class="detail-value">None &mdash; ROOT DEPENDENCY</span></div>
<div class="detail-row"><span class="detail-label">Needed by</span><span class="detail-value"><span class="dep-tag" onclick="find('electrumx')">electrumx</span> <span class="dep-tag" onclick="find('lnd')">lnd</span> <span class="dep-tag" onclick="find('mempool-api')">mempool</span> <span class="dep-tag" onclick="find('archy-nbxplorer')">nbxplorer</span> <span class="dep-tag" onclick="find('fedimint')">fedimint</span></span></div>
</div>
</div>
<div class="card" data-name="electrumx" data-tier="tier-1" data-net="archy-net" onclick="toggle(this)">
<div class="card-header">
<span class="card-name">electrumx</span>
<span class="card-network net-archy">archy-net</span>
</div>
<div class="card-image">electrumx:v1.18.0</div>
<div class="card-ports">Ports: <span>50001</span></div>
<div class="card-details">
<div class="detail-row"><span class="detail-label">Health</span><span class="detail-value">curl localhost:8000</span></div>
<div class="detail-row"><span class="detail-label">UI Tab</span><span class="tab-badge tab-apps">Apps</span></div>
<div class="detail-row"><span class="detail-label">Deps</span><span class="detail-value"><span class="dep-tag" onclick="find('bitcoin-knots')">bitcoin-knots</span></span></div>
<div class="detail-row"><span class="detail-label">Needed by</span><span class="detail-value"><span class="dep-tag" onclick="find('mempool-api')">mempool-api</span></span></div>
</div>
</div>
</div>
</div>
<div class="tier-section tier-2" data-tier="tier-2">
<div class="tier-label">Tier 2 &mdash; Services</div>
<div class="cards">
<div class="card" data-name="lnd" data-tier="tier-2" data-net="archy-net" onclick="toggle(this)">
<div class="card-header"><span class="card-name">lnd</span><span class="card-network net-archy">archy-net</span></div>
<div class="card-image">lnd:v0.18.4-beta</div>
<div class="card-ports">Ports: <span>9735</span> <span>10009</span> <span>8080</span></div>
<div class="card-details">
<div class="detail-row"><span class="detail-label">Deps</span><span class="detail-value"><span class="dep-tag" onclick="find('bitcoin-knots')">bitcoin-knots</span></span></div>
<div class="detail-row"><span class="detail-label">UI Tab</span><span class="tab-badge tab-apps">Apps</span></div>
<div class="detail-row"><span class="detail-label">Needed by</span><span class="detail-value"><span class="dep-tag" onclick="find('fedimint-gateway')">fedi-gateway (LND mode)</span></span></div>
</div>
</div>
<div class="card" data-name="mempool-api" data-tier="tier-2" data-net="archy-net" onclick="toggle(this)">
<div class="card-header"><span class="card-name">mempool-api</span><span class="card-network net-archy">archy-net</span></div>
<div class="card-image">mempool-backend:v3.0.0</div>
<div class="card-ports">Ports: <span>8999</span></div>
<div class="card-details">
<div class="detail-row"><span class="detail-label">Deps</span><span class="detail-value"><span class="dep-tag" onclick="find('bitcoin-knots')">bitcoin-knots</span> <span class="dep-tag" onclick="find('electrumx')">electrumx</span> <span class="dep-tag" onclick="find('archy-mempool-db')">mempool-db</span></span></div>
<div class="detail-row"><span class="detail-label">UI Tab</span><span class="tab-badge tab-services">Services</span></div>
<div class="detail-row"><span class="detail-label">Needed by</span><span class="detail-value"><span class="dep-tag" onclick="find('archy-mempool-web')">mempool-web</span></span></div>
</div>
</div>
<div class="card" data-name="archy-mempool-web" data-tier="tier-2" data-net="archy-net" onclick="toggle(this)">
<div class="card-header"><span class="card-name">archy-mempool-web</span><span class="card-network net-archy">archy-net</span></div>
<div class="card-image">mempool-frontend:v3.0.0</div>
<div class="card-ports">Ports: <span>4080</span></div>
<div class="card-details">
<div class="detail-row"><span class="detail-label">Deps</span><span class="detail-value"><span class="dep-tag" onclick="find('mempool-api')">mempool-api</span></span></div>
<div class="detail-row"><span class="detail-label">UI Tab</span><span class="tab-badge tab-apps">Apps</span></div>
</div>
</div>
<div class="card" data-name="archy-nbxplorer" data-tier="tier-2" data-net="archy-net" onclick="toggle(this)">
<div class="card-header"><span class="card-name">archy-nbxplorer</span><span class="card-network net-archy">archy-net</span></div>
<div class="card-image">nbxplorer:2.6.0</div>
<div class="card-ports">Ports: <span>32838</span></div>
<div class="card-details">
<div class="detail-row"><span class="detail-label">Deps</span><span class="detail-value"><span class="dep-tag" onclick="find('bitcoin-knots')">bitcoin-knots</span> <span class="dep-tag" onclick="find('archy-btcpay-db')">btcpay-db</span></span></div>
<div class="detail-row"><span class="detail-label">UI Tab</span><span class="tab-badge tab-services">Services</span></div>
<div class="detail-row"><span class="detail-label">Needed by</span><span class="detail-value"><span class="dep-tag" onclick="find('btcpay-server')">btcpay</span></span></div>
</div>
</div>
<div class="card" data-name="btcpay-server" data-tier="tier-2" data-net="archy-net" onclick="toggle(this)">
<div class="card-header"><span class="card-name">btcpay-server</span><span class="card-network net-archy">archy-net</span></div>
<div class="card-image">btcpayserver:1.13.7</div>
<div class="card-ports">Ports: <span>23000</span></div>
<div class="card-details">
<div class="detail-row"><span class="detail-label">Deps</span><span class="detail-value"><span class="dep-tag" onclick="find('archy-nbxplorer')">nbxplorer</span> <span class="dep-tag" onclick="find('archy-btcpay-db')">btcpay-db</span></span></div>
<div class="detail-row"><span class="detail-label">UI Tab</span><span class="tab-badge tab-apps">Apps</span></div>
</div>
</div>
<div class="card" data-name="fedimint" data-tier="tier-2" data-net="archy-net" onclick="toggle(this)">
<div class="card-header"><span class="card-name">fedimint</span><span class="card-network net-archy">archy-net</span></div>
<div class="card-image">fedimintd:v0.10.0</div>
<div class="card-ports">Ports: <span>8173</span> <span>8174</span> <span>8175</span></div>
<div class="card-details">
<div class="detail-row"><span class="detail-label">Deps</span><span class="detail-value"><span class="dep-tag" onclick="find('bitcoin-knots')">bitcoin-knots</span></span></div>
<div class="detail-row"><span class="detail-label">UI Tab</span><span class="tab-badge tab-apps">Apps</span></div>
<div class="detail-row"><span class="detail-label">Needed by</span><span class="detail-value"><span class="dep-tag" onclick="find('fedimint-gateway')">fedi-gateway</span></span></div>
</div>
</div>
<div class="card" data-name="fedimint-gateway" data-tier="tier-2" data-net="archy-net" onclick="toggle(this)">
<div class="card-header"><span class="card-name">fedimint-gateway</span><span class="card-network net-archy">archy-net</span></div>
<div class="card-image">gatewayd:v0.10.0</div>
<div class="card-ports">Ports: <span>8176</span></div>
<div class="card-details">
<div class="detail-row"><span class="detail-label">Deps</span><span class="detail-value"><span class="dep-tag" onclick="find('bitcoin-knots')">bitcoin-knots</span> <span class="dep-tag" onclick="find('fedimint')">fedimint</span></span></div>
<div class="detail-row"><span class="detail-label">UI Tab</span><span class="tab-badge tab-apps">Apps</span></div>
<div class="detail-row"><span class="detail-label">Note</span><span class="detail-value">Uses LDK (built-in Lightning) if no LND</span></div>
</div>
</div>
<div class="card" data-name="immich_server" data-tier="tier-2" data-net="bridge" onclick="toggle(this)" style="opacity:0.5">
<div class="card-header"><span class="card-name">immich_server</span><span class="card-network net-bridge">bridge</span></div>
<div class="card-image">immich-server:release (optional)</div>
<div class="card-ports">Ports: <span>2283</span></div>
<div class="card-details">
<div class="detail-row"><span class="detail-label">Deps</span><span class="detail-value"><span class="dep-tag" onclick="find('immich_postgres')">immich_postgres</span> <span class="dep-tag" onclick="find('immich_redis')">immich_redis</span></span></div>
</div>
</div>
</div>
</div>
<div class="tier-section tier-3" data-tier="tier-3">
<div class="tier-label">Tier 3 &mdash; Applications (independent, no dependencies)</div>
<div class="cards">
<div class="card" data-name="homeassistant" data-tier="tier-3" data-net="bridge" onclick="toggle(this)">
<div class="card-header"><span class="card-name">homeassistant</span><span class="card-network net-bridge">bridge</span></div>
<div class="card-image">home-assistant:2024.1</div>
<div class="card-ports">Ports: <span>8123</span></div>
</div>
<div class="card" data-name="grafana" data-tier="tier-3" data-net="bridge" onclick="toggle(this)">
<div class="card-header"><span class="card-name">grafana</span><span class="card-network net-bridge">bridge</span></div>
<div class="card-image">grafana:10.2.0</div>
<div class="card-ports">Ports: <span>3000</span></div>
<div class="card-details"><div class="detail-row"><span class="detail-label">UID</span><span class="detail-value">100472:100472</span></div></div>
</div>
<div class="card" data-name="jellyfin" data-tier="tier-3" data-net="bridge" onclick="toggle(this)">
<div class="card-header"><span class="card-name">jellyfin</span><span class="card-network net-bridge">bridge</span></div>
<div class="card-image">jellyfin:10.8.13</div>
<div class="card-ports">Ports: <span>8096</span></div>
</div>
<div class="card" data-name="photoprism" data-tier="tier-3" data-net="bridge" onclick="toggle(this)">
<div class="card-header"><span class="card-name">photoprism</span><span class="card-network net-bridge">bridge</span></div>
<div class="card-image">photoprism:240915</div>
<div class="card-ports">Ports: <span>2342</span></div>
</div>
<div class="card" data-name="vaultwarden" data-tier="tier-3" data-net="bridge" onclick="toggle(this)">
<div class="card-header"><span class="card-name">vaultwarden</span><span class="card-network net-bridge">bridge</span></div>
<div class="card-image">vaultwarden:1.30.0-alpine</div>
<div class="card-ports">Ports: <span>8082</span></div>
</div>
<div class="card" data-name="nextcloud" data-tier="tier-3" data-net="bridge" onclick="toggle(this)">
<div class="card-header"><span class="card-name">nextcloud</span><span class="card-network net-bridge">bridge</span></div>
<div class="card-image">nextcloud:28</div>
<div class="card-ports">Ports: <span>8085</span></div>
</div>
<div class="card" data-name="searxng" data-tier="tier-3" data-net="bridge" onclick="toggle(this)">
<div class="card-header"><span class="card-name">searxng</span><span class="card-network net-bridge">bridge</span></div>
<div class="card-image">searxng:latest</div>
<div class="card-ports">Ports: <span>8888</span></div>
</div>
<div class="card" data-name="uptime-kuma" data-tier="tier-3" data-net="bridge" onclick="toggle(this)">
<div class="card-header"><span class="card-name">uptime-kuma</span><span class="card-network net-bridge">bridge</span></div>
<div class="card-image">uptime-kuma:1</div>
<div class="card-ports">Ports: <span>3001</span></div>
</div>
<div class="card" data-name="filebrowser" data-tier="tier-3" data-net="bridge" onclick="toggle(this)">
<div class="card-header"><span class="card-name">filebrowser</span><span class="card-network net-bridge">bridge</span></div>
<div class="card-image">filebrowser:v2.27.0</div>
<div class="card-ports">Ports: <span>8083</span></div>
<div class="card-details"><div class="detail-row"><span class="detail-label">Note</span><span class="detail-value">Only container created by first-boot (unbundled ISO)</span></div></div>
</div>
<div class="card" data-name="onlyoffice" data-tier="tier-3" data-net="bridge" onclick="toggle(this)">
<div class="card-header"><span class="card-name">onlyoffice</span><span class="card-network net-bridge">bridge</span></div>
<div class="card-image">onlyoffice:latest</div>
<div class="card-ports">Ports: <span>9980</span></div>
</div>
<div class="card" data-name="ollama" data-tier="tier-3" data-net="bridge" onclick="toggle(this)" style="opacity:0.5">
<div class="card-header"><span class="card-name">ollama</span><span class="card-network net-bridge">bridge</span></div>
<div class="card-image">ollama:latest (optional)</div>
<div class="card-ports">Ports: <span>11434</span></div>
</div>
<div class="card" data-name="nginx-proxy-manager" data-tier="tier-3" data-net="bridge" onclick="toggle(this)">
<div class="card-header"><span class="card-name">nginx-proxy-manager</span><span class="card-network net-bridge">bridge</span></div>
<div class="card-image">nginx-proxy-manager:latest</div>
<div class="card-ports">Ports: <span>81</span> <span>8084</span> <span>8443</span></div>
</div>
<div class="card" data-name="portainer" data-tier="tier-3" data-net="bridge" onclick="toggle(this)">
<div class="card-header"><span class="card-name">portainer</span><span class="card-network net-bridge">bridge</span></div>
<div class="card-image">portainer:latest</div>
<div class="card-ports">Ports: <span>9000</span></div>
</div>
</div>
</div>
</div>
<div class="dep-graph">
<h2>Dependency Chain</h2>
<div class="dep-chain"><span class="comment">// Startup order: Tier 0 → 1 → 2 → 3</span>
<span class="comment">// Health monitor restarts in this order too</span>
<span class="hl" onclick="find('archy-mempool-db')">mempool-db</span> <span class="arrow">───┐</span>
<span class="hl" onclick="find('archy-btcpay-db')">btcpay-db</span> <span class="arrow">───┤</span>
<span class="arrow"></span>
<span class="arrow">├──→</span> <span class="hl" onclick="find('bitcoin-knots')">bitcoin-knots</span> <span class="arrow">──→</span> <span class="hl" onclick="find('electrumx')">electrumx</span>
<span class="arrow"></span> <span class="arrow"></span>
<span class="arrow"></span> <span class="arrow">┌────┴────┬──────────┬──────────┐</span>
<span class="arrow"></span> <span class="arrow"></span> <span class="arrow"></span> <span class="arrow"></span> <span class="arrow"></span>
<span class="arrow"></span> <span class="hl" onclick="find('lnd')">lnd</span> <span class="hl" onclick="find('fedimint')">fedimint</span> <span class="hl" onclick="find('mempool-api')">mempool-api</span> <span class="hl" onclick="find('archy-nbxplorer')">nbxplorer</span>
<span class="arrow"></span> <span class="arrow"></span> <span class="arrow"></span> <span class="arrow"></span>
<span class="arrow"></span> <span class="hl" onclick="find('fedimint-gateway')">fedi-gw</span> <span class="hl" onclick="find('archy-mempool-web')">mempool-web</span> <span class="hl" onclick="find('btcpay-server')">btcpay</span>
<span class="comment">// Tier 3: All independent — start in any order</span>
<span class="hl" onclick="find('filebrowser')">filebrowser</span> <span class="hl" onclick="find('grafana')">grafana</span> <span class="hl" onclick="find('homeassistant')">homeassist</span> <span class="hl" onclick="find('jellyfin')">jellyfin</span> <span class="hl" onclick="find('photoprism')">photoprism</span>
<span class="hl" onclick="find('vaultwarden')">vaultwarden</span> <span class="hl" onclick="find('nextcloud')">nextcloud</span> <span class="hl" onclick="find('searxng')">searxng</span> <span class="hl" onclick="find('uptime-kuma')">uptime-kuma</span> <span class="hl" onclick="find('ollama')">ollama</span></div>
</div>
<div class="legend">
<div class="legend-item"><div class="legend-dot" style="background:rgba(239,68,68,0.4)"></div> archy-net (Bitcoin stack, inter-container DNS)</div>
<div class="legend-item"><div class="legend-dot" style="background:rgba(16,185,129,0.4)"></div> bridge (standalone apps, port-mapped)</div>
<div class="legend-item"><div class="legend-dot" style="background:rgba(99,102,241,0.4)"></div> Apps tab (user-facing)</div>
<div class="legend-item"><div class="legend-dot" style="background:rgba(245,158,11,0.4)"></div> Services tab (infrastructure)</div>
</div>
<script>
function toggle(el) {
el.classList.toggle('expanded')
}
function find(name) {
event.stopPropagation()
// Reset all filters
document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'))
document.querySelector('[data-filter="all"]').classList.add('active')
document.querySelectorAll('.card').forEach(c => c.classList.remove('hidden'))
document.querySelectorAll('.tier-section').forEach(t => t.classList.remove('hidden'))
const card = document.querySelector(`[data-name="${name}"]`)
if (card) {
card.scrollIntoView({ behavior: 'smooth', block: 'center' })
card.classList.add('expanded', 'highlight')
setTimeout(() => card.classList.remove('highlight'), 1200)
}
}
document.querySelectorAll('.filter-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'))
btn.classList.add('active')
const filter = btn.dataset.filter
document.querySelectorAll('.card').forEach(card => {
if (filter === 'all') { card.classList.remove('hidden'); return }
const match = card.dataset.tier === filter || card.dataset.net === filter
card.classList.toggle('hidden', !match)
})
document.querySelectorAll('.tier-section').forEach(section => {
if (filter === 'all' || filter === 'archy-net' || filter === 'bridge') {
section.classList.remove('hidden')
return
}
section.classList.toggle('hidden', section.dataset.tier !== filter)
})
})
})
</script>
</body>
</html>

View File

@ -1,6 +1,5 @@
# Archipelago GRUB Theme
# Dark background with Bitcoin orange accents
# Font references removed — GRUB uses whatever fonts are loaded in grub.cfg
title-text: ""
desktop-color: "#0a0a0a"
@ -8,41 +7,46 @@ desktop-image: "background.png"
desktop-image-scale-method: "stretch"
+ boot_menu {
left = 15%
top = 40%
width = 70%
height = 35%
left = 10%
top = 35%
width = 80%
height = 40%
item_font = "DejaVu Sans Bold 16"
item_color = "#aaaaaa"
selected_item_font = "DejaVu Sans Bold 16"
selected_item_color = "#f7931a"
item_height = 40
item_spacing = 10
item_padding = 20
item_height = 36
item_spacing = 8
item_padding = 16
scrollbar = false
}
+ label {
left = 25%
top = 20%
width = 50%
left = 10%
top = 18%
width = 80%
font = "DejaVu Sans Mono Bold 24"
text = "a r c h i p e l a g o"
color = "#f7931a"
align = "center"
}
+ label {
left = 25%
top = 28%
width = 50%
left = 10%
top = 26%
width = 80%
font = "DejaVu Sans Bold 14"
text = "bitcoin node os"
color = "#888888"
align = "center"
}
+ label {
left = 25%
left = 10%
top = 90%
width = 50%
text = "use arrow keys to select, enter to boot"
width = 80%
font = "DejaVu Sans Bold 12"
text = "press tab to edit | use arrow keys to select | enter to boot"
color = "#555555"
align = "center"
}

View File

@ -1158,7 +1158,7 @@ LOG="/var/log/archipelago-tor.log"
mkdir -p "$ARCHY_TOR_DIR"
# Write services.json for the backend to read
cat > "\$ARCHY_TOR_DIR/services.json" <<TORJSON
cat > "$ARCHY_TOR_DIR/services.json" <<TORJSON
{
"services": [
{"name": "archipelago", "local_port": 80, "enabled": true},
@ -1947,7 +1947,7 @@ if [ -t 0 ] && [ -z "$ARCHIPELAGO_WELCOMED" ]; then
else
echo -e " ${OD}Waiting for network...${N}"
fi
if [ -b /dev/mapper/archipelago-data ]; then
if [ -b /dev/mapper/archipelago-data ] || [ -b /dev/mapper/archipelago_crypt ]; then
echo -e " ${OD}storage LUKS2 encrypted${N}"
fi
if systemctl is-active archipelago-kiosk.service >/dev/null 2>&1; then
@ -1960,6 +1960,10 @@ fi
PROFILE
chmod +x /mnt/target/etc/profile.d/archipelago.sh
# Suppress default Debian MOTD (our profile.d script handles the welcome)
echo -n > /mnt/target/etc/motd
rm -f /mnt/target/etc/motd.d/* 2>/dev/null || true
# Ensure reboot/shutdown work without sudo for the archipelago user
# profile.d only runs for login shells; .bashrc handles SSH interactive sessions
if ! grep -q '/sbin' /mnt/target/home/archipelago/.bashrc 2>/dev/null; then
@ -2088,14 +2092,14 @@ while true; do
--disable-translate \
--no-first-run \
--check-for-update-interval=31536000 \
--disable-features=TranslateUI \
--disable-features=TranslateUI,PasswordManagerOnboarding,AutofillServerCommunication,PasswordManagerEnabled \
--disable-session-crashed-bubble \
--disable-save-password-bubble \
--disable-suggestions-service \
--password-store=basic \
--disable-features=TranslateUI,PasswordManagerOnboarding,AutofillServerCommunication,PasswordManagerEnabled \
--disable-component-update \
--credentials_enable_service=false \
--disable-gpu \
--user-data-dir=/home/archipelago/.config/chromium-kiosk
sleep 3
done
@ -2113,9 +2117,9 @@ ConditionPathExists=/usr/local/bin/archipelago-kiosk-launcher
[Service]
Type=simple
ExecStartPre=/bin/bash -c 'for i in $(seq 1 15); do curl -sf http://localhost/health >/dev/null 2>&1 && exit 0; sleep 2; done; exit 0'
ExecStartPre=/bin/bash -c 'for i in $(seq 1 30); do curl -sf http://localhost/health >/dev/null 2>&1 && exit 0; sleep 2; done; exit 0'
ExecStart=/usr/local/bin/archipelago-kiosk-launcher
TimeoutStartSec=60
TimeoutStartSec=90
Restart=always
RestartSec=5
@ -2692,7 +2696,7 @@ set default=0
# Load font for graphical menu
if loadfont ($root)/boot/grub/font.pf2; then
set gfxmode=1024x768,auto
set gfxmode=auto
insmod gfxterm
insmod png
terminal_output gfxterm

View File

@ -1,6 +1,12 @@
<template>
<div class="pb-6">
<!-- LUKS Encryption Badge -->
<div v-if="diskEncrypted" class="mb-4 px-4 py-2.5 rounded-xl border bg-green-500/5 border-green-500/20 flex items-center gap-2.5">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-green-400 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" /></svg>
<span class="text-xs text-green-300/80 font-medium">LUKS2 Encrypted Storage</span>
</div>
<!-- Disk Space Warning Banner -->
<div
v-if="diskWarning"
@ -391,10 +397,17 @@ async function connectToWifi(password: string) {
// Disk space
const diskWarning = ref<{ level: 'warning' | 'critical'; used_percent: number; free_bytes: number } | null>(null)
const diskEncrypted = ref(false)
const diskCleaning = ref(false)
async function loadDiskStatus() {
try { const res = await rpcClient.diskStatus(); if (res.level === 'warning' || res.level === 'critical') { diskWarning.value = { level: res.level, used_percent: res.used_percent, free_bytes: res.free_bytes } } else { diskWarning.value = null } } catch { /* non-critical */ }
try {
const res = await rpcClient.diskStatus()
diskEncrypted.value = !!(res as Record<string, unknown>).encrypted
if (res.level === 'warning' || res.level === 'critical') {
diskWarning.value = { level: res.level, used_percent: res.used_percent, free_bytes: res.free_bytes }
} else { diskWarning.value = null }
} catch { /* non-critical */ }
}
async function runDiskCleanup() {