feat: v1.2.0-alpha — E2E encrypted mesh relay, steganography, relay status polling

Phase 5 mesh networking:
- E2E encrypted TX relay (X25519 + ChaCha20-Poly1305) — non-Archy nodes
  relay encrypted blobs transparently via Meshcore native routing
- Steganographic encoding modes (WeatherStation, SensorNetwork) — traffic
  looks like sensor data on the wire, 0xAA marker, configurable per-node
- Pre-flight Bitcoin Core health check on relay node — specific error codes
  (bitcoin_unreachable, bitcoin_syncing, tx_rejected) instead of generic fails
- mesh.relay-status RPC endpoint — frontend polls for relay result every 3s
- On-Chain / Lightning tabs in Off-Grid Bitcoin panel
- Archy Peers vs Mesh Broadcast relay mode selector
- Mesh view fills viewport (no page scroll), internal panel scrolling
- Version bump to 1.2.0-alpha

Also includes: deploy hardening, container fixes, IndeedHub updates,
boot screen, dashboard improvements, MASTER_PLAN task tracking

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-17 23:56:37 +00:00
parent d1ac098edb
commit f273816405
48 changed files with 3432 additions and 438 deletions

View File

@@ -96,8 +96,52 @@ section_end() {
echo " (${elapsed}s)"
}
# ── Progress bar ──────────────────────────────────────────────
CURRENT_STEP=0
BAR_WIDTH=30
calculate_total_steps() {
local total=4 # SSH, prereqs, health, git state
if [[ "$QUICK" == "true" ]]; then
total=$((total + 1)) # sync only
echo $total; return
fi
total=$((total + 1)) # sync code
total=$((total + 1)) # frontend build
if [[ "$FRONTEND_ONLY" != "true" ]]; then
total=$((total + 1)) # backend build
fi
if [[ "$LIVE" == "true" ]]; then
total=$((total + 14)) # rollback, frontend, AIUI, nginx, systemd, claude proxy, dev mode, data dirs, nostr-provider, filebrowser, manifest, restart, HTTPS, health check
if [[ "$FRONTEND_ONLY" != "true" ]]; then
total=$((total + 1)) # deploy backend binary
total=$((total + 16)) # container rebuilds
fi
total=$((total + 3)) # UFW, IndeedHub fix, container doctor
fi
echo $total
}
TOTAL_STEPS=$(calculate_total_steps)
progress() {
CURRENT_STEP=$((CURRENT_STEP + 1))
local pct=$((CURRENT_STEP * 100 / TOTAL_STEPS))
local filled=$((pct * BAR_WIDTH / 100))
local empty=$((BAR_WIDTH - filled))
local bar
bar=$(printf '%*s' "$filled" '' | tr ' ' '█')$(printf '%*s' "$empty" '' | tr ' ' '░')
printf "\033[1;36m━━━ [%s] %3d%% (%d/%d)\033[0m %s\n" "$bar" "$pct" "$CURRENT_STEP" "$TOTAL_STEPS" "$1"
}
# ─────────────────────────────────────────────────────────────
# SSH connectivity pre-flight check
echo "$(timestamp) Checking SSH connectivity..."
progress "Checking SSH connectivity"
if ! ssh $SSH_OPTS -o ConnectTimeout=5 "$TARGET_HOST" "echo ok" >/dev/null 2>&1; then
echo " ERROR: Cannot connect to $TARGET_HOST"
echo " Check that the server is on and reachable."
@@ -106,7 +150,7 @@ fi
echo " Connected."
# Install prerequisites if missing (rsync for code sync, python3 for Claude API proxy)
echo "$(timestamp) Checking prerequisites..."
progress "Checking prerequisites"
ssh $SSH_OPTS "$TARGET_HOST" '
NEED_INSTALL=""
command -v rsync >/dev/null 2>&1 || NEED_INSTALL="$NEED_INSTALL rsync"
@@ -125,6 +169,7 @@ ssh $SSH_OPTS "$TARGET_HOST" '
' 2>&1
# Pre-deploy health check (informational — warns but does not block)
progress "Pre-deploy health check"
TARGET_IP_ONLY="$(echo "$TARGET_HOST" | cut -d@ -f2)"
PRE_HEALTH=$(curl -s -o /dev/null -w '%{http_code}' --connect-timeout 5 "http://$TARGET_IP_ONLY/health" 2>/dev/null || echo "000")
if [ "$PRE_HEALTH" = "200" ]; then
@@ -134,6 +179,30 @@ else
fi
echo ""
# Git state check — detect uncommitted changes and record deploy version
progress "Checking git state"
DEPLOY_COMMIT=$(git -C "$PROJECT_DIR" rev-parse --short HEAD 2>/dev/null || echo "unknown")
DEPLOY_COMMIT_FULL=$(git -C "$PROJECT_DIR" rev-parse HEAD 2>/dev/null || echo "unknown")
DEPLOY_BRANCH=$(git -C "$PROJECT_DIR" rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown")
DIRTY_FILES=$(git -C "$PROJECT_DIR" status --porcelain 2>/dev/null | grep -v '^??' | grep -v '\.claude/memory/' || true)
DEPLOY_DIRTY=false
echo "$(timestamp) Git state: $DEPLOY_BRANCH @ $DEPLOY_COMMIT"
if [ -n "$DIRTY_FILES" ]; then
DEPLOY_DIRTY=true
DIRTY_COUNT=$(echo "$DIRTY_FILES" | wc -l | tr -d ' ')
echo " ⚠️ WARNING: $DIRTY_COUNT uncommitted change(s) — deploying working directory, NOT last commit"
echo "$DIRTY_FILES" | head -10 | sed 's/^/ /'
[ "$DIRTY_COUNT" -gt 10 ] && echo " ... and $((DIRTY_COUNT - 10)) more"
echo ""
echo " To deploy clean: commit or stash changes first"
echo " Continuing in 3 seconds... (Ctrl+C to abort)"
sleep 3
else
echo " Working tree clean — deploying commit $DEPLOY_COMMIT"
fi
echo ""
# When --canary: deploy to 198 first, verify health, then deploy to 228
if [ "$CANARY" = true ]; then
echo "🐤 Canary deploy: .198 first, then .228 if healthy..."
@@ -264,8 +333,26 @@ if [ "$BOTH" = true ]; then
fi
' 2>/dev/null || true
# Write deploy manifest to .198
DEPLOY_TS=$(date -u +%Y-%m-%dT%H:%M:%SZ)
ssh $SSH_OPTS "$TARGET_198" "sudo tee /opt/archipelago/deploy-manifest.json > /dev/null" << MANIFEST_198_EOF
{
"commit": "$DEPLOY_COMMIT_FULL",
"commit_short": "$DEPLOY_COMMIT",
"branch": "$DEPLOY_BRANCH",
"dirty": $DEPLOY_DIRTY,
"deployed_at": "$DEPLOY_TS",
"deployed_from": "$(hostname)",
"target": "$TARGET_198"
}
MANIFEST_198_EOF
ssh $SSH_OPTS "$TARGET_198" "sudo systemctl start archipelago && sudo systemctl restart nginx"
# Run container doctor on .198
echo " Running container doctor on .198..."
"$SCRIPT_DIR/container-doctor.sh" "$TARGET_198" 2>&1 | sed 's/^/ /' || true
# Post-deploy health check on .198
echo " Checking .198 health..."
HEALTH_198="fail"
@@ -286,7 +373,7 @@ fi
# Sync code
section_start
echo "$(timestamp) 📦 Syncing code..."
progress "Syncing code"
rsync -avz --delete \
-e "ssh $SSH_OPTS" \
--exclude 'node_modules' \
@@ -306,20 +393,17 @@ fi
# Build on target
echo ""
echo "$(timestamp) 🔨 Building on target..."
# Frontend
progress "Building frontend"
section_start
echo "$(timestamp) Building frontend (vue-tsc + vite)..."
ssh $SSH_OPTS "$TARGET_HOST" "cd $TARGET_DIR/neode-ui && npm install --silent && npm run build" 2>&1 | sed 's/^/ /'
section_end
# Backend (if Rust is installed) — skip with --frontend-only
if [ "$FRONTEND_ONLY" = true ]; then
echo "$(timestamp) Skipping backend build (--frontend-only)"
echo " Skipping backend build (--frontend-only)"
elif ssh $SSH_OPTS "$TARGET_HOST" "source ~/.cargo/env 2>/dev/null && command -v cargo" >/dev/null 2>&1; then
progress "Building backend (Rust release)"
section_start
echo "$(timestamp) Building backend (Rust release — this takes 1-2 min)..."
ssh $SSH_OPTS "$TARGET_HOST" "source ~/.cargo/env && cd $TARGET_DIR/core && cargo build --release 2>&1" | sed 's/^/ /'
section_end
else
@@ -327,11 +411,9 @@ else
fi
if [ "$LIVE" = true ]; then
echo ""
echo "$(timestamp) 🚀 Deploying to live system..."
# Create rollback backup before deploying
echo "$(timestamp) Creating rollback backup..."
progress "Creating rollback backup"
ssh $SSH_OPTS "$TARGET_HOST" '
sudo mkdir -p /opt/archipelago/rollback
[ -f /usr/local/bin/archipelago ] && sudo cp /usr/local/bin/archipelago /opt/archipelago/rollback/archipelago.bak 2>/dev/null || true
@@ -340,20 +422,21 @@ if [ "$LIVE" = true ]; then
# Deploy backend (check if binary exists) — skip with --frontend-only
if [ "$FRONTEND_ONLY" = true ]; then
echo "$(timestamp) Skipping backend deploy (--frontend-only)"
echo " Skipping backend deploy (--frontend-only)"
elif ssh $SSH_OPTS "$TARGET_HOST" "[ -f $TARGET_DIR/core/target/release/archipelago ]" 2>/dev/null; then
echo "$(timestamp) Deploying backend binary..."
progress "Deploying backend binary"
ssh $SSH_OPTS "$TARGET_HOST" "sudo systemctl stop archipelago"
ssh $SSH_OPTS "$TARGET_HOST" "sudo cp $TARGET_DIR/core/target/release/archipelago /usr/local/bin/"
fi
# Deploy frontend (preserve aiui/ and claude-login.html — they are NOT part of the neode-ui build)
echo "$(timestamp) Deploying frontend..."
progress "Deploying frontend"
ssh $SSH_OPTS "$TARGET_HOST" "sudo find /opt/archipelago/web-ui -mindepth 1 -maxdepth 1 ! -name 'aiui' ! -name 'claude-login.html' -exec rm -rf {} +"
ssh $SSH_OPTS "$TARGET_HOST" "sudo cp -rf $TARGET_DIR/web/dist/neode-ui/* /opt/archipelago/web-ui/"
ssh $SSH_OPTS "$TARGET_HOST" "sudo chown -R 1000:1000 /opt/archipelago/web-ui"
# Build and deploy AIUI (non-fatal — never delete existing AIUI on failure)
progress "Building & deploying AIUI"
AIUI_DIR="$PROJECT_DIR/../AIUI"
AIUI_DIST="$AIUI_DIR/packages/app/dist"
# Auto-build AIUI if dist is missing or older than source
@@ -378,10 +461,10 @@ if [ "$LIVE" = true ]; then
fi
# Sync nginx config from image-recipe (single source of truth)
progress "Syncing nginx configuration"
NGINX_CFG="$PROJECT_DIR/image-recipe/configs/nginx-archipelago.conf"
SNIPPETS_DIR="$PROJECT_DIR/image-recipe/configs/snippets"
if [ -f "$NGINX_CFG" ]; then
echo "$(timestamp) Syncing nginx config..."
scp $SSH_OPTS "$NGINX_CFG" "$TARGET_HOST:/tmp/nginx-archipelago.conf" 2>/dev/null || true
ssh $SSH_OPTS "$TARGET_HOST" '
sudo cp /tmp/nginx-archipelago.conf /etc/nginx/sites-available/archipelago
@@ -391,7 +474,6 @@ if [ "$LIVE" = true ]; then
# Sync nginx snippet files (HTTPS app proxies, PWA headers — included by main config)
if [ -d "$SNIPPETS_DIR" ]; then
echo "$(timestamp) Syncing nginx snippets..."
ssh $SSH_OPTS "$TARGET_HOST" "sudo mkdir -p /etc/nginx/snippets" 2>/dev/null || true
for f in "$SNIPPETS_DIR"/*.conf; do
[ -f "$f" ] && scp $SSH_OPTS "$f" "$TARGET_HOST:/tmp/nginx-snippet-$(basename $f)" 2>/dev/null || true
@@ -413,9 +495,9 @@ if [ "$LIVE" = true ]; then
ssh $SSH_OPTS "$TARGET_HOST" 'sudo nginx -t 2>&1 && echo " nginx config OK" || echo " ⚠️ nginx config test failed"' 2>/dev/null || true
# Sync systemd service file (single source of truth: image-recipe/configs/)
progress "Syncing systemd service"
SERVICE_FILE="$PROJECT_DIR/image-recipe/configs/archipelago.service"
if [ -f "$SERVICE_FILE" ]; then
echo "$(timestamp) Syncing systemd service file..."
scp $SSH_OPTS "$SERVICE_FILE" "$TARGET_HOST:/tmp/archipelago.service" 2>/dev/null || true
ssh $SSH_OPTS "$TARGET_HOST" '
if ! diff -q /tmp/archipelago.service /etc/systemd/system/archipelago.service >/dev/null 2>&1; then
@@ -430,7 +512,7 @@ if [ "$LIVE" = true ]; then
fi
# Deploy Claude API proxy (auto-install if missing)
echo "$(timestamp) Setting up Claude API proxy..."
progress "Setting up Claude API proxy"
ssh $SSH_OPTS "$TARGET_HOST" '
echo " Updating Claude API proxy on port 3142..."
# Check for API key in existing service or setup-aiui-server.sh
@@ -510,7 +592,7 @@ PYEOF
' 2>/dev/null || true
# Dev mode for Tailscale HTTP access (cookies need Secure flag disabled over plain HTTP)
echo "$(timestamp) Checking dev mode..."
progress "Configuring dev mode"
ssh $SSH_OPTS "$TARGET_HOST" '
if [ -f /etc/systemd/system/archipelago.service.d/override.conf ] && grep -q "ARCHIPELAGO_DEV_MODE=true" /etc/systemd/system/archipelago.service.d/override.conf 2>/dev/null; then
echo " Dev mode already enabled"
@@ -524,7 +606,7 @@ PYEOF
' 2>/dev/null || true
# Create data directories for DWN, content sharing, federation, identities
echo "$(timestamp) Ensuring data directories for DWN, content, federation..."
progress "Creating data directories"
ssh $SSH_OPTS "$TARGET_HOST" '
sudo mkdir -p /var/lib/archipelago/dwn/messages
sudo mkdir -p /var/lib/archipelago/dwn/protocols
@@ -537,12 +619,11 @@ PYEOF
' 2>/dev/null || true
# Deploy nostr-provider.js for NIP-07 iframe signing (window.nostr support)
echo "$(timestamp) Deploying nostr-provider.js..."
progress "Deploying nostr-provider.js"
scp $SSH_OPTS "$PROJECT_DIR/neode-ui/public/nostr-provider.js" "$TARGET_HOST:/tmp/nostr-provider.js" 2>/dev/null && \
ssh $SSH_OPTS "$TARGET_HOST" 'sudo cp /tmp/nostr-provider.js /opt/archipelago/web-ui/nostr-provider.js && echo " nostr-provider.js deployed"' 2>/dev/null || echo " (nostr-provider.js not found, skipping)"
# Sync nginx config (includes all app proxies, NIP-07 sub_filter, AIUI proxy, external URL proxies)
echo "$(timestamp) Syncing nginx config..."
# Sync nginx config (second pass — includes HTTPS snippets)
scp $SSH_OPTS "$PROJECT_DIR/image-recipe/configs/nginx-archipelago.conf" "$TARGET_HOST:/tmp/nginx-archipelago.conf" 2>/dev/null && \
ssh $SSH_OPTS "$TARGET_HOST" '
sudo cp /tmp/nginx-archipelago.conf /etc/nginx/sites-available/archipelago
@@ -557,7 +638,7 @@ PYEOF
fi
# Fix FileBrowser — recreate if read-only root, create if missing
echo "$(timestamp) Checking FileBrowser..."
progress "Checking FileBrowser"
ssh $SSH_OPTS "$TARGET_HOST" '
DOCKER=podman
command -v podman >/dev/null 2>&1 || DOCKER=docker
@@ -582,19 +663,34 @@ PYEOF
fi
' 2>/dev/null || true
# Write deploy manifest — stamps the server with exactly what was deployed
progress "Writing deploy manifest"
DEPLOY_TS=$(date -u +%Y-%m-%dT%H:%M:%SZ)
ssh $SSH_OPTS "$TARGET_HOST" "sudo tee /opt/archipelago/deploy-manifest.json > /dev/null" << MANIFEST_EOF
{
"commit": "$DEPLOY_COMMIT_FULL",
"commit_short": "$DEPLOY_COMMIT",
"branch": "$DEPLOY_BRANCH",
"dirty": $DEPLOY_DIRTY,
"deployed_at": "$DEPLOY_TS",
"deployed_from": "$(hostname)",
"target": "$TARGET_HOST"
}
MANIFEST_EOF
# Restart services
echo "$(timestamp) Restarting services..."
progress "Restarting services"
ssh $SSH_OPTS "$TARGET_HOST" "sudo systemctl start archipelago && sudo systemctl restart nginx"
# Set up HTTPS for PWA installability (browsers require secure context)
echo "$(timestamp) Setting up HTTPS for PWA install..."
progress "Setting up HTTPS"
ssh $SSH_OPTS "$TARGET_HOST" "sudo bash $TARGET_DIR/scripts/setup-https-dev.sh" 2>&1 | sed 's/^/ /' || true
if [ "$FRONTEND_ONLY" = true ]; then
echo "$(timestamp) Skipping container rebuilds (--frontend-only)"
echo " Skipping container rebuilds (--frontend-only)"
else
# Rebuild and recreate LND UI container (port 8081 so Launch from UI and http://host:8081 both work)
echo "$(timestamp) Rebuilding LND UI..."
progress "Rebuilding LND UI"
if ssh $SSH_OPTS "$TARGET_HOST" "cd $TARGET_DIR/docker/lnd-ui && (command -v podman >/dev/null 2>&1 && sudo podman build --no-cache -t lnd-ui:latest . || sudo docker build --no-cache -t lnd-ui:latest .)" 2>&1 | tail -12 | sed 's/^/ /'; then
echo " Recreating LND UI container (port 8081)..."
ssh $SSH_OPTS "$TARGET_HOST" '
@@ -608,7 +704,7 @@ PYEOF
fi
# Rebuild and recreate ElectrumX UI container (port 50002)
echo "$(timestamp) Rebuilding ElectrumX UI..."
progress "Rebuilding ElectrumX UI"
if ssh $SSH_OPTS "$TARGET_HOST" "cd $TARGET_DIR/docker/electrs-ui && (command -v podman >/dev/null 2>&1 && sudo podman build --no-cache -t electrs-ui:latest . || sudo docker build --no-cache -t electrs-ui:latest .)" 2>&1 | tail -12 | sed 's/^/ /'; then
echo " Recreating ElectrumX UI container (port 50002, host network)..."
ssh $SSH_OPTS "$TARGET_HOST" '
@@ -623,7 +719,7 @@ PYEOF
# Rebuild and recreate Bitcoin UI container (host network, port 8334 in nginx.conf)
# Host network required: bitcoin-ui proxies Bitcoin RPC at 127.0.0.1:8332
echo "$(timestamp) Rebuilding Bitcoin UI..."
progress "Rebuilding Bitcoin UI"
if ssh $SSH_OPTS "$TARGET_HOST" "cd $TARGET_DIR/docker/bitcoin-ui && (command -v podman >/dev/null 2>&1 && sudo podman build --no-cache -t bitcoin-ui:latest . || sudo docker build --no-cache -t bitcoin-ui:latest .)" 2>&1 | tail -12 | sed 's/^/ /'; then
echo " Recreating Bitcoin UI container (port 8334, host network)..."
ssh $SSH_OPTS "$TARGET_HOST" '
@@ -638,7 +734,7 @@ PYEOF
# Bitcoin Knots: required for Mempool, ElectrumX, BTCPay, Fedimint
TARGET_IP="$(echo "$TARGET_HOST" | cut -d@ -f2)"
echo "$(timestamp) Ensuring Bitcoin Knots..."
progress "Ensuring Bitcoin Knots"
ssh $SSH_OPTS "$TARGET_HOST" "
DOCKER=podman
command -v podman >/dev/null 2>&1 || DOCKER=docker
@@ -672,7 +768,7 @@ PYEOF
" 2>&1 | sed 's/^/ /' || true
# Fix Mempool: clean duplicates, ensure full stack - mysql, backend (8999), frontend (4080)
echo "$(timestamp) Fixing Mempool stack..."
progress "Fixing Mempool stack"
ssh $SSH_OPTS "$TARGET_HOST" "
DOCKER=podman
command -v podman >/dev/null 2>&1 || DOCKER=docker
@@ -770,7 +866,7 @@ PYEOF
" 2>&1 | sed 's/^/ /' || true
# Fix BTCPay Server: requires PostgreSQL + NBXplorer (BTCPay needs NBXplorer for block indexing)
echo "$(timestamp) Fixing BTCPay Server stack..."
progress "Fixing BTCPay stack"
TARGET_IP="$(echo "$TARGET_HOST" | cut -d@ -f2)"
ssh $SSH_OPTS "$TARGET_HOST" "
DOCKER=podman
@@ -847,7 +943,7 @@ PYEOF
" 2>&1 | sed 's/^/ /' || true
# Ensure Immich stack (postgres + redis + server) - creates if missing
echo "$(timestamp) Ensuring Immich stack..."
progress "Ensuring Immich stack"
ssh $SSH_OPTS "$TARGET_HOST" "
DOCKER=podman
command -v podman >/dev/null 2>&1 || DOCKER=docker
@@ -889,7 +985,7 @@ PYEOF
" 2>&1 | sed 's/^/ /' || true
# Tor: global hidden services - each service gets its own .onion address
echo "$(timestamp) Setting up Tor..."
progress "Setting up Tor"
TARGET_IP="$(echo "$TARGET_HOST" | cut -d@ -f2)"
ssh $SSH_OPTS "$TARGET_HOST" "
DOCKER=podman
@@ -983,7 +1079,7 @@ print("torrc generated with %d services" % (len(lines) // 3))
# Recreate Fedimint with FM_API_URL for Guardian UI (fixes "Api URL must be configured")
section_start
echo "$(timestamp) Fixing Fedimint API URL..."
progress "Fixing Fedimint"
TARGET_IP="$(echo "$TARGET_HOST" | cut -d@ -f2)"
TIMEOUT_CMD=""
command -v timeout >/dev/null 2>&1 && TIMEOUT_CMD="timeout 90"
@@ -1055,7 +1151,7 @@ print("torrc generated with %d services" % (len(lines) // 3))
section_end
# LND: Lightning Network Daemon (requires bitcoin-knots on archy-net)
echo "$(timestamp) Ensuring LND..."
progress "Ensuring LND"
ssh $SSH_OPTS "$TARGET_HOST" '
DOCKER=podman
command -v podman >/dev/null 2>&1 || DOCKER=docker
@@ -1106,7 +1202,7 @@ LNDCONF
' 2>&1 | sed 's/^/ /' || true
# Home Assistant
echo "$(timestamp) Ensuring Home Assistant..."
progress "Ensuring Home Assistant"
ssh $SSH_OPTS "$TARGET_HOST" '
DOCKER=podman
command -v podman >/dev/null 2>&1 || DOCKER=docker
@@ -1129,7 +1225,7 @@ LNDCONF
' 2>&1 | sed 's/^/ /' || true
# Grafana
echo "$(timestamp) Ensuring Grafana..."
progress "Ensuring Grafana"
ssh $SSH_OPTS "$TARGET_HOST" '
DOCKER=podman
command -v podman >/dev/null 2>&1 || DOCKER=docker
@@ -1153,7 +1249,7 @@ LNDCONF
' 2>&1 | sed 's/^/ /' || true
# Jellyfin
echo "$(timestamp) Ensuring Jellyfin..."
progress "Ensuring Jellyfin"
ssh $SSH_OPTS "$TARGET_HOST" '
DOCKER=podman
command -v podman >/dev/null 2>&1 || DOCKER=docker
@@ -1176,7 +1272,7 @@ LNDCONF
' 2>&1 | sed 's/^/ /' || true
# Vaultwarden
echo "$(timestamp) Ensuring Vaultwarden..."
progress "Ensuring Vaultwarden"
ssh $SSH_OPTS "$TARGET_HOST" '
DOCKER=podman
command -v podman >/dev/null 2>&1 || DOCKER=docker
@@ -1198,7 +1294,7 @@ LNDCONF
' 2>&1 | sed 's/^/ /' || true
# SearXNG (privacy search engine — used by AIUI web search)
echo "$(timestamp) Ensuring SearXNG..."
progress "Ensuring SearXNG"
ssh $SSH_OPTS "$TARGET_HOST" '
DOCKER=podman
command -v podman >/dev/null 2>&1 || DOCKER=docker
@@ -1218,7 +1314,7 @@ LNDCONF
' 2>&1 | sed 's/^/ /' || true
# Ollama (local LLM inference — used by AIUI)
echo "$(timestamp) Ensuring Ollama..."
progress "Ensuring Ollama"
ssh $SSH_OPTS "$TARGET_HOST" '
DOCKER=podman
command -v podman >/dev/null 2>&1 || DOCKER=docker
@@ -1241,6 +1337,7 @@ LNDCONF
fi # end FRONTEND_ONLY guard
# Ensure UFW allows forwarded traffic (required for podman container port access from LAN)
progress "Fixing UFW forward policy"
ssh $SSH_OPTS "$TARGET_HOST" '
if grep -q "DEFAULT_FORWARD_POLICY=\"DROP\"" /etc/default/ufw 2>/dev/null; then
sudo sed -i "s/DEFAULT_FORWARD_POLICY=\"DROP\"/DEFAULT_FORWARD_POLICY=\"ACCEPT\"/" /etc/default/ufw
@@ -1249,38 +1346,73 @@ LNDCONF
fi
' 2>&1 | sed 's/^/ /' || true
# Fix IndeedHub for iframe + NIP-07: remove X-Frame-Options, inject nostr-provider.js
# Fix IndeedHub for iframe + NIP-07: remove X-Frame-Options, inject nostr-provider.js,
# resolve container IPs for nginx proxy (DNS resolver 127.0.0.11 is unreliable in podman)
progress "Fixing IndeedHub for NIP-07"
ssh $SSH_OPTS "$TARGET_HOST" '
if sudo podman ps --format "{{.Names}}" 2>/dev/null | grep -q "^indeedhub$"; then
CHANGED=false
NETWORK=$(sudo podman inspect indeedhub --format "{{range \$k, \$v := .NetworkSettings.Networks}}{{\$k}}{{end}}" 2>/dev/null)
# Remove X-Frame-Options so iframe works
if sudo podman exec indeedhub grep -q "X-Frame-Options" /etc/nginx/conf.d/default.conf 2>/dev/null; then
sudo podman exec indeedhub sed -i "/X-Frame-Options/d" /etc/nginx/conf.d/default.conf
CHANGED=true
echo " Removed X-Frame-Options from IndeedHub"
fi
# Inject nostr-provider.js for NIP-07 signing
if ! sudo podman exec indeedhub test -f /usr/share/nginx/html/nostr-provider.js 2>/dev/null; then
sudo podman cp /opt/archipelago/web-ui/nostr-provider.js indeedhub:/usr/share/nginx/html/nostr-provider.js 2>/dev/null
echo " Copied nostr-provider.js into IndeedHub"
fi
# Add nostr-provider.js + sub_filter to nginx config
if ! sudo podman exec indeedhub grep -q "nostr-provider" /etc/nginx/conf.d/default.conf 2>/dev/null; then
sudo podman exec indeedhub cat /etc/nginx/conf.d/default.conf > /tmp/ih-nginx.conf 2>/dev/null
sed -i "/try_files.*index.html/i\\ sub_filter_once on;\n sub_filter '"'"'</head>'"'"' '"'"'<script src=\"/nostr-provider.js\"></script></head>'"'"';" /tmp/ih-nginx.conf
# Add nostr-provider location block before sw.js block
sed -i "/location = \/sw.js {/i\\ location = /nostr-provider.js {\n add_header Cache-Control \"no-cache, no-store, must-revalidate\";\n expires off;\n }\n" /tmp/ih-nginx.conf
# Add sub_filter for nostr-provider injection
sed -i "/try_files.*index.html/a\\ sub_filter_once on;\n sub_filter '"'"'</head>'"'"' '"'"'<script src=\"/nostr-provider.js\"></script></head>'"'"';" /tmp/ih-nginx.conf
sudo podman cp /tmp/ih-nginx.conf indeedhub:/etc/nginx/conf.d/default.conf 2>/dev/null
rm -f /tmp/ih-nginx.conf
CHANGED=true
echo " Injected nostr-provider.js into IndeedHub nginx"
fi
# Replace DNS-based upstream resolution with hardcoded container IPs
# (podman DNS resolver 127.0.0.11 is unreliable, causing 502 errors)
API_IP=$(sudo podman inspect indeedhub-build_api_1 --format "{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}" 2>/dev/null)
MINIO_IP=$(sudo podman inspect indeedhub-minio --format "{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}" 2>/dev/null)
RELAY_IP=$(sudo podman inspect indeedhub-relay --format "{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}" 2>/dev/null)
if [ -n "$API_IP" ] && [ -n "$MINIO_IP" ] && [ -n "$RELAY_IP" ]; then
sudo podman exec indeedhub cat /etc/nginx/conf.d/default.conf > /tmp/ih-nginx.conf 2>/dev/null
# Remove DNS resolver lines and replace upstream variables with hardcoded IPs
sed -i "s|resolver 127.0.0.11 valid=30s ipv6=off;||g" /tmp/ih-nginx.conf
sed -i "s|set \$api_upstream http://api:4000;|set \$api_upstream http://$API_IP:4000;|g" /tmp/ih-nginx.conf
sed -i "s|set \$minio_upstream http://minio:9000;|set \$minio_upstream http://$MINIO_IP:9000;|g" /tmp/ih-nginx.conf
sed -i "s|set \$relay_upstream http://relay:8080;|set \$relay_upstream http://$RELAY_IP:8080;|g" /tmp/ih-nginx.conf
sed -i "s|proxy_set_header Host \$host;|proxy_set_header Host \$http_host;|g" /tmp/ih-nginx.conf
sudo podman cp /tmp/ih-nginx.conf indeedhub:/etc/nginx/conf.d/default.conf 2>/dev/null
rm -f /tmp/ih-nginx.conf
CHANGED=true
echo " Patched IndeedHub nginx with container IPs (API=$API_IP MINIO=$MINIO_IP RELAY=$RELAY_IP)"
fi
if [ "$CHANGED" = true ]; then
sudo podman exec indeedhub nginx -s reload 2>/dev/null
fi
fi
' 2>&1 | sed 's/^/ /' || true
# Run container doctor — auto-fix common container health issues
progress "Running container doctor"
"$SCRIPT_DIR/container-doctor.sh" "$TARGET_HOST" 2>&1 | sed 's/^/ /' || true
# Post-deploy health check — wait up to 60s for server to come healthy
echo ""
echo "$(timestamp) 🩺 Post-deploy health check..."
progress "Post-deploy health check"
HEALTH_OK=false
for i in $(seq 1 12); do
POST_HEALTH=$(curl -s -o /dev/null -w '%{http_code}' --connect-timeout 5 "http://$TARGET_IP_ONLY/health" 2>/dev/null || echo "000")
@@ -1320,8 +1452,14 @@ LNDCONF
DEPLOY_END=$(date +%s)
DEPLOY_ELAPSED=$((DEPLOY_END - DEPLOY_START))
# Append to local deploy history log (gitignored)
DEPLOY_LOG="$PROJECT_DIR/scripts/deploy-history.log"
echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) | $DEPLOY_BRANCH@$DEPLOY_COMMIT | dirty=$DEPLOY_DIRTY | target=$TARGET_HOST | ${DEPLOY_ELAPSED}s" >> "$DEPLOY_LOG"
echo ""
echo "$(timestamp) ✅ Deployed to live system! (${DEPLOY_ELAPSED}s total)"
echo " Commit: $DEPLOY_BRANCH @ $DEPLOY_COMMIT (dirty=$DEPLOY_DIRTY)"
echo " Backend: $(ssh $SSH_OPTS "$TARGET_HOST" 'sudo systemctl is-active archipelago')"
echo " Web UI: http://$TARGET_IP_ONLY"
echo " PWA install: https://$TARGET_IP_ONLY (use HTTPS, accept cert once, then Install app)"

View File

@@ -670,4 +670,13 @@ for c in bitcoin-knots lnd btcpay-server fedimint homeassistant grafana uptime-k
done
log "Post-boot validation: $RUNNING/$TOTAL core containers running"
# 12. Run container doctor for any remaining issues
log "Running container doctor..."
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
if [ -x "$SCRIPT_DIR/container-doctor.sh" ]; then
bash "$SCRIPT_DIR/container-doctor.sh" --local 2>&1 | tee -a "$LOG"
elif [ -x "/opt/archipelago/scripts/container-doctor.sh" ]; then
bash "/opt/archipelago/scripts/container-doctor.sh" --local 2>&1 | tee -a "$LOG"
fi
log "First-boot container creation complete"

View File

@@ -0,0 +1,233 @@
#!/bin/bash
set -e
# Fix corrupted IndeedHub containers + SearXNG
# All images were exported as the same (wrong) image during multi-node deploy.
# This script: stops broken containers, removes them, recreates with correct images.
echo "=== IndeedHub Container Fix Script ==="
# Detect node IP (Tailscale or LAN)
NODE_IP=$(hostname -I | awk '{for(i=1;i<=NF;i++) if($i ~ /^100\./) print $i}')
if [ -z "$NODE_IP" ]; then
NODE_IP=$(hostname -I | awk '{print $1}')
fi
echo "Node IP: $NODE_IP"
NETWORK="indeedhub-build_indeedhub-network"
# Load custom images if tar exists
if [ -f /tmp/indeedhub-images.tar ]; then
echo "Loading custom images from tar..."
sudo podman load < /tmp/indeedhub-images.tar 2>&1 | tail -5
fi
# Verify correct images are available
echo "Verifying images..."
for img in "docker.io/library/redis:7-alpine" "docker.io/minio/minio:latest" "docker.io/library/postgres:16-alpine" "docker.io/scsibug/nostr-rs-relay:latest" "docker.io/searxng/searxng:latest" "localhost/indeedhub:latest" "localhost/indeedhub-build_api:latest" "localhost/indeedhub-build_ffmpeg-worker:latest"; do
if ! sudo podman image exists "$img" 2>/dev/null; then
echo "ERROR: Missing image $img"
exit 1
fi
done
echo "All images verified."
# Ensure network exists
if ! sudo podman network exists "$NETWORK" 2>/dev/null; then
echo "Creating network $NETWORK..."
sudo podman network create "$NETWORK" 2>/dev/null || true
fi
# Stop all affected containers
echo "Stopping containers..."
for c in indeedhub indeedhub-build_api_1 indeedhub-build_ffmpeg-worker_1 indeedhub-relay indeedhub-redis indeedhub-minio indeedhub-postgres searxng; do
sudo podman stop "$c" 2>/dev/null || true
done
# Remove all affected containers
echo "Removing containers..."
for c in indeedhub indeedhub-build_api_1 indeedhub-build_ffmpeg-worker_1 indeedhub-relay indeedhub-redis indeedhub-minio indeedhub-postgres searxng; do
sudo podman rm -f "$c" 2>/dev/null || true
done
# 1. PostgreSQL (must start first — others depend on it)
echo "Creating postgres..."
sudo podman run -d --name indeedhub-postgres \
--restart unless-stopped \
--network "$NETWORK" --network-alias postgres \
-v indeedhub-postgres-data:/var/lib/postgresql/data \
-e POSTGRES_USER=indeedhub \
-e POSTGRES_PASSWORD=indeehhub-archy-2026 \
-e POSTGRES_DB=indeedhub \
docker.io/library/postgres:16-alpine
# Wait for postgres to be ready
echo "Waiting for postgres..."
for i in $(seq 1 15); do
if sudo podman exec indeedhub-postgres pg_isready -U indeedhub 2>/dev/null; then
echo "Postgres ready."
break
fi
sleep 2
done
# 2. Redis
echo "Creating redis..."
sudo podman run -d --name indeedhub-redis \
--restart unless-stopped \
--network "$NETWORK" --network-alias redis \
-v indeedhub-redis-data:/data \
docker.io/library/redis:7-alpine \
redis-server --appendonly yes
# 3. MinIO
echo "Creating minio..."
sudo podman run -d --name indeedhub-minio \
--restart unless-stopped \
--network "$NETWORK" --network-alias minio \
-v indeedhub-minio-data:/data \
-e MINIO_ROOT_USER=indeeadmin \
-e MINIO_ROOT_PASSWORD=indeeadmin2026 \
docker.io/minio/minio:latest \
server /data --console-address ":9001"
# 4. Nostr Relay
echo "Creating relay..."
sudo podman run -d --name indeedhub-relay \
--restart unless-stopped \
--network "$NETWORK" --network-alias relay \
-v indeedhub-relay-data:/usr/src/app/db \
docker.io/scsibug/nostr-rs-relay:latest
# 5. API
echo "Creating api..."
sudo podman run -d --name indeedhub-build_api_1 \
--restart unless-stopped \
--network "$NETWORK" --network-alias api \
-e ENVIRONMENT=production \
-e PORT=4000 \
-e DOMAIN="$NODE_IP" \
-e FRONTEND_URL="http://$NODE_IP" \
-e DATABASE_HOST=postgres \
-e DATABASE_PORT=5432 \
-e DATABASE_USER=indeedhub \
-e DATABASE_PASSWORD=indeehhub-archy-2026 \
-e DATABASE_NAME=indeedhub \
-e QUEUE_HOST=redis \
-e QUEUE_PORT=6379 \
-e "QUEUE_PASSWORD=" \
-e S3_ENDPOINT=http://minio:9000 \
-e AWS_REGION=us-east-1 \
-e AWS_ACCESS_KEY=indeeadmin \
-e AWS_SECRET_KEY=indeeadmin2026 \
-e S3_PRIVATE_BUCKET_NAME=indeedhub-private \
-e S3_PUBLIC_BUCKET_NAME=indeedhub-public \
-e S3_PUBLIC_BUCKET_URL=/storage \
-e "BTCPAY_URL=" \
-e "BTCPAY_API_KEY=" \
-e "BTCPAY_STORE_ID=" \
-e "BTCPAY_WEBHOOK_SECRET=" \
-e NOSTR_JWT_SECRET=archipelago-indeehhub-jwt-secret-2026 \
-e NOSTR_JWT_EXPIRES_IN=7d \
-e AES_MASTER_SECRET=0123456789abcdef0123456789abcdef \
-e "ADMIN_API_KEY=" \
-e NODE_OPTIONS=--max-old-space-size=1024 \
--health-cmd "wget --no-verbose --tries=1 --spider http://localhost:4000/nostr-auth/health || exit 1" \
--health-interval 60s \
--health-timeout 30s \
--health-retries 5 \
--health-start-period 60s \
localhost/indeedhub-build_api:latest \
sh -c "echo 'Running database migrations...' && npx typeorm migration:run -d dist/database/ormconfig.js && echo 'Migrations complete.' && npm run start:prod"
# 6. FFmpeg Worker
echo "Creating ffmpeg-worker..."
sudo podman run -d --name indeedhub-build_ffmpeg-worker_1 \
--restart unless-stopped \
--network "$NETWORK" --network-alias ffmpeg-worker \
-e ENVIRONMENT=production \
-e DATABASE_HOST=postgres \
-e DATABASE_PORT=5432 \
-e DATABASE_USER=indeedhub \
-e DATABASE_PASSWORD=indeehhub-archy-2026 \
-e DATABASE_NAME=indeedhub \
-e QUEUE_HOST=redis \
-e QUEUE_PORT=6379 \
-e "QUEUE_PASSWORD=" \
-e S3_ENDPOINT=http://minio:9000 \
-e AWS_REGION=us-east-1 \
-e AWS_ACCESS_KEY=indeeadmin \
-e AWS_SECRET_KEY=indeeadmin2026 \
-e S3_PRIVATE_BUCKET_NAME=indeedhub-private \
-e S3_PUBLIC_BUCKET_NAME=indeedhub-public \
-e S3_PUBLIC_BUCKET_URL=/storage \
-e AES_MASTER_SECRET=0123456789abcdef0123456789abcdef \
localhost/indeedhub-build_ffmpeg-worker:latest
# 7. IndeedHub Frontend
echo "Creating indeedhub frontend..."
sudo podman run -d --name indeedhub \
--restart unless-stopped \
--network "$NETWORK" \
-p 7777:7777 \
--label "com.archipelago.app=indeedhub" \
--label "com.archipelago.title=IndeedHub" \
--label "com.archipelago.version=0.1.0" \
--label "com.archipelago.category=media" \
--label "com.archipelago.port=7777" \
localhost/indeedhub:latest
# Fix IndeedHub for iframe: remove X-Frame-Options, inject nostr-provider, hardcode container IPs
sleep 3
if sudo podman ps --format '{{.Names}}' 2>/dev/null | grep -q "^indeedhub$"; then
sudo podman exec indeedhub sed -i "/X-Frame-Options/d" /etc/nginx/conf.d/default.conf 2>/dev/null || true
# Inject nostr-provider.js if available
if [ -f /opt/archipelago/web-ui/nostr-provider.js ]; then
sudo podman cp /opt/archipelago/web-ui/nostr-provider.js indeedhub:/usr/share/nginx/html/nostr-provider.js 2>/dev/null || true
fi
# Add nostr-provider location block + sub_filter
if ! sudo podman exec indeedhub grep -q "nostr-provider" /etc/nginx/conf.d/default.conf 2>/dev/null; then
sudo podman exec indeedhub cat /etc/nginx/conf.d/default.conf > /tmp/ih-nginx.conf 2>/dev/null
sed -i "/location = \/sw.js {/i\\ location = /nostr-provider.js {\n add_header Cache-Control \"no-cache, no-store, must-revalidate\";\n expires off;\n }\n" /tmp/ih-nginx.conf
sed -i "/try_files.*index.html/a\\ sub_filter_once on;\n sub_filter '</head>' '<script src=\"/nostr-provider.js\"></script></head>';" /tmp/ih-nginx.conf
sudo podman cp /tmp/ih-nginx.conf indeedhub:/etc/nginx/conf.d/default.conf 2>/dev/null || true
rm -f /tmp/ih-nginx.conf
fi
# Replace DNS-based upstream resolution with hardcoded container IPs
# (podman DNS resolver 127.0.0.11 is unreliable, causing 502 errors)
API_IP=$(sudo podman inspect indeedhub-build_api_1 --format "{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}" 2>/dev/null)
MINIO_IP=$(sudo podman inspect indeedhub-minio --format "{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}" 2>/dev/null)
RELAY_IP=$(sudo podman inspect indeedhub-relay --format "{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}" 2>/dev/null)
if [ -n "$API_IP" ] && [ -n "$MINIO_IP" ] && [ -n "$RELAY_IP" ]; then
sudo podman exec indeedhub cat /etc/nginx/conf.d/default.conf > /tmp/ih-nginx.conf 2>/dev/null
sed -i "s|resolver 127.0.0.11 valid=30s ipv6=off;||g" /tmp/ih-nginx.conf
sed -i "s|set \$api_upstream http://api:4000;|set \$api_upstream http://$API_IP:4000;|g" /tmp/ih-nginx.conf
sed -i "s|set \$minio_upstream http://minio:9000;|set \$minio_upstream http://$MINIO_IP:9000;|g" /tmp/ih-nginx.conf
sed -i "s|set \$relay_upstream http://relay:8080;|set \$relay_upstream http://$RELAY_IP:8080;|g" /tmp/ih-nginx.conf
sed -i "s|proxy_set_header Host \$host;|proxy_set_header Host \$http_host;|g" /tmp/ih-nginx.conf
sudo podman cp /tmp/ih-nginx.conf indeedhub:/etc/nginx/conf.d/default.conf 2>/dev/null || true
rm -f /tmp/ih-nginx.conf
echo "Patched IndeedHub nginx with container IPs (API=$API_IP MINIO=$MINIO_IP RELAY=$RELAY_IP)"
fi
sudo podman exec indeedhub nginx -s reload 2>/dev/null || true
echo "Applied IndeedHub iframe fix."
fi
# 8. SearXNG (standalone — no cap-drop ALL, searxng needs write access to /etc/searxng/)
echo "Creating searxng..."
sudo podman run -d --name searxng \
--restart unless-stopped \
-p 8888:8080 \
docker.io/searxng/searxng:latest
echo ""
echo "=== Verifying container status ==="
sleep 5
sudo podman ps -a --filter name=indeedhub --filter name=searxng --format "table {{.Names}}\t{{.Status}}" 2>&1
echo ""
echo "=== FIX COMPLETE ==="