- ./setup.sh admin — create admin user directly in DB - Hashes password via bcryptjs (already in dependencies) - Creates both user and better-auth account records - Shows existing admins, offers to promote existing users - Validates email, password length (min 8) - Added to interactive menu and CLI router Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1347 lines
47 KiB
Bash
1347 lines
47 KiB
Bash
#!/usr/bin/env bash
|
||
set -uo pipefail
|
||
|
||
# ============================================================
|
||
# 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'
|
||
BLUE='\033[0;34m'
|
||
CYAN='\033[0;36m'
|
||
NC='\033[0m'
|
||
BOLD='\033[1m'
|
||
DIM='\033[2m'
|
||
|
||
# --- 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
|
||
echo -en " ${CYAN}?${NC} ${prompt} ${YELLOW}[${default}]${NC}: "
|
||
else
|
||
echo -en " ${CYAN}?${NC} ${prompt}: "
|
||
fi
|
||
read -r input
|
||
printf -v "$var_name" '%s' "${input:-$default}"
|
||
}
|
||
|
||
ask_secret() {
|
||
local prompt="$1" default="${2:-}" var_name="$3"
|
||
if [[ -n "$default" ]]; then
|
||
echo -en " ${CYAN}?${NC} ${prompt} ${YELLOW}[${default}]${NC}: "
|
||
else
|
||
echo -en " ${CYAN}?${NC} ${prompt}: "
|
||
fi
|
||
read -rs input
|
||
echo ""
|
||
printf -v "$var_name" '%s' "${input:-$default}"
|
||
}
|
||
|
||
ask_yn() {
|
||
local prompt="$1" default="${2:-y}"
|
||
local hint="Y/n"
|
||
[[ "$default" == "n" ]] && hint="y/N"
|
||
echo -en " ${CYAN}?${NC} ${prompt} ${YELLOW}[${hint}]${NC}: "
|
||
read -r input
|
||
input="${input:-$default}"
|
||
[[ "${input,,}" == "y" || "${input,,}" == "yes" ]]
|
||
}
|
||
|
||
generate_secret() {
|
||
openssl rand -base64 32 2>/dev/null || head -c 32 /dev/urandom | base64 | tr -d '=/+' | head -c 32
|
||
}
|
||
|
||
generate_password() {
|
||
openssl rand -base64 24 2>/dev/null | tr -d '=/+' | head -c 20 || \
|
||
head -c 24 /dev/urandom | base64 | tr -d '=/+' | head -c 20
|
||
}
|
||
|
||
# Прочитать переменную из .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 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 check_command nvm; then
|
||
echo -e " Установка Node.js через nvm..."
|
||
nvm install --lts 2>&1 | tail -3
|
||
check_command "$cmd" && { log_ok "$cmd установлен через nvm"; return 0; }
|
||
fi
|
||
fi
|
||
|
||
case "$cmd" in
|
||
docker)
|
||
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 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
|
||
|
||
log_err "$cmd не удалось установить автоматически"
|
||
echo -e " ${YELLOW}Установи вручную:${NC} $install_hint"
|
||
return 1
|
||
}
|
||
|
||
# ============================================================
|
||
# Banners
|
||
# ============================================================
|
||
|
||
print_banner() {
|
||
echo ""
|
||
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 ""
|
||
}
|
||
|
||
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}admin${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=$((issues + 1))
|
||
install_if_missing "$cmd" "см. документацию" && fixed=$((fixed + 1)) || 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=$((issues + 1))
|
||
fi
|
||
|
||
if docker info &>/dev/null; then
|
||
log_ok "Docker daemon запущен"
|
||
else
|
||
log_err "Docker daemon не запущен"
|
||
issues=$((issues + 1))
|
||
log_fix "Запускаю Docker..."
|
||
sudo systemctl start docker 2>/dev/null && fixed=$((fixed + 1)) || log_warn "Не удалось запустить Docker"
|
||
fi
|
||
fi
|
||
|
||
# --- 2. .env ---
|
||
log_step "2/8 — Файл .env"
|
||
if [[ ! -f .env ]]; then
|
||
log_err ".env не найден"
|
||
issues=$((issues + 1))
|
||
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=$((issues + 1))
|
||
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=$((issues + 1))
|
||
fi
|
||
local minio_pass
|
||
minio_pass=$(env_get MINIO_ROOT_PASSWORD)
|
||
if [[ "$minio_pass" == "minioadmin" ]]; then
|
||
log_warn "MINIO_ROOT_PASSWORD = 'minioadmin' в production — небезопасно!"
|
||
issues=$((issues + 1))
|
||
fi
|
||
fi
|
||
|
||
# Проверить что .env.example не содержит новых переменных
|
||
if [[ -f .env.example ]]; then
|
||
local missing_in_env=0
|
||
while IFS= read -r line; do
|
||
# Пропустить пустые строки и комментарии
|
||
[[ -z "$line" || "$line" =~ ^[[:space:]]*# ]] && continue
|
||
# Извлечь ключ (до первого =)
|
||
local key="${line%%=*}"
|
||
# Убрать пробелы и проверить что это валидное имя переменной
|
||
key=$(echo "$key" | tr -d '[:space:]')
|
||
[[ -z "$key" || ! "$key" =~ ^[A-Z_][A-Z0-9_]*$ ]] && 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=$((issues + 1))
|
||
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=$((issues + 1))
|
||
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=$((fixed + 1)); } || log_warn "Не удалось автоисправить"
|
||
fi
|
||
else
|
||
log_ok "package.json: нет конфликтного поля \"type\""
|
||
fi
|
||
|
||
# node_modules
|
||
if [[ ! -d "node_modules" ]]; then
|
||
log_warn "node_modules отсутствует"
|
||
issues=$((issues + 1))
|
||
log_fix "Запускаю npm install..."
|
||
npm install 2>&1 | tail -5
|
||
log_ok "npm install завершён"
|
||
fixed=$((fixed + 1))
|
||
elif [[ "package.json" -nt "node_modules/.package-lock.json" ]] 2>/dev/null; then
|
||
log_warn "node_modules устарел (package.json новее)"
|
||
issues=$((issues + 1))
|
||
log_fix "Запускаю npm install..."
|
||
npm install 2>&1 | tail -5
|
||
log_ok "npm install обновлён"
|
||
fixed=$((fixed + 1))
|
||
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=$((issues + 1))
|
||
log_fix "Генерирую Prisma Client..."
|
||
npx prisma generate 2>&1 | tail -2
|
||
fixed=$((fixed + 1))
|
||
fi
|
||
else
|
||
log_err "package.json не найден — это не корень проекта?"
|
||
issues=$((issues + 1))
|
||
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=$((issues + 1))
|
||
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=$((fixed + 1))
|
||
else
|
||
log_err "${svc}: всё ещё проблема после рестарта"
|
||
fi
|
||
;;
|
||
*)
|
||
log_err "${svc}: ${state}"
|
||
issues=$((issues + 1))
|
||
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=$((fixed + 1))
|
||
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=$((issues + 1))
|
||
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=$((issues + 1))
|
||
log_fix "Создаю БД..."
|
||
docker compose exec -T postgres createdb -U postgres liveserver 2>/dev/null && fixed=$((fixed + 1)) || 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=$((issues + 1))
|
||
log_fix "Применяю Prisma schema..."
|
||
npx prisma db push --skip-generate --accept-data-loss 2>&1 | tail -3
|
||
fixed=$((fixed + 1))
|
||
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=$((issues + 1))
|
||
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=$((issues + 1))
|
||
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=$((issues + 1))
|
||
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=$((issues + 1))
|
||
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=$((fixed + 1))
|
||
else
|
||
curl -sf -o /dev/null -X PUT "${s3_endpoint}/${bucket}" -u "${minio_user}:${minio_pass}" 2>/dev/null && fixed=$((fixed + 1)) || \
|
||
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=$((issues + 1))
|
||
if ask_yn "Очистить .next кеш?" "y"; then
|
||
rm -rf .next
|
||
log_fix "Кеш очищен"
|
||
fixed=$((fixed + 1))
|
||
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=$((issues + 1))
|
||
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
|
||
local skip_pull=false
|
||
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"
|
||
skip_pull=true
|
||
fi
|
||
fi
|
||
|
||
if [[ "$skip_pull" == "false" ]]; then
|
||
git pull --rebase 2>&1 | tail -5
|
||
log_ok "git pull завершён"
|
||
fi
|
||
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 --accept-data-loss 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: admin — создание админа
|
||
# ============================================================
|
||
|
||
cmd_admin() {
|
||
log_step "Создание администратора"
|
||
|
||
# Проверить что PostgreSQL доступен
|
||
if ! docker compose exec -T postgres pg_isready -U postgres &>/dev/null; then
|
||
log_err "PostgreSQL не запущен. Запусти: ./setup.sh doctor"
|
||
return 1
|
||
fi
|
||
|
||
# Проверить что таблица users существует
|
||
if ! docker compose exec -T postgres psql -U postgres -d liveserver -c "SELECT 1 FROM users LIMIT 0;" &>/dev/null; then
|
||
log_err "Таблица users не найдена. Запусти: npx prisma db push"
|
||
return 1
|
||
fi
|
||
|
||
# Показать существующих админов
|
||
local existing_admins
|
||
existing_admins=$(docker compose exec -T postgres psql -U postgres -d liveserver -t -c \
|
||
"SELECT email FROM users WHERE role = 'ADMIN';" 2>/dev/null | tr -d ' ' | grep -v '^$' || true)
|
||
|
||
if [[ -n "$existing_admins" ]]; then
|
||
echo ""
|
||
log_info "Существующие админы:"
|
||
echo "$existing_admins" | while read -r admin_email; do
|
||
echo -e " ${CYAN}${admin_email}${NC}"
|
||
done
|
||
echo ""
|
||
fi
|
||
|
||
# Спросить данные
|
||
local ADMIN_NAME ADMIN_EMAIL ADMIN_PASS
|
||
echo ""
|
||
ask "Имя администратора" "Admin" ADMIN_NAME
|
||
ask "Email" "" ADMIN_EMAIL
|
||
ask_secret "Пароль (мин. 8 символов)" "" ADMIN_PASS
|
||
|
||
# Валидация
|
||
if [[ -z "$ADMIN_EMAIL" ]]; then
|
||
log_err "Email обязателен"
|
||
return 1
|
||
fi
|
||
|
||
if [[ ${#ADMIN_PASS} -lt 8 ]]; then
|
||
log_err "Пароль должен быть минимум 8 символов"
|
||
return 1
|
||
fi
|
||
|
||
# Проверить что email не занят
|
||
local exists
|
||
exists=$(docker compose exec -T postgres psql -U postgres -d liveserver -t -c \
|
||
"SELECT count(*) FROM users WHERE email = '${ADMIN_EMAIL}';" 2>/dev/null | tr -d ' ')
|
||
|
||
if [[ "$exists" -gt 0 ]]; then
|
||
echo ""
|
||
if ask_yn "Пользователь ${ADMIN_EMAIL} уже существует. Сделать админом?" "y"; then
|
||
docker compose exec -T postgres psql -U postgres -d liveserver -c \
|
||
"UPDATE users SET role = 'ADMIN' WHERE email = '${ADMIN_EMAIL}';" &>/dev/null
|
||
log_ok "${ADMIN_EMAIL} теперь ADMIN"
|
||
return 0
|
||
else
|
||
log_info "Отменено"
|
||
return 0
|
||
fi
|
||
fi
|
||
|
||
# Хешировать пароль через node + bcryptjs (уже в зависимостях)
|
||
log_info "Хеширование пароля..."
|
||
local HASH
|
||
HASH=$(node -e "
|
||
const bcrypt = require('bcryptjs');
|
||
const hash = bcrypt.hashSync(process.argv[1], 10);
|
||
process.stdout.write(hash);
|
||
" "$ADMIN_PASS" 2>/dev/null)
|
||
|
||
if [[ -z "$HASH" ]]; then
|
||
log_err "Не удалось хешировать пароль (bcryptjs не установлен?)"
|
||
log_info "Попробуй: npm install && ./setup.sh admin"
|
||
return 1
|
||
fi
|
||
|
||
# Генерировать ID в стиле cuid
|
||
local USER_ID
|
||
USER_ID=$(node -e "process.stdout.write(require('crypto').randomBytes(12).toString('hex'))" 2>/dev/null || echo "admin_$(date +%s)")
|
||
|
||
# Создать пользователя
|
||
docker compose exec -T postgres psql -U postgres -d liveserver -c "
|
||
INSERT INTO users (id, email, name, \"emailVerified\", role, \"createdAt\", \"updatedAt\")
|
||
VALUES ('${USER_ID}', '${ADMIN_EMAIL}', '${ADMIN_NAME}', true, 'ADMIN', NOW(), NOW());
|
||
" &>/dev/null
|
||
|
||
# Создать аккаунт (better-auth accounts table для email+password)
|
||
local ACCOUNT_ID
|
||
ACCOUNT_ID=$(node -e "process.stdout.write(require('crypto').randomBytes(12).toString('hex'))" 2>/dev/null || echo "acc_$(date +%s)")
|
||
|
||
docker compose exec -T postgres psql -U postgres -d liveserver -c "
|
||
INSERT INTO accounts (id, \"userId\", \"accountId\", \"providerId\", \"password\", \"createdAt\", \"updatedAt\")
|
||
VALUES ('${ACCOUNT_ID}', '${USER_ID}', '${USER_ID}', 'credential', '${HASH}', NOW(), NOW());
|
||
" &>/dev/null
|
||
|
||
if [[ $? -eq 0 ]]; then
|
||
echo ""
|
||
log_ok "${GREEN}${BOLD}Администратор создан!${NC}"
|
||
echo -e " ${CYAN}Email:${NC} ${ADMIN_EMAIL}"
|
||
echo -e " ${CYAN}Пароль:${NC} (тот что ввёл)"
|
||
echo -e " ${CYAN}Роль:${NC} ADMIN"
|
||
echo ""
|
||
echo -e " Войди через: ${CYAN}http://localhost:3000/login${NC}"
|
||
else
|
||
log_err "Не удалось создать аккаунт"
|
||
fi
|
||
}
|
||
|
||
# ============================================================
|
||
# 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
|
||
|
||
local NODE_VER
|
||
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
|
||
|
||
local DOMAIN="" ACME_EMAIL=""
|
||
|
||
if [[ "$MODE" == "2" ]]; then
|
||
echo ""
|
||
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 '<server-ip>')"
|
||
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
|
||
|
||
# --- Step 3: Configure environment ---
|
||
log_step "Шаг 3/6 — Настройка окружения"
|
||
|
||
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"
|
||
|
||
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
|
||
|
||
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
|
||
|
||
if [[ "$MODE" == "1" ]]; then
|
||
echo ""
|
||
echo -e " ${BOLD}Защита локалки${NC}"
|
||
ask "Ключ доступа (DEV_ACCESS_KEY)" "$DEV_ACCESS_KEY" DEV_ACCESS_KEY
|
||
fi
|
||
fi
|
||
|
||
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) ===
|
||
# Все пароли сгенерированы автоматически — можешь заменить на свои.
|
||
|
||
# Domain & SSL
|
||
DOMAIN=${DOMAIN}
|
||
ACME_EMAIL=${ACME_EMAIL}
|
||
|
||
# Database
|
||
DATABASE_URL=postgresql://postgres:${PG_PASSWORD}@localhost:5432/liveserver
|
||
POSTGRES_PASSWORD=${PG_PASSWORD}
|
||
|
||
# Redis
|
||
# Для Docker контейнеров: redis://redis:6379
|
||
# Для локального npm run dev: redis://localhost:6379
|
||
REDIS_URL=redis://localhost:6379
|
||
|
||
# LiveKit
|
||
LIVEKIT_URL=${LK_URL}
|
||
NEXT_PUBLIC_LIVEKIT_URL=${LK_URL}
|
||
LIVEKIT_API_KEY=${LK_KEY}
|
||
LIVEKIT_API_SECRET=${LK_SECRET}
|
||
|
||
# AI Agent
|
||
DEEPGRAM_API_KEY=${DG_KEY}
|
||
OPENAI_API_KEY=${OAI_KEY}
|
||
|
||
# Storage (MinIO/S3)
|
||
S3_ENDPOINT=${S3_ENDPOINT}
|
||
S3_ACCESS_KEY=${MINIO_USER}
|
||
S3_SECRET_KEY=${MINIO_PASS}
|
||
S3_BUCKET=${S3_BUCKET}
|
||
MINIO_ROOT_USER=${MINIO_USER}
|
||
MINIO_ROOT_PASSWORD=${MINIO_PASS}
|
||
|
||
# Auth
|
||
BETTER_AUTH_SECRET=${AUTH_SECRET}
|
||
BETTER_AUTH_URL=${APP_URL}
|
||
NEXT_PUBLIC_APP_URL=${APP_URL}
|
||
|
||
# Local dev protection
|
||
DEV_ACCESS_KEY=${DEV_ACCESS_KEY:-}
|
||
ALLOWED_IPS=${ALLOWED_IPS:-}
|
||
ENVEOF
|
||
|
||
log_ok ".env создан"
|
||
|
||
# --- Step 5: Install & setup ---
|
||
log_step "Шаг 5/6 — Установка и настройка"
|
||
|
||
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
|
||
|
||
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 --accept-data-loss 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
|
||
echo ""
|
||
echo -e " ${YELLOW}Для доступа из сети:${NC}"
|
||
echo -e " ${CYAN}http://<ip>:3000?key=${DEV_ACCESS_KEY}${NC}"
|
||
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
|
||
|
||
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
|
||
|
||
echo ""
|
||
echo -e " Диагностика: ${CYAN}./setup.sh doctor${NC}"
|
||
echo -e " Обновление: ${CYAN}./setup.sh update${NC}"
|
||
echo -e " Статус: ${CYAN}./setup.sh status${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
|
||
;;
|
||
admin)
|
||
cmd_admin
|
||
;;
|
||
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}admin${NC} — создать администратора"
|
||
echo -e " ${BOLD}5)${NC} ${CYAN}restart${NC} — перезапуск контейнеров"
|
||
echo -e " ${BOLD}6)${NC} ${CYAN}install${NC} — полная переустановка"
|
||
echo -e " ${BOLD}7)${NC} ${CYAN}reset${NC} — сброс всего"
|
||
echo ""
|
||
ask "Выбери действие" "1" CHOICE
|
||
|
||
case "$CHOICE" in
|
||
1|doctor) cmd_doctor ;;
|
||
2|update) cmd_update ;;
|
||
3|status) cmd_status ;;
|
||
4|admin) cmd_admin ;;
|
||
5|restart) cmd_restart ;;
|
||
6|install) cmd_install ;;
|
||
7|reset) cmd_reset ;;
|
||
*) log_err "Неизвестный выбор: ${CHOICE}"; print_usage ;;
|
||
esac
|
||
fi
|
||
;;
|
||
*)
|
||
log_err "Неизвестная команда: ${CMD}"
|
||
print_usage
|
||
exit 1
|
||
;;
|
||
esac
|