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)"