diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..d2ade7f --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +# Telegram notifications (used by backup.sh and notify.sh) +TELEGRAM_BOT_TOKEN= +TELEGRAM_CHAT_ID= diff --git a/backup.sh b/backup.sh old mode 100644 new mode 100755 index e1f8f87..5f17976 --- a/backup.sh +++ b/backup.sh @@ -1,63 +1,73 @@ #!/bin/bash -# Configuration +# NAS Backup Script — runs daily via cron +# Crontab: 0 3 * * * /volume1/docker/nas-tools/backup.sh >> /volume1/backups/nas-tools/backup.log 2>&1 + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" BACKUP_DIR="/volume1/backups/nas-tools" DATE=$(date +%Y%m%d_%H%M%S) RETENTION_DAYS=7 +ERRORS=0 + +[ -f "$SCRIPT_DIR/.env" ] && source "$SCRIPT_DIR/.env" -# Ensure backup directory exists mkdir -p "$BACKUP_DIR" -log() { - echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" +log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"; } + +notify() { + if [ -n "$TELEGRAM_BOT_TOKEN" ] && [ -n "$TELEGRAM_CHAT_ID" ]; then + curl -s -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \ + -d chat_id="$TELEGRAM_CHAT_ID" -d text="$1" -d parse_mode="Markdown" > /dev/null + fi } -log "Starting backup..." +run_backup() { + local desc="$1"; shift + log "Backing up $desc..." + if "$@"; then + log "$desc OK" + else + log "FAILED: $desc" + ERRORS=$((ERRORS + 1)) + fi +} -# 1. Database Dumps -log "Dumping databases..." +log "=== Backup started ===" -# Gitea DB -docker exec gitea-db pg_dump -U gitea gitea > "$BACKUP_DIR/gitea_db_$DATE.sql" -if [ $? -eq 0 ]; then - log "Gitea DB dumped successfully." -else - log "Error dumping Gitea DB!" +# Database dumps +run_backup "Gitea DB" docker exec gitea-db pg_dump -U gitea -f /tmp/gitea_dump.sql gitea +docker cp gitea-db:/tmp/gitea_dump.sql "$BACKUP_DIR/gitea_db_$DATE.sql" 2>/dev/null + +run_backup "Immich DB" docker exec immich-db pg_dump -U postgres -f /tmp/immich_dump.sql immich +docker cp immich-db:/tmp/immich_dump.sql "$BACKUP_DIR/immich_db_$DATE.sql" 2>/dev/null + +# Config backups +CONFIGS=() +for p in \ + /volume1/docker/nas-dashboard/.env \ + /volume1/docker/nas-dashboard/auth.json \ + /volume1/docker/gitea/conf \ + /volume1/docker/immich/.env \ + /volume1/docker/navidrome/data/navidrome.db \ + /volume1/docker/openclaw/data; do + [ -e "$p" ] && CONFIGS+=("$p") +done + +if [ ${#CONFIGS[@]} -gt 0 ]; then + run_backup "Configs" tar -czf "$BACKUP_DIR/configs_$DATE.tar.gz" "${CONFIGS[@]}" fi -# Immich DB -docker exec immich-db pg_dump -U postgres immich > "$BACKUP_DIR/immich_db_$DATE.sql" -if [ $? -eq 0 ]; then - log "Immich DB dumped successfully." +# Cleanup old backups +log "Cleaning backups older than $RETENTION_DAYS days..." +find "$BACKUP_DIR" -type f \( -name "*.sql" -o -name "*.tar.gz" \) -mtime +$RETENTION_DAYS -delete + +# Summary +log "=== Backup finished ($ERRORS errors) ===" + +if [ $ERRORS -gt 0 ]; then + notify "🔴 *NAS Backup FAILED* ($DATE) +$ERRORS error(s) — check logs" else - log "Error dumping Immich DB!" + notify "🟢 *NAS Backup OK* ($DATE)" fi - -# 2. File Backups (Configs & Data) -# Exclude heavy media files if handled separately, but for now backing up key configs -log "Compressing configuration files..." - -# Dashboard & Gitea & Immich configs -# Assuming /volume1/docker structure -tar -czf "$BACKUP_DIR/configs_$DATE.tar.gz" \ - -C /volume1/docker \ - nas-dashboard/backend/config.py \ - nas-dashboard/.env \ - gitea/conf \ - immich/.env \ - --exclude='gitea/data' \ - --exclude='immich/upload' \ - --exclude='immich/thumbs' - -if [ $? -eq 0 ]; then - log "Configs compressed successfully." -else - log "Error compressing configs!" -fi - -# 3. Cleanup Old Backups -log "Cleaning up backups older than $RETENTION_DAYS days..." -find "$BACKUP_DIR" -type f -name "*_db_*.sql" -mtime +$RETENTION_DAYS -delete -find "$BACKUP_DIR" -type f -name "configs_*.tar.gz" -mtime +$RETENTION_DAYS -delete - -log "Backup completed." diff --git a/dashboard/.env.example b/dashboard/.env.example index 8f807ca..464d65a 100644 --- a/dashboard/.env.example +++ b/dashboard/.env.example @@ -1 +1,3 @@ GITEA_TOKEN=your_gitea_api_token_here +TELEGRAM_BOT_TOKEN= +TELEGRAM_CHAT_ID= diff --git a/dashboard/backend/routers/system.py b/dashboard/backend/routers/system.py index 408dabf..ec193af 100644 --- a/dashboard/backend/routers/system.py +++ b/dashboard/backend/routers/system.py @@ -1,11 +1,30 @@ +import os import shutil import psutil import platform import time +import httpx from fastapi import APIRouter router = APIRouter() +TELEGRAM_BOT_TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN", "") +TELEGRAM_CHAT_ID = os.environ.get("TELEGRAM_CHAT_ID", "") + + +@router.post("/notify") +async def send_notification(body: dict): + """Send a Telegram notification. Body: {"message": "text"}""" + msg = body.get("message", "") + if not msg or not TELEGRAM_BOT_TOKEN or not TELEGRAM_CHAT_ID: + return {"ok": False, "error": "missing message or Telegram config"} + async with httpx.AsyncClient() as client: + r = await client.post( + f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendMessage", + data={"chat_id": TELEGRAM_CHAT_ID, "text": msg, "parse_mode": "Markdown"}, + ) + return {"ok": r.status_code == 200} + @router.get("/stats") def system_stats(): diff --git a/dashboard/docker-compose.yml b/dashboard/docker-compose.yml index 5ecb4d6..f6b76ee 100644 --- a/dashboard/docker-compose.yml +++ b/dashboard/docker-compose.yml @@ -32,9 +32,16 @@ services: - VOLUME_ROOT=/volume1 - SSH_HOST=host.docker.internal - SSH_USER=zjgump + - TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN:-} + - TELEGRAM_CHAT_ID=${TELEGRAM_CHAT_ID:-} volumes: - /volume1:/volume1 - ./ssh/dashboard_terminal:/app/ssh/id_ed25519:ro + healthcheck: + test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:4000/api/system/stats')"] + interval: 30s + timeout: 5s + retries: 3 extra_hosts: - "host.docker.internal:host-gateway" networks: diff --git a/immich/docker-compose.yml b/immich/docker-compose.yml index 35f36d2..9e73c37 100644 --- a/immich/docker-compose.yml +++ b/immich/docker-compose.yml @@ -17,6 +17,11 @@ services: DB_DATABASE_NAME: ${DB_DATABASE_NAME} REDIS_HOSTNAME: immich-redis IMMICH_MACHINE_LEARNING_ENABLED: "false" + healthcheck: + test: ["CMD", "wget", "-q", "--spider", "http://localhost:2283/api/server/ping"] + interval: 30s + timeout: 5s + retries: 3 depends_on: - immich-redis - immich-db diff --git a/navidrome/docker-compose.yml b/navidrome/docker-compose.yml index 138290b..4b2c83c 100644 --- a/navidrome/docker-compose.yml +++ b/navidrome/docker-compose.yml @@ -9,6 +9,11 @@ services: ND_SCANSCHEDULE: 1h ND_LOGLEVEL: info ND_BASEURL: "" + healthcheck: + test: ["CMD", "wget", "-q", "--spider", "http://localhost:4533/ping"] + interval: 30s + timeout: 5s + retries: 3 volumes: - ./data:/data - /volume1/music:/music:ro diff --git a/notify.sh b/notify.sh new file mode 100755 index 0000000..676aad3 --- /dev/null +++ b/notify.sh @@ -0,0 +1,18 @@ +#!/bin/bash +# notify.sh — Send Telegram notification +# Usage: ./notify.sh "message text" +# Requires TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID env vars (or source from .env) + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +[ -f "$SCRIPT_DIR/.env" ] && source "$SCRIPT_DIR/.env" + +if [ -z "$TELEGRAM_BOT_TOKEN" ] || [ -z "$TELEGRAM_CHAT_ID" ]; then + echo "TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID required" + exit 1 +fi + +MSG="${1:-No message}" +curl -s -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \ + -d chat_id="$TELEGRAM_CHAT_ID" \ + -d text="$MSG" \ + -d parse_mode="Markdown" > /dev/null diff --git a/openclaw/.env.example b/openclaw/.env.example new file mode 100644 index 0000000..24c0198 --- /dev/null +++ b/openclaw/.env.example @@ -0,0 +1,3 @@ +OPENCLAW_GATEWAY_TOKEN=change-me-to-a-long-random-token +ANTHROPIC_API_KEY=sk-ant-... +TELEGRAM_BOT_TOKEN= diff --git a/openclaw/docker-compose.yml b/openclaw/docker-compose.yml new file mode 100644 index 0000000..f9dde60 --- /dev/null +++ b/openclaw/docker-compose.yml @@ -0,0 +1,36 @@ +services: + openclaw-gateway: + image: ghcr.io/openclaw/openclaw:latest + container_name: openclaw-gateway + restart: unless-stopped + ports: + - "3100:18789" + environment: + HOME: /home/node + TERM: xterm-256color + OPENCLAW_GATEWAY_TOKEN: ${OPENCLAW_GATEWAY_TOKEN} + ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY} + TELEGRAM_BOT_TOKEN: ${TELEGRAM_BOT_TOKEN:-} + volumes: + - ./data:/home/node/.openclaw + init: true + command: + [ + "node", + "dist/index.js", + "gateway", + "--bind", + "lan", + "--port", + "18789", + ] + healthcheck: + test: ["CMD", "node", "-e", "fetch('http://localhost:18789').then(r=>{process.exit(r.ok?0:1)}).catch(()=>process.exit(1))"] + interval: 30s + timeout: 5s + retries: 3 + logging: + driver: json-file + options: + max-size: "10m" + max-file: "3"