#!/usr/bin/env bash set -euo pipefail RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' CYAN='\033[0;36m' NC='\033[0m' ok() { echo -e " ${GREEN}[OK]${NC} $*"; } fail() { echo -e " ${RED}[FAIL]${NC} $*"; } warn() { echo -e " ${YELLOW}[WARN]${NC} $*"; } header() { echo -e "\n${CYAN}══════════════════════════════════════════${NC}"; echo -e "${CYAN} $*${NC}"; echo -e "${CYAN}══════════════════════════════════════════${NC}"; } SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" HUB_USER="${SUDO_USER:-${USER}}" if [ "$(id -u)" -eq 0 ] && [ -n "${SUDO_USER:-}" ]; then HUB_HOME=$(getent passwd "$SUDO_USER" | cut -d: -f6) else HUB_HOME="$HOME" fi if [ "$(id -u)" -eq 0 ] && [ "$HUB_HOME" = "/root" ]; then for u in armbian; do CANDIDATE=$(getent passwd "$u" 2>/dev/null | cut -d: -f6) if [ -f "${CANDIDATE}/.config/tinyboard/spokes" ]; then HUB_HOME="$CANDIDATE" break fi done fi REGISTRY="${HUB_HOME}/.config/tinyboard/spokes" COMPOSE="$SCRIPT_DIR/spoke/compose.yaml" RCLONE_CONF="${HUB_HOME}/.config/rclone/rclone.conf" IS_SPOKE=false IS_HUB=false if docker ps --format '{{.Names}}' 2>/dev/null | grep -qi autossh; then IS_SPOKE=true fi if [ -f "$REGISTRY" ]; then IS_HUB=true fi if [ "$IS_SPOKE" = false ] && [ "$IS_HUB" = false ]; then echo -e "${YELLOW}Could not detect hub or spoke configuration. Is tinyboard set up?${NC}" exit 1 fi check_disk() { header "Disk Space" while IFS= read -r line; do local pct mount pct=$(echo "$line" | awk '{print $5}' | tr -d '%') mount=$(echo "$line" | awk '{print $6}') if [ "$pct" -ge 90 ]; then fail "Disk usage at ${pct}% on $mount — critically low" elif [ "$pct" -ge 80 ]; then warn "Disk usage at ${pct}% on $mount — getting full" else ok "Disk usage at ${pct}% on $mount" fi done < <(df -h | awk 'NR>1' | grep -v ' /dev' | grep -v ' /sys' | grep -v ' /proc' | grep -v ' /run' | grep -v ' /snap' | grep -v 'overlay2' | grep -v 'docker') } check_common() { header "System" local ssh_svc="" if systemctl list-unit-files ssh.service >/dev/null 2>&1; then ssh_svc="ssh" elif systemctl list-unit-files sshd.service >/dev/null 2>&1; then ssh_svc="sshd" fi if [ -n "$ssh_svc" ]; then if systemctl is-active "$ssh_svc" >/dev/null 2>&1; then ok "SSH server running ($ssh_svc)" else fail "SSH server not running ($ssh_svc)" fi else warn "Could not detect SSH service" fi if [ "$IS_SPOKE" = true ]; then if command -v docker >/dev/null 2>&1; then ok "docker installed" else fail "docker not found" fi if docker info >/dev/null 2>&1; then ok "docker daemon running" else fail "docker daemon not running" fi local st_container st_container=$(docker ps --format '{{.Names}}' 2>/dev/null | grep -i syncthing | head -1 || true) if [ -n "$st_container" ]; then ok "Syncthing container running ($st_container)" if curl -sf http://127.0.0.1:8384 >/dev/null 2>&1; then ok "Syncthing API reachable" else warn "Syncthing container running but API not reachable on :8384" fi else warn "No Syncthing container running" fi fi } check_spoke() { header "Spoke" local autossh_container autossh_container=$(docker ps --format '{{.Names}}' 2>/dev/null | grep -i autossh | head -1 || true) if [ -n "$autossh_container" ]; then ok "autossh container running ($autossh_container)" local logs logs=$(docker logs "$autossh_container" 2>&1 | tail -20 || true) if echo "$logs" | grep -q "remote port forwarding failed"; then fail "Tunnel reports port forwarding failed — check hub authorized_keys" else ok "No tunnel errors in recent logs" fi else fail "No autossh container running" fi if [ -n "$autossh_container" ]; then local tunnel_port hub_host tunnel_port=$(docker inspect "$autossh_container" 2>/dev/null | python3 -c " import sys, json data = json.load(sys.stdin) cmd = ' '.join(data[0].get('Config', {}).get('Cmd', [])) import re m = re.search(r'-R (\d+):localhost', cmd) print(m.group(1) if m else '') " 2>/dev/null || true) hub_host=$(docker inspect "$autossh_container" 2>/dev/null | python3 -c " import sys, json data = json.load(sys.stdin) cmd = ' '.join(data[0].get('Config', {}).get('Cmd', [])) import re m = re.search(r'[a-zA-Z0-9._-]+@([a-zA-Z0-9._-]+\.[a-zA-Z0-9._-]+)', cmd) print(m.group(1) if m else '') " 2>/dev/null || true) if [ -n "$tunnel_port" ] && [ -n "$hub_host" ]; then ok "Tunnel configured: port $tunnel_port → $hub_host" else warn "Could not parse tunnel config from running container" fi fi local st_data="/home/armbian/st/data" if [ -d "$st_data" ]; then ok "Syncthing data directory exists ($st_data)" else warn "Syncthing data directory not found ($st_data)" fi } check_hub() { header "Hub" local spoke_count spoke_count=$(wc -l < "$REGISTRY" 2>/dev/null || echo 0) ok "$spoke_count spoke(s) in registry" while IFS= read -r line; do [ -n "$line" ] || continue local spoke_name tunnel_port key_path mount_point spoke_name=$(echo "$line" | awk '{print $1}') tunnel_port=$(echo "$line" | awk '{print $2}') key_path=$(echo "$line" | awk '{print $3}') mount_point=$(echo "$line" | awk '{print $4}') echo "" echo -e " ${CYAN}Spoke: $spoke_name${NC}" if ss -tlnp 2>/dev/null | grep -q ":${tunnel_port}"; then ok "Tunnel port $tunnel_port is listening" else fail "Tunnel port $tunnel_port not listening — is the spoke connected?" fi if [ -f "$key_path" ]; then ok "Hub key exists ($key_path)" else fail "Hub key missing ($key_path)" fi if mountpoint -q "$mount_point" 2>/dev/null; then ok "Mounted at $mount_point" else fail "Not mounted at $mount_point" fi if grep -q "\[${spoke_name}-remote\]" "$RCLONE_CONF" 2>/dev/null; then ok "rclone remote [${spoke_name}-remote] configured" else fail "rclone remote [${spoke_name}-remote] not found in rclone.conf" fi if crontab -u "$(basename "$HUB_HOME")" -l 2>/dev/null | grep -q "${spoke_name}-remote:"; then ok "Auto-mount crontab entry present" else warn "No auto-mount crontab entry for $spoke_name" fi done < "$REGISTRY" echo "" } check_common check_disk if [ "$IS_SPOKE" = true ]; then check_spoke fi if [ "$IS_HUB" = true ]; then check_hub fi echo ""