mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-10-23 20:52:26 +00:00
feat: rewrite manage.sh as idempotent setup wizard
- State tracking (.setup-state) — resume from where you left off - Every step checks what's already done and skips it - Safe to Ctrl+C and re-run at any point - Auto-generates random API key on first config creation - HTTPS choice menu: domain (auto), Cloudflare/proxy (HTTP), or later - DNS validation with IP comparison - Port 80 conflict detection - Status shows packet/node counts, DB size, per-service health - Backup auto-creates ./backups/ dir, restore backs up current first - Reset command (keeps data + config, removes container + image) - .setup-state in .gitignore
This commit is contained in:
1 parent
eaa42ed097
commit
e169f9c7b2
1 file changed
+383
-129
@@ -1,26 +1,44 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# MeshCore Analyzer — Setup & Management Helper
|
# MeshCore Analyzer — Setup & Management Helper
|
||||||
# Usage: ./manage.sh [command]
|
# Usage: ./manage.sh [command]
|
||||||
|
#
|
||||||
|
# Idempotent: safe to cancel and re-run at any point.
|
||||||
|
# Each step checks what's already done and skips it.
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
CONTAINER_NAME="meshcore-analyzer"
|
CONTAINER_NAME="meshcore-analyzer"
|
||||||
IMAGE_NAME="meshcore-analyzer"
|
IMAGE_NAME="meshcore-analyzer"
|
||||||
DATA_VOLUME="meshcore-data"
|
DATA_VOLUME="meshcore-data"
|
||||||
CADDY_VOLUME="caddy-data"
|
CADDY_VOLUME="caddy-data"
|
||||||
|
STATE_FILE=".setup-state"
|
||||||
|
|
||||||
# Colors
|
# Colors
|
||||||
RED='\033[0;31m'
|
RED='\033[0;31m'
|
||||||
GREEN='\033[0;32m'
|
GREEN='\033[0;32m'
|
||||||
YELLOW='\033[1;33m'
|
YELLOW='\033[1;33m'
|
||||||
CYAN='\033[0;36m'
|
CYAN='\033[0;36m'
|
||||||
|
BOLD='\033[1m'
|
||||||
NC='\033[0m'
|
NC='\033[0m'
|
||||||
|
|
||||||
log() { echo -e "${GREEN}✓${NC} $1"; }
|
log() { echo -e "${GREEN}✓${NC} $1"; }
|
||||||
warn() { echo -e "${YELLOW}⚠${NC} $1"; }
|
warn() { echo -e "${YELLOW}⚠${NC} $1"; }
|
||||||
err() { echo -e "${RED}✗${NC} $1"; }
|
err() { echo -e "${RED}✗${NC} $1"; }
|
||||||
info() { echo -e "${CYAN}→${NC} $1"; }
|
info() { echo -e "${CYAN}→${NC} $1"; }
|
||||||
|
step() { echo -e "\n${BOLD}[$1/$TOTAL_STEPS] $2${NC}"; }
|
||||||
|
|
||||||
# ─── Setup ────────────────────────────────────────────────────────────────
|
confirm() {
|
||||||
|
read -p " $1 [y/N] " -n 1 -r
|
||||||
|
echo
|
||||||
|
[[ $REPLY =~ ^[Yy]$ ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
# State tracking — marks completed steps so re-runs skip them
|
||||||
|
mark_done() { echo "$1" >> "$STATE_FILE"; }
|
||||||
|
is_done() { [ -f "$STATE_FILE" ] && grep -qx "$1" "$STATE_FILE" 2>/dev/null; }
|
||||||
|
|
||||||
|
# ─── Setup Wizard ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
TOTAL_STEPS=6
|
||||||
|
|
||||||
cmd_setup() {
|
cmd_setup() {
|
||||||
echo ""
|
echo ""
|
||||||
@@ -29,123 +47,271 @@ cmd_setup() {
|
|||||||
echo "═══════════════════════════════════════"
|
echo "═══════════════════════════════════════"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Check Docker
|
if [ -f "$STATE_FILE" ]; then
|
||||||
|
info "Resuming previous setup. Delete ${STATE_FILE} to start over."
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Step 1: Check Docker ──
|
||||||
|
step 1 "Checking Docker"
|
||||||
|
|
||||||
if ! command -v docker &> /dev/null; then
|
if ! command -v docker &> /dev/null; then
|
||||||
err "Docker is not installed."
|
err "Docker is not installed."
|
||||||
echo ""
|
echo ""
|
||||||
echo "Install it with:"
|
echo " Install it:"
|
||||||
echo " curl -fsSL https://get.docker.com | sh"
|
echo " curl -fsSL https://get.docker.com | sh"
|
||||||
echo " sudo usermod -aG docker \$USER"
|
echo " sudo usermod -aG docker \$USER"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Then log out, log back in, and run this script again."
|
echo " Then log out, log back in, and run ./manage.sh setup again."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
log "Docker found: $(docker --version | head -1)"
|
|
||||||
|
|
||||||
# Check if container already exists
|
# Check if user can actually run Docker
|
||||||
if docker ps -a --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
|
if ! docker info &> /dev/null; then
|
||||||
warn "Container '${CONTAINER_NAME}' already exists."
|
err "Docker is installed but your user can't run it."
|
||||||
read -p " Remove it and start fresh? [y/N] " -n 1 -r
|
echo ""
|
||||||
echo
|
echo " Fix: sudo usermod -aG docker \$USER"
|
||||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
echo " Then log out, log back in, and try again."
|
||||||
docker stop "$CONTAINER_NAME" 2>/dev/null || true
|
exit 1
|
||||||
docker rm "$CONTAINER_NAME" 2>/dev/null || true
|
|
||||||
log "Old container removed."
|
|
||||||
else
|
|
||||||
echo "Aborting. Use './manage.sh stop' and './manage.sh start' to manage the existing container."
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Config
|
log "Docker $(docker --version | grep -oP 'version \K[^ ,]+')"
|
||||||
if [ ! -f config.json ]; then
|
mark_done "docker"
|
||||||
cp config.example.json config.json
|
|
||||||
log "Created config.json from example."
|
# ── Step 2: Config ──
|
||||||
warn "Edit config.json to set your apiKey before continuing."
|
step 2 "Configuration"
|
||||||
echo " nano config.json"
|
|
||||||
read -p " Press Enter when done..."
|
if [ -f config.json ]; then
|
||||||
else
|
|
||||||
log "config.json exists."
|
log "config.json exists."
|
||||||
fi
|
# Sanity check the JSON
|
||||||
|
if ! python3 -c "import json; json.load(open('config.json'))" 2>/dev/null && \
|
||||||
|
! node -e "JSON.parse(require('fs').readFileSync('config.json'))" 2>/dev/null; then
|
||||||
|
err "config.json has invalid JSON. Fix it and re-run setup."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
log "config.json is valid JSON."
|
||||||
|
else
|
||||||
|
info "Creating config.json from example..."
|
||||||
|
cp config.example.json config.json
|
||||||
|
|
||||||
# Caddyfile
|
# Generate a random API key
|
||||||
if [ ! -f caddy-config/Caddyfile ]; then
|
if command -v openssl &> /dev/null; then
|
||||||
|
API_KEY=$(openssl rand -hex 16)
|
||||||
|
else
|
||||||
|
API_KEY=$(head -c 32 /dev/urandom | xxd -p | head -c 32)
|
||||||
|
fi
|
||||||
|
# Replace the placeholder API key
|
||||||
|
if command -v sed &> /dev/null; then
|
||||||
|
sed -i "s/your-secret-api-key-here/${API_KEY}/" config.json
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Created config.json with random API key."
|
||||||
|
echo ""
|
||||||
|
echo " You can customize config.json later (map center, branding, etc)."
|
||||||
|
echo " Edit with: nano config.json"
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
mark_done "config"
|
||||||
|
|
||||||
|
# ── Step 3: Domain & HTTPS ──
|
||||||
|
step 3 "Domain & HTTPS"
|
||||||
|
|
||||||
|
if [ -f caddy-config/Caddyfile ]; then
|
||||||
|
EXISTING_DOMAIN=$(grep -v '^#' caddy-config/Caddyfile 2>/dev/null | head -1 | tr -d ' {')
|
||||||
|
if [ "$EXISTING_DOMAIN" = ":80" ]; then
|
||||||
|
log "Caddyfile exists (HTTP only, no HTTPS)."
|
||||||
|
else
|
||||||
|
log "Caddyfile exists for ${EXISTING_DOMAIN}"
|
||||||
|
fi
|
||||||
|
else
|
||||||
mkdir -p caddy-config
|
mkdir -p caddy-config
|
||||||
echo ""
|
echo ""
|
||||||
read -p " Enter your domain (e.g., analyzer.example.com): " DOMAIN
|
echo " How do you want to handle HTTPS?"
|
||||||
if [ -z "$DOMAIN" ]; then
|
echo ""
|
||||||
warn "No domain entered. Using HTTP-only (port 80, no HTTPS)."
|
echo " 1) I have a domain pointed at this server (automatic HTTPS)"
|
||||||
echo ':80 {
|
echo " 2) I'll use Cloudflare Tunnel or my own proxy (HTTP only)"
|
||||||
reverse_proxy localhost:3000
|
echo " 3) Just HTTP for now, I'll set up HTTPS later"
|
||||||
}' > caddy-config/Caddyfile
|
echo ""
|
||||||
else
|
read -p " Choose [1/2/3]: " -n 1 -r
|
||||||
echo "${DOMAIN} {
|
echo ""
|
||||||
|
|
||||||
|
case $REPLY in
|
||||||
|
1)
|
||||||
|
read -p " Enter your domain (e.g., analyzer.example.com): " DOMAIN
|
||||||
|
if [ -z "$DOMAIN" ]; then
|
||||||
|
err "No domain entered. Re-run setup to try again."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "${DOMAIN} {
|
||||||
reverse_proxy localhost:3000
|
reverse_proxy localhost:3000
|
||||||
}" > caddy-config/Caddyfile
|
}" > caddy-config/Caddyfile
|
||||||
log "Caddyfile created for ${DOMAIN}"
|
log "Caddyfile created for ${DOMAIN}"
|
||||||
|
|
||||||
# Check DNS
|
# Validate DNS
|
||||||
info "Checking DNS for ${DOMAIN}..."
|
info "Checking DNS..."
|
||||||
RESOLVED_IP=$(dig +short "$DOMAIN" 2>/dev/null | head -1)
|
RESOLVED_IP=$(dig +short "$DOMAIN" 2>/dev/null | grep -E '^[0-9]+\.' | head -1)
|
||||||
MY_IP=$(curl -s ifconfig.me 2>/dev/null || echo "unknown")
|
MY_IP=$(curl -s -4 ifconfig.me 2>/dev/null || curl -s -4 icanhazip.com 2>/dev/null || echo "unknown")
|
||||||
if [ -z "$RESOLVED_IP" ]; then
|
|
||||||
warn "DNS not resolving yet. Make sure your A record points to ${MY_IP}"
|
if [ -z "$RESOLVED_IP" ]; then
|
||||||
warn "HTTPS provisioning will fail until DNS propagates."
|
warn "${DOMAIN} doesn't resolve yet."
|
||||||
elif [ "$RESOLVED_IP" = "$MY_IP" ]; then
|
warn "Create an A record pointing to ${MY_IP}"
|
||||||
log "DNS resolves to ${RESOLVED_IP} — matches this server."
|
warn "HTTPS won't work until DNS propagates (1-60 min)."
|
||||||
else
|
echo ""
|
||||||
warn "DNS resolves to ${RESOLVED_IP} but this server is ${MY_IP}"
|
if ! confirm "Continue anyway?"; then
|
||||||
warn "HTTPS may fail if the domain doesn't point here."
|
echo " Run ./manage.sh setup again when DNS is ready."
|
||||||
fi
|
exit 0
|
||||||
|
fi
|
||||||
|
elif [ "$RESOLVED_IP" = "$MY_IP" ]; then
|
||||||
|
log "DNS resolves correctly: ${DOMAIN} → ${MY_IP}"
|
||||||
|
else
|
||||||
|
warn "${DOMAIN} resolves to ${RESOLVED_IP} but this server is ${MY_IP}"
|
||||||
|
warn "HTTPS provisioning will fail if the domain doesn't point here."
|
||||||
|
if ! confirm "Continue anyway?"; then
|
||||||
|
echo " Fix DNS and run ./manage.sh setup again."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check port 80
|
||||||
|
if command -v curl &> /dev/null; then
|
||||||
|
if curl -s --connect-timeout 3 "http://localhost:80" &>/dev/null || \
|
||||||
|
curl -s --connect-timeout 3 "http://${MY_IP}:80" &>/dev/null 2>&1; then
|
||||||
|
warn "Something is already listening on port 80."
|
||||||
|
warn "Stop it first: sudo systemctl stop nginx apache2"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
2|3)
|
||||||
|
echo ':80 {
|
||||||
|
reverse_proxy localhost:3000
|
||||||
|
}' > caddy-config/Caddyfile
|
||||||
|
log "Caddyfile created (HTTP only on port 80)."
|
||||||
|
if [ "$REPLY" = "2" ]; then
|
||||||
|
echo " Point your Cloudflare Tunnel or proxy to this server's port 80."
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
warn "Invalid choice. Defaulting to HTTP only."
|
||||||
|
echo ':80 {
|
||||||
|
reverse_proxy localhost:3000
|
||||||
|
}' > caddy-config/Caddyfile
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
mark_done "caddyfile"
|
||||||
|
|
||||||
|
# ── Step 4: Build ──
|
||||||
|
step 4 "Building Docker image"
|
||||||
|
|
||||||
|
# Check if image exists and source hasn't changed
|
||||||
|
IMAGE_EXISTS=$(docker images -q "$IMAGE_NAME" 2>/dev/null)
|
||||||
|
if [ -n "$IMAGE_EXISTS" ] && is_done "build"; then
|
||||||
|
log "Image already built."
|
||||||
|
if confirm "Rebuild? (only needed if you updated the code)"; then
|
||||||
|
docker build -t "$IMAGE_NAME" .
|
||||||
|
log "Image rebuilt."
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
log "caddy-config/Caddyfile exists."
|
info "This takes 1-2 minutes the first time..."
|
||||||
|
docker build -t "$IMAGE_NAME" .
|
||||||
|
log "Image built."
|
||||||
fi
|
fi
|
||||||
|
mark_done "build"
|
||||||
|
|
||||||
# Build
|
# ── Step 5: Start container ──
|
||||||
echo ""
|
step 5 "Starting container"
|
||||||
info "Building Docker image..."
|
|
||||||
docker build -t "$IMAGE_NAME" .
|
|
||||||
log "Image built."
|
|
||||||
|
|
||||||
# Run
|
if docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
|
||||||
echo ""
|
log "Container already running."
|
||||||
info "Starting container..."
|
elif docker ps -a --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
|
||||||
docker run -d \
|
# Exists but stopped — check if it needs recreating (new image)
|
||||||
--name "$CONTAINER_NAME" \
|
info "Container exists but is stopped. Starting..."
|
||||||
--restart unless-stopped \
|
docker start "$CONTAINER_NAME"
|
||||||
-p 80:80 \
|
log "Started."
|
||||||
-p 443:443 \
|
else
|
||||||
-v "$(pwd)/config.json:/app/config.json:ro" \
|
# Determine ports
|
||||||
-v "$(pwd)/caddy-config/Caddyfile:/etc/caddy/Caddyfile:ro" \
|
PORTS="-p 80:80 -p 443:443"
|
||||||
-v "${DATA_VOLUME}:/app/data" \
|
CADDYFILE_DOMAIN=$(grep -v '^#' caddy-config/Caddyfile 2>/dev/null | head -1 | tr -d ' {')
|
||||||
-v "${CADDY_VOLUME}:/data/caddy" \
|
if [ "$CADDYFILE_DOMAIN" = ":80" ]; then
|
||||||
"$IMAGE_NAME"
|
PORTS="-p 80:80"
|
||||||
log "Container started."
|
fi
|
||||||
|
|
||||||
|
docker run -d \
|
||||||
|
--name "$CONTAINER_NAME" \
|
||||||
|
--restart unless-stopped \
|
||||||
|
$PORTS \
|
||||||
|
-v "$(pwd)/config.json:/app/config.json:ro" \
|
||||||
|
-v "$(pwd)/caddy-config/Caddyfile:/etc/caddy/Caddyfile:ro" \
|
||||||
|
-v "${DATA_VOLUME}:/app/data" \
|
||||||
|
-v "${CADDY_VOLUME}:/data/caddy" \
|
||||||
|
"$IMAGE_NAME"
|
||||||
|
log "Container started."
|
||||||
|
fi
|
||||||
|
mark_done "container"
|
||||||
|
|
||||||
|
# ── Step 6: Verify ──
|
||||||
|
step 6 "Verifying"
|
||||||
|
|
||||||
# Wait and check
|
|
||||||
echo ""
|
|
||||||
info "Waiting for startup..."
|
info "Waiting for startup..."
|
||||||
sleep 5
|
sleep 5
|
||||||
|
|
||||||
if docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
|
if docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
|
||||||
log "Container is running."
|
# Check if Node.js is responding
|
||||||
echo ""
|
HEALTHY=false
|
||||||
DOMAIN_LINE=$(grep -v '^#' caddy-config/Caddyfile 2>/dev/null | head -1 | tr -d ' {')
|
for i in 1 2 3; do
|
||||||
if [ "$DOMAIN_LINE" = ":80" ]; then
|
if docker exec "$CONTAINER_NAME" wget -qO- http://localhost:3000/api/stats &>/dev/null; then
|
||||||
echo " Open http://$(curl -s ifconfig.me 2>/dev/null || echo 'your-server-ip')"
|
HEALTHY=true
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
|
||||||
|
if $HEALTHY; then
|
||||||
|
log "All services running."
|
||||||
else
|
else
|
||||||
echo " Open https://${DOMAIN_LINE}"
|
warn "Container is running but Node.js hasn't responded yet."
|
||||||
|
warn "Check logs: ./manage.sh logs"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "═══════════════════════════════════════"
|
||||||
|
echo " Setup complete!"
|
||||||
|
echo "═══════════════════════════════════════"
|
||||||
|
echo ""
|
||||||
|
if [ "$CADDYFILE_DOMAIN" != ":80" ] && [ -n "$CADDYFILE_DOMAIN" ]; then
|
||||||
|
echo " 🌐 https://${CADDYFILE_DOMAIN}"
|
||||||
|
else
|
||||||
|
MY_IP=$(curl -s -4 ifconfig.me 2>/dev/null || echo "your-server-ip")
|
||||||
|
echo " 🌐 http://${MY_IP}"
|
||||||
fi
|
fi
|
||||||
echo ""
|
echo ""
|
||||||
echo " Logs: ./manage.sh logs"
|
echo " Next steps:"
|
||||||
echo " Status: ./manage.sh status"
|
echo " • Connect an observer to start receiving packets"
|
||||||
echo " Stop: ./manage.sh stop"
|
echo " • Customize branding in config.json"
|
||||||
|
echo " • Set up backups: ./manage.sh backup"
|
||||||
|
echo ""
|
||||||
|
echo " Useful commands:"
|
||||||
|
echo " ./manage.sh status Check health"
|
||||||
|
echo " ./manage.sh logs View logs"
|
||||||
|
echo " ./manage.sh backup Backup database"
|
||||||
|
echo " ./manage.sh update Update to latest version"
|
||||||
echo ""
|
echo ""
|
||||||
else
|
else
|
||||||
err "Container failed to start. Check logs:"
|
err "Container failed to start."
|
||||||
docker logs "$CONTAINER_NAME" 2>&1 | tail -20
|
echo ""
|
||||||
|
echo " Check what went wrong:"
|
||||||
|
echo " docker logs ${CONTAINER_NAME}"
|
||||||
|
echo ""
|
||||||
|
echo " Common fixes:"
|
||||||
|
echo " • Invalid config.json — check JSON syntax"
|
||||||
|
echo " • Port conflict — stop other web servers"
|
||||||
|
echo " • Re-run: ./manage.sh setup"
|
||||||
|
echo ""
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
mark_done "verify"
|
||||||
}
|
}
|
||||||
|
|
||||||
# ─── Start / Stop / Restart ──────────────────────────────────────────────
|
# ─── Start / Stop / Restart ──────────────────────────────────────────────
|
||||||
@@ -167,26 +333,60 @@ cmd_stop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cmd_restart() {
|
cmd_restart() {
|
||||||
docker restart "$CONTAINER_NAME" 2>/dev/null && log "Restarted." || err "Failed."
|
docker restart "$CONTAINER_NAME" 2>/dev/null && log "Restarted." || err "Not running. Use './manage.sh start'."
|
||||||
}
|
}
|
||||||
|
|
||||||
# ─── Status ───────────────────────────────────────────────────────────────
|
# ─── Status ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
cmd_status() {
|
cmd_status() {
|
||||||
|
echo ""
|
||||||
if docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
|
if docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
|
||||||
log "Container is running."
|
log "Container is running."
|
||||||
echo ""
|
echo ""
|
||||||
docker ps --filter "name=${CONTAINER_NAME}" --format "table {{.Status}}\t{{.Ports}}"
|
docker ps --filter "name=${CONTAINER_NAME}" --format " Status: {{.Status}}"
|
||||||
|
docker ps --filter "name=${CONTAINER_NAME}" --format " Ports: {{.Ports}}"
|
||||||
echo ""
|
echo ""
|
||||||
# Quick health check
|
|
||||||
info "Checking services..."
|
info "Service health:"
|
||||||
docker exec "$CONTAINER_NAME" sh -c '
|
# Node.js
|
||||||
printf " Node.js: "; wget -qO- http://localhost:3000/api/stats 2>/dev/null | head -c 80 && echo " ✓" || echo "✗ not responding"
|
if docker exec "$CONTAINER_NAME" wget -qO /dev/null http://localhost:3000/api/stats 2>/dev/null; then
|
||||||
printf " Mosquitto: "; mosquitto_sub -h localhost -t "test" -C 0 -W 1 2>/dev/null && echo "✓ running" || echo "✓ running (no messages)"
|
STATS=$(docker exec "$CONTAINER_NAME" wget -qO- http://localhost:3000/api/stats 2>/dev/null)
|
||||||
' 2>/dev/null || true
|
PACKETS=$(echo "$STATS" | grep -oP '"totalPackets":\K[0-9]+' 2>/dev/null || echo "?")
|
||||||
|
NODES=$(echo "$STATS" | grep -oP '"totalNodes":\K[0-9]+' 2>/dev/null || echo "?")
|
||||||
|
log " Node.js — ${PACKETS} packets, ${NODES} nodes"
|
||||||
|
else
|
||||||
|
err " Node.js — not responding"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Mosquitto
|
||||||
|
if docker exec "$CONTAINER_NAME" pgrep mosquitto &>/dev/null; then
|
||||||
|
log " Mosquitto — running"
|
||||||
|
else
|
||||||
|
err " Mosquitto — not running"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Caddy
|
||||||
|
if docker exec "$CONTAINER_NAME" pgrep caddy &>/dev/null; then
|
||||||
|
log " Caddy — running"
|
||||||
|
else
|
||||||
|
err " Caddy — not running"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Disk usage
|
||||||
|
DB_SIZE=$(docker exec "$CONTAINER_NAME" du -h /app/data/meshcore.db 2>/dev/null | cut -f1)
|
||||||
|
if [ -n "$DB_SIZE" ]; then
|
||||||
|
echo ""
|
||||||
|
info "Database size: ${DB_SIZE}"
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
err "Container is not running."
|
err "Container is not running."
|
||||||
|
if docker ps -a --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
|
||||||
|
echo " Start with: ./manage.sh start"
|
||||||
|
else
|
||||||
|
echo " Set up with: ./manage.sh setup"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
echo ""
|
||||||
}
|
}
|
||||||
|
|
||||||
# ─── Logs ─────────────────────────────────────────────────────────────────
|
# ─── Logs ─────────────────────────────────────────────────────────────────
|
||||||
@@ -204,16 +404,21 @@ cmd_update() {
|
|||||||
info "Rebuilding image..."
|
info "Rebuilding image..."
|
||||||
docker build -t "$IMAGE_NAME" .
|
docker build -t "$IMAGE_NAME" .
|
||||||
|
|
||||||
info "Restarting container with new image..."
|
# Capture the run config before removing
|
||||||
|
CADDYFILE_DOMAIN=$(grep -v '^#' caddy-config/Caddyfile 2>/dev/null | head -1 | tr -d ' {')
|
||||||
|
PORTS="-p 80:80 -p 443:443"
|
||||||
|
if [ "$CADDYFILE_DOMAIN" = ":80" ]; then
|
||||||
|
PORTS="-p 80:80"
|
||||||
|
fi
|
||||||
|
|
||||||
|
info "Restarting with new image..."
|
||||||
docker stop "$CONTAINER_NAME" 2>/dev/null || true
|
docker stop "$CONTAINER_NAME" 2>/dev/null || true
|
||||||
docker rm "$CONTAINER_NAME" 2>/dev/null || true
|
docker rm "$CONTAINER_NAME" 2>/dev/null || true
|
||||||
|
|
||||||
# Re-read the run flags
|
|
||||||
docker run -d \
|
docker run -d \
|
||||||
--name "$CONTAINER_NAME" \
|
--name "$CONTAINER_NAME" \
|
||||||
--restart unless-stopped \
|
--restart unless-stopped \
|
||||||
-p 80:80 \
|
$PORTS \
|
||||||
-p 443:443 \
|
|
||||||
-v "$(pwd)/config.json:/app/config.json:ro" \
|
-v "$(pwd)/config.json:/app/config.json:ro" \
|
||||||
-v "$(pwd)/caddy-config/Caddyfile:/etc/caddy/Caddyfile:ro" \
|
-v "$(pwd)/caddy-config/Caddyfile:/etc/caddy/Caddyfile:ro" \
|
||||||
-v "${DATA_VOLUME}:/app/data" \
|
-v "${DATA_VOLUME}:/app/data" \
|
||||||
@@ -226,21 +431,26 @@ cmd_update() {
|
|||||||
# ─── Backup ───────────────────────────────────────────────────────────────
|
# ─── Backup ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
cmd_backup() {
|
cmd_backup() {
|
||||||
DEST="${1:-./meshcore-backup-$(date +%Y%m%d-%H%M%S).db}"
|
DEST="${1:-./backups/meshcore-$(date +%Y%m%d-%H%M%S).db}"
|
||||||
DB_PATH=$(docker volume inspect "$DATA_VOLUME" --format '{{ .Mountpoint }}')/meshcore.db
|
mkdir -p "$(dirname "$DEST")"
|
||||||
|
|
||||||
if [ ! -f "$DB_PATH" ]; then
|
# Try volume path first, fall back to docker cp
|
||||||
# Fallback: try docker cp
|
DB_PATH=$(docker volume inspect "$DATA_VOLUME" --format '{{ .Mountpoint }}' 2>/dev/null)/meshcore.db
|
||||||
|
|
||||||
|
if [ -f "$DB_PATH" ]; then
|
||||||
|
cp "$DB_PATH" "$DEST"
|
||||||
|
elif docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
|
||||||
docker cp "${CONTAINER_NAME}:/app/data/meshcore.db" "$DEST" 2>/dev/null
|
docker cp "${CONTAINER_NAME}:/app/data/meshcore.db" "$DEST" 2>/dev/null
|
||||||
else
|
else
|
||||||
cp "$DB_PATH" "$DEST"
|
err "Can't find database. Is the container running?"
|
||||||
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -f "$DEST" ]; then
|
if [ -f "$DEST" ]; then
|
||||||
SIZE=$(du -h "$DEST" | cut -f1)
|
SIZE=$(du -h "$DEST" | cut -f1)
|
||||||
log "Backed up to ${DEST} (${SIZE})"
|
log "Backed up to ${DEST} (${SIZE})"
|
||||||
else
|
else
|
||||||
err "Backup failed — database not found."
|
err "Backup failed."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
@@ -250,6 +460,12 @@ cmd_backup() {
|
|||||||
cmd_restore() {
|
cmd_restore() {
|
||||||
if [ -z "$1" ]; then
|
if [ -z "$1" ]; then
|
||||||
err "Usage: ./manage.sh restore <backup-file.db>"
|
err "Usage: ./manage.sh restore <backup-file.db>"
|
||||||
|
# List available backups
|
||||||
|
if [ -d "./backups" ]; then
|
||||||
|
echo ""
|
||||||
|
echo " Available backups:"
|
||||||
|
ls -lh ./backups/*.db 2>/dev/null | awk '{print " " $NF " (" $5 ")"}'
|
||||||
|
fi
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
if [ ! -f "$1" ]; then
|
if [ ! -f "$1" ]; then
|
||||||
@@ -257,18 +473,20 @@ cmd_restore() {
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
warn "This will replace the current database with $1"
|
warn "This will stop the analyzer, replace the database with $1, and restart."
|
||||||
read -p " Continue? [y/N] " -n 1 -r
|
if ! confirm "Continue?"; then
|
||||||
echo
|
echo " Aborted."
|
||||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
|
||||||
echo "Aborted."
|
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Backup current DB first
|
||||||
|
info "Backing up current database first..."
|
||||||
|
cmd_backup "./backups/meshcore-pre-restore-$(date +%Y%m%d-%H%M%S).db"
|
||||||
|
|
||||||
docker stop "$CONTAINER_NAME" 2>/dev/null || true
|
docker stop "$CONTAINER_NAME" 2>/dev/null || true
|
||||||
|
|
||||||
DB_PATH=$(docker volume inspect "$DATA_VOLUME" --format '{{ .Mountpoint }}')/meshcore.db
|
DB_PATH=$(docker volume inspect "$DATA_VOLUME" --format '{{ .Mountpoint }}' 2>/dev/null)/meshcore.db
|
||||||
if [ -d "$(dirname "$DB_PATH")" ]; then
|
if [ -f "$DB_PATH" ] || [ -d "$(dirname "$DB_PATH")" ]; then
|
||||||
cp "$1" "$DB_PATH"
|
cp "$1" "$DB_PATH"
|
||||||
else
|
else
|
||||||
docker cp "$1" "${CONTAINER_NAME}:/app/data/meshcore.db"
|
docker cp "$1" "${CONTAINER_NAME}:/app/data/meshcore.db"
|
||||||
@@ -281,13 +499,44 @@ cmd_restore() {
|
|||||||
# ─── MQTT Test ────────────────────────────────────────────────────────────
|
# ─── MQTT Test ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
cmd_mqtt_test() {
|
cmd_mqtt_test() {
|
||||||
info "Listening for MQTT messages (10 second timeout)..."
|
if ! docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
|
||||||
docker exec "$CONTAINER_NAME" mosquitto_sub -h localhost -t 'meshcore/#' -C 1 -W 10 2>/dev/null
|
err "Container not running. Start with: ./manage.sh start"
|
||||||
if [ $? -eq 0 ]; then
|
exit 1
|
||||||
log "MQTT is receiving data."
|
|
||||||
else
|
|
||||||
warn "No MQTT messages received in 10 seconds. Is an observer connected?"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
info "Listening for MQTT messages (10 second timeout)..."
|
||||||
|
MSG=$(docker exec "$CONTAINER_NAME" mosquitto_sub -h localhost -t 'meshcore/#' -C 1 -W 10 2>/dev/null)
|
||||||
|
if [ -n "$MSG" ]; then
|
||||||
|
log "Received MQTT message:"
|
||||||
|
echo " $MSG" | head -c 200
|
||||||
|
echo ""
|
||||||
|
else
|
||||||
|
warn "No messages received in 10 seconds."
|
||||||
|
echo ""
|
||||||
|
echo " This means no observer is publishing packets."
|
||||||
|
echo " See the deployment guide for connecting observers."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─── Reset ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
cmd_reset() {
|
||||||
|
echo ""
|
||||||
|
warn "This will remove the container, image, and setup state."
|
||||||
|
warn "Your config.json, Caddyfile, and data volume are NOT deleted."
|
||||||
|
echo ""
|
||||||
|
if ! confirm "Continue?"; then
|
||||||
|
echo " Aborted."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
docker stop "$CONTAINER_NAME" 2>/dev/null || true
|
||||||
|
docker rm "$CONTAINER_NAME" 2>/dev/null || true
|
||||||
|
docker rmi "$IMAGE_NAME" 2>/dev/null || true
|
||||||
|
rm -f "$STATE_FILE"
|
||||||
|
|
||||||
|
log "Reset complete. Run './manage.sh setup' to start over."
|
||||||
|
echo " Data volume preserved. To delete it: docker volume rm ${DATA_VOLUME}"
|
||||||
}
|
}
|
||||||
|
|
||||||
# ─── Help ─────────────────────────────────────────────────────────────────
|
# ─── Help ─────────────────────────────────────────────────────────────────
|
||||||
@@ -298,18 +547,22 @@ cmd_help() {
|
|||||||
echo ""
|
echo ""
|
||||||
echo "Usage: ./manage.sh <command>"
|
echo "Usage: ./manage.sh <command>"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Commands:"
|
echo " ${BOLD}Setup${NC}"
|
||||||
echo " setup Interactive first-time setup (config, build, run)"
|
echo " setup First-time setup wizard (safe to re-run)"
|
||||||
echo " start Start the container"
|
echo " reset Remove container + image (keeps data + config)"
|
||||||
echo " stop Stop the container"
|
echo ""
|
||||||
echo " restart Restart the container"
|
echo " ${BOLD}Run${NC}"
|
||||||
echo " status Show container status and service health"
|
echo " start Start the container"
|
||||||
echo " logs [N] Follow logs (last N lines, default 100)"
|
echo " stop Stop the container"
|
||||||
echo " update Pull latest code, rebuild, restart (preserves data)"
|
echo " restart Restart the container"
|
||||||
echo " backup [f] Backup database to file (default: timestamped)"
|
echo " status Show health, stats, and service status"
|
||||||
echo " restore <f> Restore database from backup file"
|
echo " logs [N] Follow logs (last N lines, default 100)"
|
||||||
echo " mqtt-test Listen for MQTT messages (10s timeout)"
|
echo ""
|
||||||
echo " help Show this help"
|
echo " ${BOLD}Maintain${NC}"
|
||||||
|
echo " update Pull latest code, rebuild, restart (keeps data)"
|
||||||
|
echo " backup [f] Backup database (default: ./backups/timestamped)"
|
||||||
|
echo " restore <f> Restore database from backup (backs up current first)"
|
||||||
|
echo " mqtt-test Check if MQTT data is flowing"
|
||||||
echo ""
|
echo ""
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,5 +579,6 @@ case "${1:-help}" in
|
|||||||
backup) cmd_backup "$2" ;;
|
backup) cmd_backup "$2" ;;
|
||||||
restore) cmd_restore "$2" ;;
|
restore) cmd_restore "$2" ;;
|
||||||
mqtt-test) cmd_mqtt_test ;;
|
mqtt-test) cmd_mqtt_test ;;
|
||||||
|
reset) cmd_reset ;;
|
||||||
help|*) cmd_help ;;
|
help|*) cmd_help ;;
|
||||||
esac
|
esac
|
||||||
Reference in new issue
Block a user