Files
Claude-Code-Monitor/deployments/scripts/db-backup.sh
T
nntrivi2001 57dc91585d feat: Claude Code Monitor — lanes, pipelines and a merged workspace
Internal SmartGift build of a Claude Code monitoring dashboard.

Lanes: a durable unit of parallel agent work, one per working directory,
tracked across session restarts. Managed lanes are git worktrees the
dashboard provisions and can reset or remove behind a three-check destroy
guard and a counted preflight; adopted lanes are directories you already
own and are never destroyable.

Pipelines: a lane moves through pipeline stages. A stage the agent declares
with evidence renders green; a stage inferred from the tool-event stream
renders dashed amber and never counts as done. Detection is forward-only
within a 30-minute window, and never writes the declared stage.

Workspace: one page at /run with a lane grid, the selected lane's pipeline,
and a full Claude console behind a disclosure.
2026-07-30 14:39:03 +07:00

299 lines
12 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# ─────────────────────────────────────────────────────────────────────────────
# db-backup.sh SQLite database backup for Claude Code Agent Monitor
#
# Usage:
# ./db-backup.sh --env production --output ./backups/
# ./db-backup.sh --env production --output ./backups/ --upload s3://bucket/path
# ./db-backup.sh --help
# ─────────────────────────────────────────────────────────────────────────────
# @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
set -euo pipefail
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly APP_NAME="agent-monitor"
readonly DB_PATH_IN_CONTAINER="/app/data"
# ── Colors & logging ───────────────────────────────────────────────────────
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m'
log() { echo -e "${CYAN}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $*"; }
info() { echo -e "${CYAN}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} ${BLUE}${NC} $*"; }
ok() { echo -e "${CYAN}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} ${GREEN}${NC} $*"; }
warn() { echo -e "${CYAN}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} ${YELLOW}${NC} $*" >&2; }
err() { echo -e "${CYAN}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} ${RED}${NC} $*" >&2; }
fatal() { err "$@"; exit 1; }
# ── Defaults ────────────────────────────────────────────────────────────────
ENVIRONMENT=""
OUTPUT_DIR=""
NAMESPACE=""
UPLOAD_DEST=""
POD_NAME=""
DB_FILENAME=""
COMPRESS=true
# ── Usage ───────────────────────────────────────────────────────────────────
usage() {
cat <<EOF
${BOLD}Usage:${NC}
$(basename "$0") --env <environment> --output <directory> [options]
${BOLD}Required:${NC}
--env, -e Environment: dev, staging, production
--output, -o Local directory for the backup file
${BOLD}Options:${NC}
--namespace, -n Kubernetes namespace (default: agent-monitor-<env>)
--pod Specific pod name to copy from (auto-detected if omitted)
--upload Upload backup to S3/GCS (e.g., s3://bucket/backups/)
--no-compress Skip gzip compression
--help, -h Show this help message
${BOLD}Examples:${NC}
$(basename "$0") --env production --output ./backups/
$(basename "$0") --env staging --output /tmp/backups --upload s3://my-bucket/db-backups/
EOF
exit 0
}
# ── Argument parsing ────────────────────────────────────────────────────────
parse_args() {
[[ $# -eq 0 ]] && usage
while [[ $# -gt 0 ]]; do
case "$1" in
--env|-e) ENVIRONMENT="$2"; shift 2 ;;
--output|-o) OUTPUT_DIR="$2"; shift 2 ;;
--namespace|-n) NAMESPACE="$2"; shift 2 ;;
--pod) POD_NAME="$2"; shift 2 ;;
--upload) UPLOAD_DEST="$2"; shift 2 ;;
--no-compress) COMPRESS=false; shift ;;
--help|-h) usage ;;
*) fatal "Unknown option: $1" ;;
esac
done
[[ -z "$ENVIRONMENT" ]] && fatal "Missing required argument: --env"
[[ -z "$OUTPUT_DIR" ]] && fatal "Missing required argument: --output"
[[ -z "$NAMESPACE" ]] && NAMESPACE="agent-monitor-${ENVIRONMENT}"
}
# ── Find target pod ────────────────────────────────────────────────────────
find_pod() {
if [[ -n "$POD_NAME" ]]; then
info "Using specified pod: ${POD_NAME}"
return
fi
info "Finding running pod in namespace '${NAMESPACE}'..."
POD_NAME=$(kubectl get pods -n "${NAMESPACE}" \
-l "app.kubernetes.io/name=${APP_NAME}" \
--field-selector=status.phase=Running \
-o jsonpath='{.items[0].metadata.name}' 2>/dev/null || echo "")
if [[ -z "$POD_NAME" ]]; then
fatal "No running pods found for ${APP_NAME} in ${NAMESPACE}"
fi
info "Selected pod: ${POD_NAME}"
}
# ── Create backup ──────────────────────────────────────────────────────────
create_backup() {
local timestamp
timestamp=$(date -u +%Y%m%d_%H%M%S)
DB_FILENAME="${APP_NAME}_${ENVIRONMENT}_${timestamp}.db"
# Create output directory
mkdir -p "${OUTPUT_DIR}"
info "Creating backup..."
# Use sqlite3 .backup inside the pod for a consistent snapshot
# This avoids copying a potentially locked/in-flight database
local remote_backup_path="/tmp/${DB_FILENAME}"
info "Running SQLite backup inside pod (consistent snapshot)..."
if kubectl exec "${POD_NAME}" -n "${NAMESPACE}" -- \
sh -c "
if command -v sqlite3 >/dev/null 2>&1; then
sqlite3 '${DB_PATH_IN_CONTAINER}/dashboard.db' '.backup ${remote_backup_path}'
else
cp '${DB_PATH_IN_CONTAINER}/dashboard.db' '${remote_backup_path}'
fi
" 2>/dev/null; then
ok "In-pod backup created at ${remote_backup_path}"
else
# Fallback: also copy WAL files if present
warn "sqlite3 not available in pod, falling back to file copy"
kubectl exec "${POD_NAME}" -n "${NAMESPACE}" -- \
sh -c "cp '${DB_PATH_IN_CONTAINER}/dashboard.db' '${remote_backup_path}'" \
|| fatal "Failed to copy database file"
# Try to also get WAL and SHM files
kubectl exec "${POD_NAME}" -n "${NAMESPACE}" -- \
sh -c "cp '${DB_PATH_IN_CONTAINER}/dashboard.db-wal' '${remote_backup_path}-wal' 2>/dev/null || true"
kubectl exec "${POD_NAME}" -n "${NAMESPACE}" -- \
sh -c "cp '${DB_PATH_IN_CONTAINER}/dashboard.db-shm' '${remote_backup_path}-shm' 2>/dev/null || true"
fi
# Copy backup from pod to local
info "Copying backup to local filesystem..."
kubectl cp "${NAMESPACE}/${POD_NAME}:${remote_backup_path}" "${OUTPUT_DIR}/${DB_FILENAME}" \
|| fatal "Failed to copy backup from pod"
# Copy WAL if it exists
kubectl cp "${NAMESPACE}/${POD_NAME}:${remote_backup_path}-wal" "${OUTPUT_DIR}/${DB_FILENAME}-wal" 2>/dev/null || true
# Cleanup remote temp file
kubectl exec "${POD_NAME}" -n "${NAMESPACE}" -- \
sh -c "rm -f '${remote_backup_path}' '${remote_backup_path}-wal' '${remote_backup_path}-shm'" 2>/dev/null || true
local file_size
file_size=$(du -sh "${OUTPUT_DIR}/${DB_FILENAME}" 2>/dev/null | awk '{print $1}')
ok "Backup saved: ${OUTPUT_DIR}/${DB_FILENAME} (${file_size})"
}
# ── Validate backup integrity ──────────────────────────────────────────────
validate_backup() {
info "Validating backup integrity..."
local db_file="${OUTPUT_DIR}/${DB_FILENAME}"
if ! command -v sqlite3 &>/dev/null; then
warn "sqlite3 not found locally skipping integrity check"
return
fi
# Check integrity
local integrity
integrity=$(sqlite3 "${db_file}" "PRAGMA integrity_check;" 2>/dev/null || echo "error")
if [[ "$integrity" == "ok" ]]; then
ok "SQLite integrity check: OK"
else
err "SQLite integrity check failed: ${integrity}"
warn "Backup may be corrupted consider re-running the backup"
return 1
fi
# Show basic stats
local table_count
table_count=$(sqlite3 "${db_file}" "SELECT count(*) FROM sqlite_master WHERE type='table';" 2>/dev/null || echo "?")
local page_count
page_count=$(sqlite3 "${db_file}" "PRAGMA page_count;" 2>/dev/null || echo "?")
local page_size
page_size=$(sqlite3 "${db_file}" "PRAGMA page_size;" 2>/dev/null || echo "?")
info "Database stats: ${table_count} tables, ${page_count} pages × ${page_size} bytes"
}
# ── Compress backup ────────────────────────────────────────────────────────
compress_backup() {
if [[ "$COMPRESS" == false ]]; then
info "Compression skipped"
return
fi
local db_file="${OUTPUT_DIR}/${DB_FILENAME}"
if ! command -v gzip &>/dev/null; then
warn "gzip not available skipping compression"
return
fi
info "Compressing backup..."
gzip -k "${db_file}"
DB_FILENAME="${DB_FILENAME}.gz"
local compressed_size
compressed_size=$(du -sh "${OUTPUT_DIR}/${DB_FILENAME}" 2>/dev/null | awk '{print $1}')
ok "Compressed: ${OUTPUT_DIR}/${DB_FILENAME} (${compressed_size})"
}
# ── Upload to cloud storage ────────────────────────────────────────────────
upload_backup() {
if [[ -z "$UPLOAD_DEST" ]]; then
return
fi
local backup_file="${OUTPUT_DIR}/${DB_FILENAME}"
info "Uploading backup to ${UPLOAD_DEST}..."
if [[ "$UPLOAD_DEST" == s3://* ]]; then
if ! command -v aws &>/dev/null; then
fatal "AWS CLI not found. Install it to upload to S3."
fi
aws s3 cp "${backup_file}" "${UPLOAD_DEST}${DB_FILENAME}" \
--storage-class STANDARD_IA \
|| fatal "S3 upload failed"
ok "Uploaded to ${UPLOAD_DEST}${DB_FILENAME}"
elif [[ "$UPLOAD_DEST" == gs://* ]]; then
if ! command -v gsutil &>/dev/null; then
fatal "gsutil not found. Install Google Cloud SDK to upload to GCS."
fi
gsutil cp "${backup_file}" "${UPLOAD_DEST}${DB_FILENAME}" \
|| fatal "GCS upload failed"
ok "Uploaded to ${UPLOAD_DEST}${DB_FILENAME}"
elif [[ "$UPLOAD_DEST" == az://* ]] || [[ "$UPLOAD_DEST" == https://*.blob.core.windows.net/* ]]; then
if ! command -v az &>/dev/null; then
fatal "Azure CLI not found. Install it to upload to Azure Blob."
fi
local container_url="${UPLOAD_DEST}"
az storage blob upload \
--file "${backup_file}" \
--name "${DB_FILENAME}" \
--overwrite \
|| fatal "Azure Blob upload failed"
ok "Uploaded to Azure Blob Storage"
else
warn "Unknown upload destination scheme: ${UPLOAD_DEST}"
warn "Supported: s3://, gs://, az://"
fi
}
# ── Main ────────────────────────────────────────────────────────────────────
main() {
echo ""
echo -e "${BOLD}${BLUE}╔══════════════════════════════════════════════════╗${NC}"
echo -e "${BOLD}${BLUE}║ Claude Code Agent Monitor DB Backup ║${NC}"
echo -e "${BOLD}${BLUE}╚══════════════════════════════════════════════════╝${NC}"
echo ""
parse_args "$@"
info "Configuration:"
echo -e " ${BOLD}Environment:${NC} ${ENVIRONMENT}"
echo -e " ${BOLD}Namespace:${NC} ${NAMESPACE}"
echo -e " ${BOLD}Output:${NC} ${OUTPUT_DIR}"
[[ -n "$UPLOAD_DEST" ]] && echo -e " ${BOLD}Upload:${NC} ${UPLOAD_DEST}"
echo ""
find_pod
create_backup
validate_backup
compress_backup
upload_backup
echo ""
ok "${BOLD}Backup complete!${NC}"
echo -e " ${BOLD}File:${NC} ${OUTPUT_DIR}/${DB_FILENAME}"
echo -e " ${BOLD}Timestamp:${NC} $(date -u +%Y-%m-%dT%H:%M:%SZ)"
echo ""
}
main "$@"