diff --git a/setup.sh b/setup.sh index c60ce58..463d17c 100644 --- a/setup.sh +++ b/setup.sh @@ -2,9 +2,23 @@ set -euo pipefail # ============================================================ -# LiveServer-M1 — Interactive Setup Script +# LiveServer-M1 — Universal Setup / Update / Doctor Script +# ============================================================ +# Usage: +# ./setup.sh — интерактивное меню +# ./setup.sh install — первоначальная установка +# ./setup.sh update — обновить проект (git pull + npm + prisma + rebuild) +# ./setup.sh doctor — диагностика и автоисправление +# ./setup.sh status — статус всех сервисов +# ./setup.sh logs [svc] — логи сервиса (по умолчанию app) +# ./setup.sh restart — перезапуск всех контейнеров +# ./setup.sh reset — полный сброс (с подтверждением) # ============================================================ +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" + +# --- Colors --- RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' @@ -12,21 +26,17 @@ BLUE='\033[0;34m' CYAN='\033[0;36m' NC='\033[0m' BOLD='\033[1m' +DIM='\033[2m' -print_banner() { - echo "" - echo -e "${CYAN}╔══════════════════════════════════════════════╗${NC}" - echo -e "${CYAN}║${NC} ${BOLD}LiveServer-M1${NC} — Setup Wizard ${CYAN}║${NC}" - echo -e "${CYAN}║${NC} Образовательная видеоконференц-платформа ${CYAN}║${NC}" - echo -e "${CYAN}╚══════════════════════════════════════════════╝${NC}" - echo "" -} - -log_step() { echo -e "\n${BLUE}[$(date +%H:%M:%S)]${NC} ${BOLD}$1${NC}"; } -log_ok() { echo -e " ${GREEN}✓${NC} $1"; } -log_warn() { echo -e " ${YELLOW}⚠${NC} $1"; } -log_err() { echo -e " ${RED}✗${NC} $1"; } +# --- Logging --- +log_step() { echo -e "\n${BLUE}[$(date +%H:%M:%S)]${NC} ${BOLD}$1${NC}"; } +log_ok() { echo -e " ${GREEN}✓${NC} $1"; } +log_warn() { echo -e " ${YELLOW}⚠${NC} $1"; } +log_err() { echo -e " ${RED}✗${NC} $1"; } +log_info() { echo -e " ${CYAN}→${NC} $1"; } +log_fix() { echo -e " ${GREEN}⚙${NC} ${BOLD}FIX:${NC} $1"; } +# --- Helpers --- ask() { local prompt="$1" default="${2:-}" var_name="$3" if [[ -n "$default" ]]; then @@ -65,76 +75,111 @@ generate_secret() { } generate_password() { - # 20 символов: буквы + цифры (безопасно для URL и CLI) openssl rand -base64 24 2>/dev/null | tr -d '=/+' | head -c 20 || \ head -c 24 /dev/urandom | base64 | tr -d '=/+' | head -c 20 } -check_command() { - if command -v "$1" &>/dev/null; then - log_ok "$1 найден: $(command -v "$1")" - return 0 - else - log_err "$1 не найден" - return 1 +# Прочитать переменную из .env +env_get() { + local key="$1" + if [[ -f .env ]]; then + grep -E "^${key}=" .env 2>/dev/null | head -1 | cut -d'=' -f2- | sed 's/^"//' | sed 's/"$//' fi } +# Определить режим (local/prod) из .env +detect_mode() { + local domain + domain=$(env_get DOMAIN) + if [[ -n "$domain" ]]; then + echo "prod" + else + echo "local" + fi +} + +# Docker compose wrapper (определяет prod/local) +dc() { + if [[ "$(detect_mode)" == "prod" ]]; then + docker compose -f docker-compose.yml -f docker-compose.prod.yml "$@" + else + docker compose "$@" + fi +} + +# Проверить что сервис запущен и healthy +check_service() { + local name="$1" + local state + state=$(docker compose ps --format '{{.State}}' "$name" 2>/dev/null || echo "missing") + local health + health=$(docker compose ps --format '{{.Health}}' "$name" 2>/dev/null || echo "") + + if [[ "$state" == "running" ]]; then + if [[ "$health" == "healthy" || -z "$health" ]]; then + echo "ok" + else + echo "unhealthy" + fi + elif [[ "$state" == "missing" || -z "$state" ]]; then + echo "stopped" + else + echo "$state" + fi +} + +# Ожидание сервиса с таймаутом +wait_for_service() { + local name="$1" check_cmd="$2" timeout="${3:-30}" + echo -e " Ожидание ${name}..." + for i in $(seq 1 "$timeout"); do + if eval "$check_cmd" &>/dev/null; then + log_ok "${name} готов" + return 0 + fi + sleep 1 + done + log_err "${name} не ответил за ${timeout}с" + return 1 +} + +check_command() { + command -v "$1" &>/dev/null +} + install_if_missing() { local cmd="$1" install_hint="$2" - if command -v "$cmd" &>/dev/null; then + if check_command "$cmd"; then log_ok "$cmd найден: $(command -v "$cmd")" return 0 fi log_warn "$cmd не найден" - # Попытка автоустановки через системные менеджеры if [[ "$cmd" == "node" || "$cmd" == "npm" || "$cmd" == "npx" ]]; then - if command -v nvm &>/dev/null; then + if check_command nvm; then echo -e " Установка Node.js через nvm..." nvm install --lts 2>&1 | tail -3 - if command -v "$cmd" &>/dev/null; then - log_ok "$cmd установлен через nvm" - return 0 - fi + check_command "$cmd" && { log_ok "$cmd установлен через nvm"; return 0; } fi fi - # Автоустановка через apt/yum/brew если доступно case "$cmd" in docker) - if command -v apt-get &>/dev/null; then - if ask_yn "Установить Docker автоматически (apt)?" "y"; then - echo -e " Установка Docker..." - curl -fsSL https://get.docker.com | sh 2>&1 | tail -5 - sudo usermod -aG docker "$USER" 2>/dev/null || true - if command -v docker &>/dev/null; then - log_ok "Docker установлен" - return 0 - fi - fi + if check_command apt-get && ask_yn "Установить Docker автоматически (apt)?" "y"; then + curl -fsSL https://get.docker.com | sh 2>&1 | tail -5 + sudo usermod -aG docker "$USER" 2>/dev/null || true + check_command docker && { log_ok "Docker установлен"; return 0; } fi ;; node|npm|npx) - if command -v apt-get &>/dev/null; then - if ask_yn "Установить Node.js 22 автоматически (apt)?" "y"; then - echo -e " Установка Node.js..." - curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - 2>&1 | tail -3 - sudo apt-get install -y nodejs 2>&1 | tail -3 - if command -v "$cmd" &>/dev/null; then - log_ok "$cmd установлен" - return 0 - fi - fi - elif command -v brew &>/dev/null; then - if ask_yn "Установить Node.js через Homebrew?" "y"; then - brew install node 2>&1 | tail -3 - if command -v "$cmd" &>/dev/null; then - log_ok "$cmd установлен" - return 0 - fi - fi + if check_command apt-get && ask_yn "Установить Node.js 22 автоматически?" "y"; then + curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - 2>&1 | tail -3 + sudo apt-get install -y nodejs 2>&1 | tail -3 + check_command "$cmd" && { log_ok "$cmd установлен"; return 0; } + elif check_command brew && ask_yn "Установить Node.js через Homebrew?" "y"; then + brew install node 2>&1 | tail -3 + check_command "$cmd" && { log_ok "$cmd установлен"; return 0; } fi ;; esac @@ -145,150 +190,717 @@ install_if_missing() { } # ============================================================ -# Main +# Banners # ============================================================ -print_banner - -# --- Step 1: Check & install dependencies --- -log_step "Шаг 1/6 — Проверка и установка зависимостей" - -MISSING=0 -install_if_missing docker "https://docs.docker.com/get-docker/" || MISSING=1 -check_command "docker compose" 2>/dev/null || check_command docker-compose 2>/dev/null || { - log_err "docker compose не найден (нужен Docker Compose V2)" - MISSING=1 -} -install_if_missing node "https://nodejs.org/" || MISSING=1 -install_if_missing npm "https://nodejs.org/" || MISSING=1 - -if [[ "$MISSING" -eq 1 ]]; then - log_err "Установи недостающие зависимости и запусти скрипт снова." - exit 1 -fi - -NODE_VER=$(node -v) -log_ok "Node.js: $NODE_VER" - -# --- Step 2: Choose mode --- -log_step "Шаг 2/6 — Режим запуска" - -echo "" -echo -e " ${BOLD}1)${NC} Локальная разработка (localhost, без SSL)" -echo -e " ${BOLD}2)${NC} Production (домен + Traefik + Let's Encrypt SSL)" -echo "" -ask "Выбери режим" "1" MODE - -DOMAIN="" -ACME_EMAIL="" - -if [[ "$MODE" == "2" ]]; then +print_banner() { echo "" - ask "Домен (например live.example.com)" "" DOMAIN - ask "Email для Let's Encrypt" "" ACME_EMAIL + echo -e "${CYAN}╔══════════════════════════════════════════════╗${NC}" + echo -e "${CYAN}║${NC} ${BOLD}LiveServer-M1${NC} — Setup & Doctor ${CYAN}║${NC}" + echo -e "${CYAN}║${NC} Образовательная видеоконференц-платформа ${CYAN}║${NC}" + echo -e "${CYAN}╚══════════════════════════════════════════════╝${NC}" + echo "" +} - if [[ -z "$DOMAIN" || -z "$ACME_EMAIL" ]]; then - log_err "Домен и email обязательны для production." +print_usage() { + echo -e " ${BOLD}Команды:${NC}" + echo -e " ${CYAN}install${NC} Первоначальная установка" + echo -e " ${CYAN}update${NC} Обновить проект (git pull + npm + prisma + rebuild)" + echo -e " ${CYAN}doctor${NC} Диагностика и автоисправление проблем" + echo -e " ${CYAN}status${NC} Статус всех сервисов" + echo -e " ${CYAN}logs${NC} [svc] Логи сервиса (по умолчанию: app)" + echo -e " ${CYAN}restart${NC} Перезапуск контейнеров" + echo -e " ${CYAN}reset${NC} Полный сброс (с подтверждением)" + echo "" +} + +# ============================================================ +# COMMAND: status +# ============================================================ + +cmd_status() { + log_step "Статус сервисов" + + local services=(postgres redis pgbouncer minio app ai-agent) + local all_ok=true + + printf " %-14s %-12s %-10s %s\n" "СЕРВИС" "СОСТОЯНИЕ" "ПОРТ" "ДЕТАЛИ" + printf " %-14s %-12s %-10s %s\n" "──────" "─────────" "────" "──────" + + for svc in "${services[@]}"; do + local state + state=$(check_service "$svc") + local port="" details="" + + case "$svc" in + postgres) port="5432" ;; + redis) port="6379" ;; + pgbouncer) port="6432" ;; + minio) port="9000/9001" ;; + app) port="3000" ;; + ai-agent) port="—" ;; + esac + + local color="$RED" + local icon="✗" + case "$state" in + ok) color="$GREEN"; icon="✓" ;; + unhealthy) color="$YELLOW"; icon="⚠"; all_ok=false ;; + *) color="$RED"; icon="✗"; all_ok=false ;; + esac + + # Доп. детали + if [[ "$state" == "ok" ]]; then + case "$svc" in + postgres) + local db_size + db_size=$(docker compose exec -T postgres psql -U postgres -d liveserver -t -c "SELECT pg_size_pretty(pg_database_size('liveserver'));" 2>/dev/null | tr -d ' ' || echo "?") + details="size: ${db_size}" + ;; + redis) + local keys + keys=$(docker compose exec -T redis redis-cli dbsize 2>/dev/null | grep -oP '\d+' || echo "?") + details="keys: ${keys}" + ;; + minio) + details="bucket: $(env_get S3_BUCKET)" + ;; + esac + fi + + printf " ${color}${icon}${NC} %-12s %-12s %-10s %s\n" "$svc" "$state" "$port" "$details" + done + + # .env check + echo "" + if [[ -f .env ]]; then + log_ok ".env существует" + local domain + domain=$(env_get DOMAIN) + if [[ -n "$domain" ]]; then + log_info "Режим: ${BOLD}Production${NC} (${domain})" + else + log_info "Режим: ${BOLD}Локальная разработка${NC}" + fi + else + log_err ".env не найден — запусти ${CYAN}./setup.sh install${NC}" + fi + + # node_modules check + if [[ -d "node_modules" ]]; then + log_ok "node_modules установлен" + else + log_warn "node_modules отсутствует" + fi + + # .next check + if [[ -d ".next" ]]; then + log_ok ".next кеш существует" + else + log_info ".next кеш отсутствует (нормально до первого запуска)" + fi + + if $all_ok; then + echo "" + log_ok "${GREEN}Все сервисы работают${NC}" + else + echo "" + log_warn "Есть проблемы — запусти ${CYAN}./setup.sh doctor${NC}" + fi +} + +# ============================================================ +# COMMAND: doctor +# ============================================================ + +cmd_doctor() { + log_step "Диагностика LiveServer-M1" + local issues=0 + local fixed=0 + + # --- 1. Системные зависимости --- + log_step "1/8 — Системные зависимости" + for cmd in docker node npm; do + if check_command "$cmd"; then + log_ok "$cmd: $(command -v "$cmd")" + else + log_err "$cmd не найден" + ((issues++)) + install_if_missing "$cmd" "см. документацию" && ((fixed++)) || true + fi + done + + if check_command docker; then + if docker compose version &>/dev/null; then + log_ok "docker compose: $(docker compose version --short 2>/dev/null)" + else + log_err "docker compose V2 не найден" + ((issues++)) + fi + + if docker info &>/dev/null; then + log_ok "Docker daemon запущен" + else + log_err "Docker daemon не запущен" + ((issues++)) + log_fix "Запускаю Docker..." + sudo systemctl start docker 2>/dev/null && ((fixed++)) || log_warn "Не удалось запустить Docker" + fi + fi + + # --- 2. .env --- + log_step "2/8 — Файл .env" + if [[ ! -f .env ]]; then + log_err ".env не найден" + ((issues++)) + if ask_yn "Запустить полную установку?" "y"; then + cmd_install + return + fi + else + log_ok ".env существует" + + # Проверить обязательные переменные + local required_vars=(DATABASE_URL POSTGRES_PASSWORD REDIS_URL BETTER_AUTH_SECRET S3_ENDPOINT S3_ACCESS_KEY S3_SECRET_KEY MINIO_ROOT_USER MINIO_ROOT_PASSWORD) + for var in "${required_vars[@]}"; do + local val + val=$(env_get "$var") + if [[ -z "$val" ]]; then + log_err "${var} не задан в .env" + ((issues++)) + fi + done + + # Проверить что пароли не дефолтные (для prod) + if [[ "$(detect_mode)" == "prod" ]]; then + local pg_pass + pg_pass=$(env_get POSTGRES_PASSWORD) + if [[ "$pg_pass" == "postgres" ]]; then + log_warn "POSTGRES_PASSWORD = 'postgres' в production — небезопасно!" + ((issues++)) + fi + local minio_pass + minio_pass=$(env_get MINIO_ROOT_PASSWORD) + if [[ "$minio_pass" == "minioadmin" ]]; then + log_warn "MINIO_ROOT_PASSWORD = 'minioadmin' в production — небезопасно!" + ((issues++)) + fi + fi + + # Проверить что .env.example не содержит новых переменных + if [[ -f .env.example ]]; then + local missing_in_env=0 + while IFS='=' read -r key _; do + key=$(echo "$key" | sed 's/^#\s*//' | xargs) + [[ -z "$key" || "$key" =~ ^# ]] && continue + if ! grep -qE "^${key}=" .env 2>/dev/null; then + if [[ "$missing_in_env" -eq 0 ]]; then + log_warn "Новые переменные из .env.example отсутствуют в .env:" + fi + echo -e " ${YELLOW}${key}${NC}" + missing_in_env=1 + fi + done < .env.example + [[ "$missing_in_env" -eq 1 ]] && ((issues++)) + fi + fi + + # --- 3. package.json --- + log_step "3/8 — package.json и зависимости" + if [[ -f package.json ]]; then + # Проверить "type" поле — оно не должно быть + if grep -q '"type"' package.json; then + local type_val + type_val=$(node -e "console.log(require('./package.json').type || '')" 2>/dev/null || echo "") + if [[ -n "$type_val" ]]; then + log_err "package.json содержит \"type\": \"${type_val}\" — вызывает ESM/CJS конфликт с Turbopack" + ((issues++)) + log_fix "Удаляю поле \"type\" из package.json..." + node -e " + const fs = require('fs'); + const pkg = JSON.parse(fs.readFileSync('package.json','utf8')); + delete pkg.type; + fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n'); + " 2>/dev/null && { log_ok "\"type\" удалён"; ((fixed++)); } || log_warn "Не удалось автоисправить" + fi + else + log_ok "package.json: нет конфликтного поля \"type\"" + fi + + # node_modules + if [[ ! -d "node_modules" ]]; then + log_warn "node_modules отсутствует" + ((issues++)) + log_fix "Запускаю npm install..." + npm install 2>&1 | tail -5 + log_ok "npm install завершён" + ((fixed++)) + elif [[ "package.json" -nt "node_modules/.package-lock.json" ]] 2>/dev/null; then + log_warn "node_modules устарел (package.json новее)" + ((issues++)) + log_fix "Запускаю npm install..." + npm install 2>&1 | tail -5 + log_ok "npm install обновлён" + ((fixed++)) + else + log_ok "node_modules актуален" + fi + + # Prisma client + if [[ -d "node_modules/.prisma/client" ]] || [[ -d "node_modules/@prisma/client" ]]; then + log_ok "Prisma Client сгенерирован" + else + log_warn "Prisma Client не найден" + ((issues++)) + log_fix "Генерирую Prisma Client..." + npx prisma generate 2>&1 | tail -2 + ((fixed++)) + fi + else + log_err "package.json не найден — это не корень проекта?" + ((issues++)) + fi + + # --- 4. Docker контейнеры --- + log_step "4/8 — Docker контейнеры" + local infra_services=(postgres redis pgbouncer minio) + local stopped_services=() + + for svc in "${infra_services[@]}"; do + local state + state=$(check_service "$svc") + case "$state" in + ok) + log_ok "${svc}: работает" + ;; + unhealthy) + log_warn "${svc}: unhealthy" + ((issues++)) + log_fix "Перезапускаю ${svc}..." + docker compose restart "$svc" 2>/dev/null + sleep 3 + local new_state + new_state=$(check_service "$svc") + if [[ "$new_state" == "ok" ]]; then + log_ok "${svc}: восстановлен" + ((fixed++)) + else + log_err "${svc}: всё ещё проблема после рестарта" + fi + ;; + *) + log_err "${svc}: ${state}" + ((issues++)) + stopped_services+=("$svc") + ;; + esac + done + + if [[ ${#stopped_services[@]} -gt 0 ]]; then + log_fix "Запускаю остановленные сервисы: ${stopped_services[*]}..." + dc up -d "${stopped_services[@]}" 2>&1 | tail -3 + sleep 5 + for svc in "${stopped_services[@]}"; do + local new_state + new_state=$(check_service "$svc") + if [[ "$new_state" == "ok" ]]; then + log_ok "${svc}: запущен" + ((fixed++)) + else + log_err "${svc}: не удалось запустить" + fi + done + fi + + # --- 5. Подключения к БД --- + log_step "5/8 — Подключение к базе данных" + if [[ "$(check_service postgres)" == "ok" ]]; then + # Прямое подключение + if docker compose exec -T postgres pg_isready -U postgres &>/dev/null; then + log_ok "PostgreSQL: pg_isready OK" + else + log_err "PostgreSQL: pg_isready FAIL" + ((issues++)) + fi + + # Проверить что БД liveserver существует + if docker compose exec -T postgres psql -U postgres -d liveserver -c "SELECT 1;" &>/dev/null; then + log_ok "БД 'liveserver' существует" + else + log_warn "БД 'liveserver' не найдена" + ((issues++)) + log_fix "Создаю БД..." + docker compose exec -T postgres createdb -U postgres liveserver 2>/dev/null && ((fixed++)) || true + fi + + # Проверить схему (есть ли таблицы) + local table_count + table_count=$(docker compose exec -T postgres psql -U postgres -d liveserver -t -c "SELECT count(*) FROM information_schema.tables WHERE table_schema='public';" 2>/dev/null | tr -d ' ' || echo "0") + if [[ "$table_count" -gt 0 ]]; then + log_ok "Схема БД: ${table_count} таблиц" + else + log_warn "Схема БД пуста" + ((issues++)) + log_fix "Применяю Prisma schema..." + npx prisma db push --skip-generate 2>&1 | tail -3 + ((fixed++)) + fi + + # PgBouncer + if [[ "$(check_service pgbouncer)" == "ok" ]]; then + if docker compose exec -T pgbouncer pg_isready -h 127.0.0.1 -p 6432 &>/dev/null; then + log_ok "PgBouncer: подключение OK" + else + log_warn "PgBouncer: не отвечает через pg_isready" + ((issues++)) + fi + fi + else + log_err "PostgreSQL не запущен — пропускаю проверки БД" + fi + + # --- 6. Redis --- + log_step "6/8 — Redis" + if [[ "$(check_service redis)" == "ok" ]]; then + if docker compose exec -T redis redis-cli ping 2>/dev/null | grep -q PONG; then + log_ok "Redis PING → PONG" + else + log_err "Redis не отвечает на PING" + ((issues++)) + fi + + local redis_mem + redis_mem=$(docker compose exec -T redis redis-cli info memory 2>/dev/null | grep used_memory_human | cut -d: -f2 | tr -d '[:space:]' || echo "?") + log_info "Redis memory: ${redis_mem}" + + local redis_keys + redis_keys=$(docker compose exec -T redis redis-cli dbsize 2>/dev/null | grep -oP '\d+' || echo "?") + log_info "Redis keys: ${redis_keys}" + else + log_err "Redis не запущен" + fi + + # --- 7. MinIO --- + log_step "7/8 — MinIO (S3 Storage)" + if [[ "$(check_service minio)" == "ok" ]]; then + log_ok "MinIO контейнер запущен" + + local s3_endpoint="http://localhost:9000" + local minio_user + minio_user=$(env_get MINIO_ROOT_USER) + local minio_pass + minio_pass=$(env_get MINIO_ROOT_PASSWORD) + local bucket + bucket=$(env_get S3_BUCKET) + bucket="${bucket:-liveserver}" + + # Проверить healthcheck + if curl -sf -o /dev/null "${s3_endpoint}/minio/health/live" 2>/dev/null; then + log_ok "MinIO health: live" + else + log_warn "MinIO health endpoint не отвечает (может быть закрыт порт)" + ((issues++)) + fi + + # Проверить bucket + local bucket_check + bucket_check=$(curl -sf -o /dev/null -w "%{http_code}" "${s3_endpoint}/${bucket}/" -u "${minio_user}:${minio_pass}" 2>/dev/null || echo "000") + if [[ "$bucket_check" == "200" || "$bucket_check" == "404" ]]; then + # 404 от bucket listing может быть нормально если нет объектов + log_ok "MinIO: bucket '${bucket}' доступен" + else + log_warn "MinIO: bucket '${bucket}' недоступен (HTTP ${bucket_check})" + ((issues++)) + log_fix "Создаю bucket..." + if check_command mc; then + mc alias set local "${s3_endpoint}" "$minio_user" "$minio_pass" 2>/dev/null || true + mc mb "local/${bucket}" 2>/dev/null || true + ((fixed++)) + else + curl -sf -o /dev/null -X PUT "${s3_endpoint}/${bucket}" -u "${minio_user}:${minio_pass}" 2>/dev/null && ((fixed++)) || \ + log_warn "Создай вручную: http://localhost:9001" + fi + fi + else + log_err "MinIO не запущен" + fi + + # --- 8. .next и кеш --- + log_step "8/8 — Кеш и сборка" + if [[ -d ".next" ]]; then + local next_size + next_size=$(du -sh .next 2>/dev/null | cut -f1 || echo "?") + log_ok ".next кеш: ${next_size}" + + # Проверить что .next не битый + if [[ -f ".next/BUILD_ID" ]]; then + log_ok "BUILD_ID: $(cat .next/BUILD_ID)" + else + log_warn ".next может быть повреждён (нет BUILD_ID)" + ((issues++)) + if ask_yn "Очистить .next кеш?" "y"; then + rm -rf .next + log_fix "Кеш очищен" + ((fixed++)) + fi + fi + else + log_info ".next кеш отсутствует (создастся при npm run dev / npm run build)" + fi + + # Проверить lock file конфликт + if [[ -f "package-lock.json" ]] && [[ -f "yarn.lock" ]]; then + log_warn "Найдены И package-lock.json И yarn.lock — выбери один менеджер" + ((issues++)) + fi + + # --- Итого --- + echo "" + echo -e "${CYAN}══════════════════════════════════════════════${NC}" + if [[ "$issues" -eq 0 ]]; then + echo -e " ${GREEN}${BOLD}Всё в порядке! Проблем не найдено.${NC}" + else + local remaining=$((issues - fixed)) + echo -e " Найдено проблем: ${YELLOW}${issues}${NC}" + echo -e " Автоисправлено: ${GREEN}${fixed}${NC}" + if [[ "$remaining" -gt 0 ]]; then + echo -e " Осталось: ${RED}${remaining}${NC}" + echo "" + echo -e " ${YELLOW}Исправь оставшиеся проблемы вручную или запусти doctor ещё раз${NC}" + else + echo -e " ${GREEN}${BOLD}Все проблемы исправлены!${NC}" + fi + fi + echo -e "${CYAN}══════════════════════════════════════════════${NC}" + echo "" +} + +# ============================================================ +# COMMAND: update +# ============================================================ + +cmd_update() { + log_step "Обновление LiveServer-M1" + + # 1. Git pull + log_step "1/5 — Обновление кода" + if [[ -d .git ]]; then + local branch + branch=$(git branch --show-current 2>/dev/null || echo "unknown") + log_info "Текущая ветка: ${branch}" + + # Проверить uncommitted changes + if ! git diff --quiet 2>/dev/null || ! git diff --cached --quiet 2>/dev/null; then + log_warn "Есть незакоммиченные изменения" + if ask_yn "Сохранить через git stash и продолжить?" "y"; then + git stash push -m "auto-stash before update $(date +%Y%m%d_%H%M%S)" 2>&1 + log_ok "Изменения сохранены в stash" + else + log_warn "Пропускаю git pull" + fi + fi + + git pull --rebase 2>&1 | tail -5 + log_ok "git pull завершён" + else + log_warn "Не git-репозиторий — пропускаю git pull" + fi + + # 2. npm install + log_step "2/5 — Зависимости" + npm install 2>&1 | tail -5 + log_ok "npm install завершён" + + # 3. Prisma + log_step "3/5 — База данных" + npx prisma generate 2>&1 | tail -2 + log_ok "Prisma Client обновлён" + + # Проверить нужна ли миграция + if docker compose exec -T postgres pg_isready -U postgres &>/dev/null; then + log_info "Применяю schema changes..." + npx prisma db push --skip-generate 2>&1 | tail -3 + log_ok "Схема БД синхронизирована" + else + log_warn "PostgreSQL недоступен — миграция пропущена" + fi + + # 4. Пересборка контейнеров + log_step "4/5 — Пересборка контейнеров" + local mode + mode=$(detect_mode) + if [[ "$mode" == "prod" ]]; then + log_info "Production mode — пересборка app + ai-agent" + dc up -d --build app ai-agent 2>&1 | tail -5 + else + log_info "Local mode — пересборка инфраструктуры" + dc up -d postgres minio redis pgbouncer 2>&1 | tail -5 + fi + log_ok "Контейнеры обновлены" + + # 5. Очистка .next + log_step "5/5 — Очистка кеша" + if [[ -d ".next" ]]; then + rm -rf .next + log_ok ".next кеш очищен (пересоберётся при запуске)" + else + log_ok "Кеш чист" + fi + + echo "" + log_ok "${GREEN}${BOLD}Обновление завершено!${NC}" + + if [[ "$mode" == "local" ]]; then + echo -e " Запусти: ${CYAN}npm run dev${NC}" + else + echo -e " Приложение должно быть доступно через несколько секунд" + fi + echo "" +} + +# ============================================================ +# COMMAND: install (оригинальный setup) +# ============================================================ + +cmd_install() { + # --- Step 1: Check & install dependencies --- + log_step "Шаг 1/6 — Проверка и установка зависимостей" + + local MISSING=0 + install_if_missing docker "https://docs.docker.com/get-docker/" || MISSING=1 + check_command "docker compose" || check_command docker-compose || { + log_err "docker compose не найден (нужен Docker Compose V2)" + MISSING=1 + } + install_if_missing node "https://nodejs.org/" || MISSING=1 + install_if_missing npm "https://nodejs.org/" || MISSING=1 + + if [[ "$MISSING" -eq 1 ]]; then + log_err "Установи недостающие зависимости и запусти скрипт снова." exit 1 fi - log_ok "Production: ${DOMAIN}" - log_warn "Убедись что DNS записи указывают на этот сервер:" - echo -e " ${DOMAIN} → $(curl -s ifconfig.me 2>/dev/null || echo '')" - echo -e " s3.${DOMAIN} → тот же IP" - echo -e " minio.${DOMAIN} → тот же IP" - echo "" - if ! ask_yn "DNS настроен?" "y"; then - log_warn "Настрой DNS и запусти скрипт снова." - exit 0 - fi -else - log_ok "Локальная разработка" -fi + local NODE_VER + NODE_VER=$(node -v) + log_ok "Node.js: $NODE_VER" -# --- Step 3: Configure environment --- -log_step "Шаг 3/6 — Настройка окружения" - -# Автогенерация паролей -PG_PASSWORD=$(generate_password) -MINIO_USER="minio-$(head -c 4 /dev/urandom | xxd -p)" -MINIO_PASS=$(generate_password) -AUTH_SECRET=$(generate_secret) -DEV_ACCESS_KEY=$(generate_password | head -c 16) -S3_BUCKET="liveserver" - -echo "" -echo -e " ${GREEN}${BOLD}Пароли сгенерированы автоматически:${NC}" -echo -e " ${CYAN}PostgreSQL:${NC} ${PG_PASSWORD}" -echo -e " ${CYAN}MinIO user:${NC} ${MINIO_USER}" -echo -e " ${CYAN}MinIO password:${NC} ${MINIO_PASS}" -echo -e " ${CYAN}Auth secret:${NC} ${AUTH_SECRET:0:16}..." -if [[ "$MODE" == "1" ]]; then - echo -e " ${CYAN}DEV_ACCESS_KEY:${NC} ${DEV_ACCESS_KEY}" -fi -echo "" -echo -e " ${YELLOW}Все пароли сохранены в .env — можешь изменить позже${NC}" - -if ask_yn "Хочешь задать свои пароли вместо сгенерированных?" "n"; then - echo "" - echo -e " ${BOLD}PostgreSQL${NC}" - ask "Пароль PostgreSQL" "$PG_PASSWORD" PG_PASSWORD + # --- Step 2: Choose mode --- + log_step "Шаг 2/6 — Режим запуска" echo "" - echo -e " ${BOLD}MinIO (S3 Storage)${NC}" - ask "MinIO root user" "$MINIO_USER" MINIO_USER - ask_secret "MinIO root password" "$MINIO_PASS" MINIO_PASS - ask "Название S3 bucket" "$S3_BUCKET" S3_BUCKET + echo -e " ${BOLD}1)${NC} Локальная разработка (localhost, без SSL)" + echo -e " ${BOLD}2)${NC} Production (домен + Traefik + Let's Encrypt SSL)" + echo "" + ask "Выбери режим" "1" MODE - if [[ "$MODE" == "1" ]]; then + local DOMAIN="" ACME_EMAIL="" + + if [[ "$MODE" == "2" ]]; then echo "" - echo -e " ${BOLD}Защита локалки${NC}" - ask "Ключ доступа (DEV_ACCESS_KEY)" "$DEV_ACCESS_KEY" DEV_ACCESS_KEY + ask "Домен (например live.example.com)" "" DOMAIN + ask "Email для Let's Encrypt" "" ACME_EMAIL + + if [[ -z "$DOMAIN" || -z "$ACME_EMAIL" ]]; then + log_err "Домен и email обязательны для production." + exit 1 + fi + + log_ok "Production: ${DOMAIN}" + log_warn "Убедись что DNS записи указывают на этот сервер:" + echo -e " ${DOMAIN} → $(curl -s ifconfig.me 2>/dev/null || echo '')" + echo -e " s3.${DOMAIN} → тот же IP" + echo -e " minio.${DOMAIN} → тот же IP" + echo "" + if ! ask_yn "DNS настроен?" "y"; then + log_warn "Настрой DNS и запусти скрипт снова." + exit 0 + fi + else + log_ok "Локальная разработка" fi -fi -echo "" -echo -e " ${BOLD}LiveKit${NC} (Enter чтобы пропустить, настроишь позже)" -ask "LiveKit URL (wss://...)" "" LK_URL -LK_KEY="" -LK_SECRET="" -LK_WEBHOOK="" -if [[ -n "$LK_URL" ]]; then - ask "LiveKit API Key" "" LK_KEY - ask_secret "LiveKit API Secret" "" LK_SECRET - ask_secret "LiveKit Webhook Secret (Enter = пропустить)" "" LK_WEBHOOK -fi + # --- Step 3: Configure environment --- + log_step "Шаг 3/6 — Настройка окружения" -echo "" -echo -e " ${BOLD}AI Agent${NC} (Enter чтобы пропустить, настроишь позже)" -ask_secret "Deepgram API Key" "" DG_KEY -ask_secret "OpenAI API Key" "" OAI_KEY + local PG_PASSWORD MINIO_USER MINIO_PASS AUTH_SECRET DEV_ACCESS_KEY S3_BUCKET + PG_PASSWORD=$(generate_password) + MINIO_USER="minio-$(head -c 4 /dev/urandom | xxd -p)" + MINIO_PASS=$(generate_password) + AUTH_SECRET=$(generate_secret) + DEV_ACCESS_KEY=$(generate_password | head -c 16) + S3_BUCKET="liveserver" -if [[ "$MODE" == "1" ]]; then - ALLOWED_IPS="" - if ask_yn "Ограничить доступ по IP? (кроме localhost)" "n"; then - ask "Разрешённые IP (через запятую)" "" ALLOWED_IPS + echo "" + echo -e " ${GREEN}${BOLD}Пароли сгенерированы автоматически:${NC}" + echo -e " ${CYAN}PostgreSQL:${NC} ${PG_PASSWORD}" + echo -e " ${CYAN}MinIO user:${NC} ${MINIO_USER}" + echo -e " ${CYAN}MinIO password:${NC} ${MINIO_PASS}" + echo -e " ${CYAN}Auth secret:${NC} ${AUTH_SECRET:0:16}..." + if [[ "$MODE" == "1" ]]; then + echo -e " ${CYAN}DEV_ACCESS_KEY:${NC} ${DEV_ACCESS_KEY}" fi -fi + echo "" + echo -e " ${YELLOW}Все пароли сохранены в .env — можешь изменить позже${NC}" -# --- Step 4: Generate .env --- -log_step "Шаг 4/6 — Генерация .env" + if ask_yn "Хочешь задать свои пароли вместо сгенерированных?" "n"; then + echo "" + echo -e " ${BOLD}PostgreSQL${NC}" + ask "Пароль PostgreSQL" "$PG_PASSWORD" PG_PASSWORD -if [[ "$MODE" == "2" ]]; then - APP_URL="https://${DOMAIN}" - S3_ENDPOINT="http://minio:9000" -else - APP_URL="http://localhost:3000" - S3_ENDPOINT="http://localhost:9000" -fi + echo "" + echo -e " ${BOLD}MinIO (S3 Storage)${NC}" + ask "MinIO root user" "$MINIO_USER" MINIO_USER + ask_secret "MinIO root password" "$MINIO_PASS" MINIO_PASS + ask "Название S3 bucket" "$S3_BUCKET" S3_BUCKET -# Бэкап существующего .env -if [[ -f .env ]]; then - cp .env ".env.backup.$(date +%Y%m%d_%H%M%S)" - log_ok "Бэкап старого .env создан" -fi + if [[ "$MODE" == "1" ]]; then + echo "" + echo -e " ${BOLD}Защита локалки${NC}" + ask "Ключ доступа (DEV_ACCESS_KEY)" "$DEV_ACCESS_KEY" DEV_ACCESS_KEY + fi + fi -cat > .env << ENVEOF + echo "" + echo -e " ${BOLD}LiveKit${NC} (Enter чтобы пропустить, настроишь позже)" + local LK_URL="" LK_KEY="" LK_SECRET="" LK_WEBHOOK="" + ask "LiveKit URL (wss://...)" "" LK_URL + if [[ -n "$LK_URL" ]]; then + ask "LiveKit API Key" "" LK_KEY + ask_secret "LiveKit API Secret" "" LK_SECRET + ask_secret "LiveKit Webhook Secret (Enter = пропустить)" "" LK_WEBHOOK + fi + + echo "" + echo -e " ${BOLD}AI Agent${NC} (Enter чтобы пропустить, настроишь позже)" + local DG_KEY="" OAI_KEY="" + ask_secret "Deepgram API Key" "" DG_KEY + ask_secret "OpenAI API Key" "" OAI_KEY + + local ALLOWED_IPS="" + if [[ "$MODE" == "1" ]]; then + if ask_yn "Ограничить доступ по IP? (кроме localhost)" "n"; then + ask "Разрешённые IP (через запятую)" "" ALLOWED_IPS + fi + fi + + # --- Step 4: Generate .env --- + log_step "Шаг 4/6 — Генерация .env" + + local APP_URL S3_ENDPOINT + if [[ "$MODE" == "2" ]]; then + APP_URL="https://${DOMAIN}" + S3_ENDPOINT="http://minio:9000" + else + APP_URL="http://localhost:3000" + S3_ENDPOINT="http://localhost:9000" + fi + + if [[ -f .env ]]; then + cp .env ".env.backup.$(date +%Y%m%d_%H%M%S)" + log_ok "Бэкап старого .env создан" + fi + + cat > .env << ENVEOF # === Generated by setup.sh at $(date) === # Все пароли сгенерированы автоматически — можешь заменить на свои. @@ -333,159 +945,271 @@ DEV_ACCESS_KEY=${DEV_ACCESS_KEY:-} ALLOWED_IPS=${ALLOWED_IPS:-} ENVEOF -log_ok ".env создан" + log_ok ".env создан" -# --- Step 5: Install & setup --- -log_step "Шаг 5/6 — Установка и настройка" + # --- Step 5: Install & setup --- + log_step "Шаг 5/6 — Установка и настройка" -# npm install — автоматически если нет node_modules или package-lock изменился -if [[ ! -d "node_modules" ]]; then - echo -e " Установка npm зависимостей..." - npm install 2>&1 | tail -5 - log_ok "npm install завершён" -elif [[ "package.json" -nt "node_modules/.package-lock.json" ]] 2>/dev/null; then - echo -e " package.json обновлён, переустановка зависимостей..." - npm install 2>&1 | tail -5 - log_ok "npm install завершён (обновление)" -else - log_ok "node_modules актуален" -fi - -# Prisma generate -echo -e " Генерация Prisma Client..." -npx prisma generate 2>&1 | tail -1 -log_ok "Prisma Client сгенерирован" - -# Docker compose up -echo -e " Запуск Docker контейнеров..." -if [[ "$MODE" == "2" ]]; then - docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build 2>&1 | tail -5 -else - docker compose up -d postgres minio redis pgbouncer 2>&1 | tail -5 -fi -log_ok "Docker контейнеры запущены" - -# Wait for postgres -echo -e " Ожидание PostgreSQL..." -for i in $(seq 1 30); do - if docker compose exec -T postgres pg_isready -U postgres &>/dev/null; then - break - fi - if [[ "$i" -eq 30 ]]; then - log_err "PostgreSQL не запустился за 30 секунд" - exit 1 - fi - sleep 1 -done -log_ok "PostgreSQL готов" - -# Wait for Redis -echo -e " Ожидание Redis..." -for i in $(seq 1 15); do - if docker compose exec -T redis redis-cli ping 2>/dev/null | grep -q PONG; then - break - fi - if [[ "$i" -eq 15 ]]; then - log_err "Redis не запустился за 15 секунд" - exit 1 - fi - sleep 1 -done -log_ok "Redis готов" - -# Wait for PgBouncer -echo -e " Ожидание PgBouncer..." -for i in $(seq 1 15); do - if docker compose exec -T pgbouncer pg_isready -h 127.0.0.1 -p 6432 &>/dev/null; then - break - fi - if [[ "$i" -eq 15 ]]; then - log_warn "PgBouncer не отвечает — продолжаем без него" - fi - sleep 1 -done -log_ok "PgBouncer готов" - -# Prisma migrate -echo -e " Применение миграций..." -npx prisma db push --skip-generate 2>&1 | tail -3 -log_ok "Схема БД синхронизирована" - -# Create MinIO bucket (через docker exec если mc не установлен) -echo -e " Создание S3 bucket..." -sleep 2 -if command -v mc &>/dev/null; then - mc alias set local http://localhost:9000 "$MINIO_USER" "$MINIO_PASS" 2>/dev/null || true - mc mb "local/${S3_BUCKET}" 2>/dev/null || true - log_ok "Bucket '${S3_BUCKET}' создан" -else - # Создание через curl (MinIO S3 API) - if curl -sf -o /dev/null -X PUT "http://localhost:9000/${S3_BUCKET}" \ - -u "${MINIO_USER}:${MINIO_PASS}" 2>/dev/null; then - log_ok "Bucket '${S3_BUCKET}' создан через API" + if [[ ! -d "node_modules" ]]; then + echo -e " Установка npm зависимостей..." + npm install 2>&1 | tail -5 + log_ok "npm install завершён" + elif [[ "package.json" -nt "node_modules/.package-lock.json" ]] 2>/dev/null; then + echo -e " package.json обновлён, переустановка зависимостей..." + npm install 2>&1 | tail -5 + log_ok "npm install завершён (обновление)" else - log_warn "Не удалось создать bucket автоматически" - echo -e " Создай вручную: ${CYAN}http://localhost:9001${NC} → Buckets → Create" - echo -e " Логин: ${CYAN}${MINIO_USER}${NC} / пароль в .env" + log_ok "node_modules актуален" fi -fi -# --- Step 6: Done --- -log_step "Шаг 6/6 — Готово!" + echo -e " Генерация Prisma Client..." + npx prisma generate 2>&1 | tail -1 + log_ok "Prisma Client сгенерирован" + + echo -e " Запуск Docker контейнеров..." + if [[ "$MODE" == "2" ]]; then + docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build 2>&1 | tail -5 + else + docker compose up -d postgres minio redis pgbouncer 2>&1 | tail -5 + fi + log_ok "Docker контейнеры запущены" + + wait_for_service "PostgreSQL" "docker compose exec -T postgres pg_isready -U postgres" 30 || exit 1 + wait_for_service "Redis" "docker compose exec -T redis redis-cli ping 2>/dev/null | grep -q PONG" 15 || exit 1 + wait_for_service "PgBouncer" "docker compose exec -T pgbouncer pg_isready -h 127.0.0.1 -p 6432" 15 || \ + log_warn "PgBouncer не отвечает — продолжаем без него" + + echo -e " Применение миграций..." + npx prisma db push --skip-generate 2>&1 | tail -3 + log_ok "Схема БД синхронизирована" + + echo -e " Создание S3 bucket..." + sleep 2 + if check_command mc; then + mc alias set local http://localhost:9000 "$MINIO_USER" "$MINIO_PASS" 2>/dev/null || true + mc mb "local/${S3_BUCKET}" 2>/dev/null || true + log_ok "Bucket '${S3_BUCKET}' создан" + else + if curl -sf -o /dev/null -X PUT "http://localhost:9000/${S3_BUCKET}" \ + -u "${MINIO_USER}:${MINIO_PASS}" 2>/dev/null; then + log_ok "Bucket '${S3_BUCKET}' создан через API" + else + log_warn "Не удалось создать bucket автоматически" + echo -e " Создай вручную: ${CYAN}http://localhost:9001${NC}" + fi + fi + + # --- Step 6: Done --- + log_step "Шаг 6/6 — Готово!" -echo "" -if [[ "$MODE" == "2" ]]; then - echo -e " ${GREEN}${BOLD}Production запущен!${NC}" echo "" - echo -e " Приложение: ${CYAN}https://${DOMAIN}${NC}" - echo -e " MinIO Console: ${CYAN}https://minio.${DOMAIN}${NC}" - echo -e " MinIO S3 API: ${CYAN}https://s3.${DOMAIN}${NC}" -else - echo -e " ${GREEN}${BOLD}Локальная среда готова!${NC}" - echo "" - echo -e " Запусти приложение: ${CYAN}npm run dev${NC}" - echo -e " Приложение: ${CYAN}http://localhost:3000${NC}" - echo -e " MinIO Console: ${CYAN}http://localhost:9001${NC}" - echo -e " Prisma Studio: ${CYAN}npx prisma studio${NC}" - - if [[ -n "${DEV_ACCESS_KEY:-}" ]]; then + if [[ "$MODE" == "2" ]]; then + echo -e " ${GREEN}${BOLD}Production запущен!${NC}" echo "" - echo -e " ${YELLOW}Для доступа из сети:${NC}" - echo -e " ${CYAN}http://:3000?key=${DEV_ACCESS_KEY}${NC}" + echo -e " Приложение: ${CYAN}https://${DOMAIN}${NC}" + echo -e " MinIO Console: ${CYAN}https://minio.${DOMAIN}${NC}" + echo -e " MinIO S3 API: ${CYAN}https://s3.${DOMAIN}${NC}" + else + echo -e " ${GREEN}${BOLD}Локальная среда готова!${NC}" + echo "" + echo -e " Запусти приложение: ${CYAN}npm run dev${NC}" + echo -e " Приложение: ${CYAN}http://localhost:3000${NC}" + echo -e " MinIO Console: ${CYAN}http://localhost:9001${NC}" + echo -e " Prisma Studio: ${CYAN}npx prisma studio${NC}" + if [[ -n "${DEV_ACCESS_KEY:-}" ]]; then + echo "" + echo -e " ${YELLOW}Для доступа из сети:${NC}" + echo -e " ${CYAN}http://:3000?key=${DEV_ACCESS_KEY}${NC}" + fi fi -fi -echo "" -echo -e " ${BOLD}Сгенерированные пароли (сохранены в .env):${NC}" -echo -e " ${CYAN}PostgreSQL:${NC} ${PG_PASSWORD}" -echo -e " ${CYAN}MinIO:${NC} ${MINIO_USER} / ${MINIO_PASS}" -if [[ "$MODE" == "1" && -n "${DEV_ACCESS_KEY:-}" ]]; then - echo -e " ${CYAN}DEV_ACCESS_KEY:${NC} ${DEV_ACCESS_KEY}" -fi -echo -e " ${YELLOW}Изменить пароли: отредактируй .env и перезапусти docker compose${NC}" - -if [[ -z "$LK_URL" ]]; then echo "" - log_warn "LiveKit не настроен — видеозвонки не будут работать." - echo -e " Зарегистрируйся на ${CYAN}https://cloud.livekit.io${NC}" - echo -e " и заполни LIVEKIT_URL, LIVEKIT_API_KEY, LIVEKIT_API_SECRET в .env" -fi + echo -e " ${BOLD}Сгенерированные пароли (сохранены в .env):${NC}" + echo -e " ${CYAN}PostgreSQL:${NC} ${PG_PASSWORD}" + echo -e " ${CYAN}MinIO:${NC} ${MINIO_USER} / ${MINIO_PASS}" + if [[ "$MODE" == "1" && -n "${DEV_ACCESS_KEY:-}" ]]; then + echo -e " ${CYAN}DEV_ACCESS_KEY:${NC} ${DEV_ACCESS_KEY}" + fi + echo -e " ${YELLOW}Изменить пароли: отредактируй .env и перезапусти docker compose${NC}" + + if [[ -z "$LK_URL" ]]; then + echo "" + log_warn "LiveKit не настроен — видеозвонки не будут работать." + echo -e " Зарегистрируйся на ${CYAN}https://cloud.livekit.io${NC}" + echo -e " и заполни LIVEKIT_URL, LIVEKIT_API_KEY, LIVEKIT_API_SECRET в .env" + fi + + if [[ -z "$DG_KEY" ]]; then + echo "" + log_warn "Deepgram не настроен — AI транскрипция отключена." + echo -e " Получи ключ: ${CYAN}https://console.deepgram.com${NC}" + fi + + if [[ -z "$OAI_KEY" ]]; then + echo "" + log_warn "OpenAI не настроен — AI суммаризация отключена." + echo -e " Получи ключ: ${CYAN}https://platform.openai.com/api-keys${NC}" + fi -if [[ -z "$DG_KEY" ]]; then echo "" - log_warn "Deepgram не настроен — AI транскрипция отключена." - echo -e " Получи ключ: ${CYAN}https://console.deepgram.com${NC}" -fi - -if [[ -z "$OAI_KEY" ]]; then + echo -e " Диагностика: ${CYAN}./setup.sh doctor${NC}" + echo -e " Обновление: ${CYAN}./setup.sh update${NC}" + echo -e " Статус: ${CYAN}./setup.sh status${NC}" echo "" - log_warn "OpenAI не настроен — AI суммаризация отключена." - echo -e " Получи ключ: ${CYAN}https://platform.openai.com/api-keys${NC}" -fi +} -echo "" -echo -e "${CYAN}══════════════════════════════════════════════${NC}" -echo -e " Документация: ${CYAN}README.md${NC}" -echo -e " Спецификация: ${CYAN}PROMPT.md${NC}" -echo -e "${CYAN}══════════════════════════════════════════════${NC}" -echo "" +# ============================================================ +# COMMAND: logs +# ============================================================ + +cmd_logs() { + local service="${1:-app}" + log_info "Логи: ${service} (Ctrl+C для выхода)" + docker compose logs -f --tail 100 "$service" +} + +# ============================================================ +# COMMAND: restart +# ============================================================ + +cmd_restart() { + log_step "Перезапуск контейнеров" + local mode + mode=$(detect_mode) + + if [[ "$mode" == "prod" ]]; then + dc down 2>&1 | tail -3 + dc up -d --build 2>&1 | tail -5 + else + docker compose restart postgres redis pgbouncer minio 2>&1 | tail -3 + fi + + log_ok "Контейнеры перезапущены" + + # Очистить .next на всякий случай + if [[ -d ".next" ]] && ask_yn "Очистить .next кеш?" "n"; then + rm -rf .next + log_ok ".next очищен" + fi +} + +# ============================================================ +# COMMAND: reset +# ============================================================ + +cmd_reset() { + echo "" + echo -e " ${RED}${BOLD}ВНИМАНИЕ: Полный сброс удалит:${NC}" + echo -e " ${RED}• Все Docker контейнеры и volumes (БД, файлы MinIO)${NC}" + echo -e " ${RED}• node_modules${NC}" + echo -e " ${RED}• .next кеш${NC}" + echo -e " ${RED}• .env (будет создан бэкап)${NC}" + echo "" + + if ! ask_yn "Точно сбросить всё?" "n"; then + log_info "Отменено" + return + fi + + if ! ask_yn "Ты уверен? Данные БД будут УДАЛЕНЫ" "n"; then + log_info "Отменено" + return + fi + + log_step "Полный сброс" + + # Бэкап .env + if [[ -f .env ]]; then + cp .env ".env.backup.$(date +%Y%m%d_%H%M%S)" + log_ok "Бэкап .env создан" + fi + + # Остановить и удалить контейнеры + volumes + log_info "Остановка контейнеров..." + dc down -v 2>&1 | tail -5 + log_ok "Контейнеры и volumes удалены" + + # Очистка + [[ -d "node_modules" ]] && { rm -rf node_modules; log_ok "node_modules удалён"; } + [[ -d ".next" ]] && { rm -rf .next; log_ok ".next удалён"; } + [[ -f ".env" ]] && { rm .env; log_ok ".env удалён"; } + + echo "" + log_ok "${GREEN}Сброс завершён${NC}" + echo -e " Для повторной установки: ${CYAN}./setup.sh install${NC}" + echo "" +} + +# ============================================================ +# MAIN — Роутер команд +# ============================================================ + +print_banner + +CMD="${1:-}" +shift 2>/dev/null || true + +case "$CMD" in + install) + cmd_install + ;; + update) + cmd_update + ;; + doctor|fix) + cmd_doctor + ;; + status|st) + cmd_status + ;; + logs|log) + cmd_logs "$@" + ;; + restart) + cmd_restart + ;; + reset) + cmd_reset + ;; + help|-h|--help) + print_usage + ;; + "") + # Интерактивное меню + if [[ ! -f .env ]]; then + echo -e " ${YELLOW}.env не найден — похоже, первый запуск${NC}" + echo "" + if ask_yn "Запустить установку?" "y"; then + cmd_install + else + print_usage + fi + else + echo -e " ${BOLD}Что делаем?${NC}" + echo "" + echo -e " ${BOLD}1)${NC} ${CYAN}doctor${NC} — диагностика и исправление" + echo -e " ${BOLD}2)${NC} ${CYAN}update${NC} — обновить проект" + echo -e " ${BOLD}3)${NC} ${CYAN}status${NC} — статус сервисов" + echo -e " ${BOLD}4)${NC} ${CYAN}restart${NC} — перезапуск контейнеров" + echo -e " ${BOLD}5)${NC} ${CYAN}install${NC} — полная переустановка" + echo -e " ${BOLD}6)${NC} ${CYAN}reset${NC} — сброс всего" + echo "" + ask "Выбери действие" "1" CHOICE + + case "$CHOICE" in + 1|doctor) cmd_doctor ;; + 2|update) cmd_update ;; + 3|status) cmd_status ;; + 4|restart) cmd_restart ;; + 5|install) cmd_install ;; + 6|reset) cmd_reset ;; + *) log_err "Неизвестный выбор: ${CHOICE}"; print_usage ;; + esac + fi + ;; + *) + log_err "Неизвестная команда: ${CMD}" + print_usage + exit 1 + ;; +esac