#!/usr/bin/env bash # ============================================================================= # Timmy node — status / diagnostics # Run this any time to get a full health snapshot. # Also machine-readable via: bash status.sh --json # ============================================================================= set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SECRETS_FILE="$SCRIPT_DIR/.node-secrets" LND_DIR="$HOME/.lnd" LOG_DIR="$HOME/Library/Logs/timmy-node" LNCLI="/opt/homebrew/bin/lncli" BCLI="/opt/homebrew/bin/bitcoin-cli" JSON_MODE=false [[ "${1:-}" == "--json" ]] && JSON_MODE=true [[ -f "$SECRETS_FILE" ]] && source "$SECRETS_FILE" RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; BOLD='\033[1m'; NC='\033[0m' section() { $JSON_MODE || echo -e "\n${BOLD}${CYAN}── $* ──${NC}"; } pass() { $JSON_MODE || echo -e " ${GREEN}✓${NC} $*"; } fail() { $JSON_MODE || echo -e " ${RED}✗${NC} $*"; } warn() { $JSON_MODE || echo -e " ${YELLOW}!${NC} $*"; } declare -A STATUS # ─── Bitcoin Core ──────────────────────────────────────────────────────────── section "Bitcoin Core" BTC_ARGS=() [[ -n "${RPC_USER:-}" ]] && BTC_ARGS+=(-rpcuser="$RPC_USER" -rpcpassword="$RPC_PASS") if ! pgrep -x bitcoind &>/dev/null; then fail "bitcoind process not running" STATUS[bitcoind]="down" else pass "bitcoind is running (PID: $(pgrep -x bitcoind))" STATUS[bitcoind]="running" if BTC_INFO=$("$BCLI" "${BTC_ARGS[@]}" getblockchaininfo 2>/dev/null); then CHAIN=$(echo "$BTC_INFO" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['chain'])") BLOCKS=$(echo "$BTC_INFO" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['blocks'])") HEADERS=$(echo "$BTC_INFO" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['headers'])") PROGRESS=$(echo "$BTC_INFO" | python3 -c "import sys,json; d=json.load(sys.stdin); print(f\"{d['verificationprogress']*100:.2f}%\")") PRUNED=$(echo "$BTC_INFO" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('pruned', False))") PRUNEHEIGHT=$(echo "$BTC_INFO" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('pruneheight', 'n/a'))") SIZE_ON_DISK=$(echo "$BTC_INFO" | python3 -c "import sys,json; d=json.load(sys.stdin); print(f\"{d.get('size_on_disk',0)/1e9:.1f} GB\")") pass "Chain: $CHAIN | Blocks: $BLOCKS / $HEADERS | Sync: $PROGRESS" pass "Pruned: $PRUNED (prune height: $PRUNEHEIGHT) | Disk: $SIZE_ON_DISK" STATUS[bitcoin_sync]="$PROGRESS" STATUS[bitcoin_blocks]="$BLOCKS" if MEMPOOL=$("$BCLI" "${BTC_ARGS[@]}" getmempoolinfo 2>/dev/null); then TXCOUNT=$(echo "$MEMPOOL" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['size'])") pass "Mempool: $TXCOUNT txs" fi NETINFO=$("$BCLI" "${BTC_ARGS[@]}" getnetworkinfo 2>/dev/null || echo "{}") PEERS=$(echo "$NETINFO" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('connections', '?'))") pass "Peers: $PEERS" STATUS[bitcoin_peers]="$PEERS" else warn "bitcoind is running but RPC not ready yet (starting up?)" STATUS[bitcoin_rpc]="not ready" fi fi # ─── LND ───────────────────────────────────────────────────────────────────── section "LND" if ! pgrep -x lnd &>/dev/null; then fail "lnd process not running" STATUS[lnd]="down" else pass "lnd is running (PID: $(pgrep -x lnd))" STATUS[lnd]="running" if LND_INFO=$("$LNCLI" --lnddir="$LND_DIR" getinfo 2>/dev/null); then LND_VERSION=$(echo "$LND_INFO" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['version'])" 2>/dev/null || echo "?") LND_ALIAS=$(echo "$LND_INFO" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['alias'])" 2>/dev/null || echo "?") LND_SYNCED=$(echo "$LND_INFO" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['synced_to_chain'])" 2>/dev/null || echo "?") LND_PEERS=$(echo "$LND_INFO" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['num_peers'])" 2>/dev/null || echo "?") LND_CHANS=$(echo "$LND_INFO" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['num_active_channels'])" 2>/dev/null || echo "?") LND_BLOCK=$(echo "$LND_INFO" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['block_height'])" 2>/dev/null || echo "?") pass "Version: $LND_VERSION | Alias: $LND_ALIAS" pass "Synced to chain: $LND_SYNCED | Block: $LND_BLOCK" pass "Peers: $LND_PEERS | Active channels: $LND_CHANS" STATUS[lnd_synced]="$LND_SYNCED" STATUS[lnd_channels]="$LND_CHANS" if BAL=$("$LNCLI" --lnddir="$LND_DIR" walletbalance 2>/dev/null); then CONFIRMED=$(echo "$BAL" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['confirmed_balance'])" 2>/dev/null || echo "?") UNCONFIRMED=$(echo "$BAL" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['unconfirmed_balance'])" 2>/dev/null || echo "?") pass "On-chain balance: ${CONFIRMED} sats confirmed, ${UNCONFIRMED} unconfirmed" STATUS[wallet_confirmed]="$CONFIRMED" fi if CHANBAL=$("$LNCLI" --lnddir="$LND_DIR" channelbalance 2>/dev/null); then LOCAL=$(echo "$CHANBAL" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['local_balance']['sat'])" 2>/dev/null || echo "?") REMOTE=$(echo "$CHANBAL" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['remote_balance']['sat'])" 2>/dev/null || echo "?") pass "Channel balance: ${LOCAL} sats local, ${REMOTE} sats remote" STATUS[channel_local]="$LOCAL" fi elif "$LNCLI" --lnddir="$LND_DIR" state 2>/dev/null | grep -q "WAITING_TO_START\|LOCKED"; then warn "LND wallet is LOCKED — run: lncli --lnddir=$LND_DIR unlock" STATUS[lnd_state]="locked" else warn "LND running but wallet not ready (still starting or needs creation)" STATUS[lnd_state]="not ready" fi fi # ─── LNbits ────────────────────────────────────────────────────────────────── section "LNbits" LNBITS_URL_LOCAL="http://127.0.0.1:5000" if ! pgrep -f "lnbits" &>/dev/null; then fail "lnbits process not running" STATUS[lnbits]="down" else pass "lnbits process is running" STATUS[lnbits]="running" if curl -sf "$LNBITS_URL_LOCAL/api/v1/health" &>/dev/null 2>&1; then pass "LNbits HTTP reachable at $LNBITS_URL_LOCAL" STATUS[lnbits_http]="ok" else warn "LNbits process is up but HTTP not responding yet (starting?)" STATUS[lnbits_http]="starting" fi fi # ─── Bore tunnel (if running) ───────────────────────────────────────────────── section "Bore tunnel (external access)" if BORE_PID=$(pgrep -f "bore local.*5000" 2>/dev/null); then BORE_CMD=$(ps -p "$BORE_PID" -o args= 2>/dev/null || echo "?") pass "bore tunnel active (PID $BORE_PID): $BORE_CMD" STATUS[bore]="active" else warn "No bore tunnel running. Timmy (Replit) cannot reach LNbits." echo " Start with: bash $SCRIPT_DIR/expose.sh" STATUS[bore]="down" fi # ─── Recent errors ──────────────────────────────────────────────────────────── section "Recent log errors (last 20 lines)" for svc in bitcoind lnd lnbits; do ERR_FILE="$LOG_DIR/$svc.err" if [[ -f "$ERR_FILE" ]] && [[ -s "$ERR_FILE" ]]; then ERRS=$(tail -5 "$ERR_FILE" | grep -iE "error|fatal|panic|crit" || true) if [[ -n "$ERRS" ]]; then fail "$svc recent errors:" echo "$ERRS" | sed 's/^/ /' STATUS["${svc}_errors"]="yes" else pass "$svc error log: clean" fi fi done # ─── JSON output ───────────────────────────────────────────────────────────── if $JSON_MODE; then echo "{" i=0; count=${#STATUS[@]} for k in "${!STATUS[@]}"; do i=$((i+1)) comma=$([[ $i -lt $count ]] && echo "," || echo "") echo " \"$k\": \"${STATUS[$k]}\"$comma" done echo "}" exit 0 fi # ─── Summary ───────────────────────────────────────────────────────────────── section "Summary" ALL_OK=true [[ "${STATUS[bitcoind]:-down}" != "running" ]] && { fail "bitcoind is down"; ALL_OK=false; } [[ "${STATUS[lnd]:-down}" != "running" ]] && { fail "lnd is down"; ALL_OK=false; } [[ "${STATUS[lnbits]:-down}" != "running" ]] && { fail "lnbits is down"; ALL_OK=false; } [[ "${STATUS[bore]:-down}" != "active" ]] && { warn "bore tunnel is down — Timmy can't reach LNbits"; } $ALL_OK && echo -e "\n ${GREEN}${BOLD}All core services are running.${NC}\n" || \ echo -e "\n ${RED}${BOLD}Some services need attention — see above.${NC}\n" echo " Run 'bash $SCRIPT_DIR/start.sh' to start stopped services." echo " Run 'bash $SCRIPT_DIR/expose.sh' to open the bore tunnel for Replit." echo ""