Files
LiveServer-M1/setup.sh
joylessorchid f6d3f37a5f feat: dark design system + full UI redesign + HTTPS for local dev
- Design system: CSS custom properties (surface levels, accent, status colors, scrollbar, focus-visible)
- Landing: hero with gradient title, feature cards with SVG icons
- Auth pages: consistent card layout with design tokens
- Dashboard: sticky top bar, room grid with status dots, empty state
- Create room: toggle switches, form validation with spinners
- Join flow: room info card with status badge, monospace code input
- Room page: top bar with sidebar toggles (chat/lobby/moderation)
- Chat: bubble messages with optimistic UI, empty state
- Moderation: participant list with avatar initials, kick/ban
- Lobby: waiting animation with pulsing rings, approve/reject
- HTTPS: dev:https script, setup.sh auto-configures BETTER_AUTH_URL
- Auth: trustedOrigins now includes both http:// and https://
2026-03-24 12:14:49 +03:00

1586 lines
56 KiB
Bash
Raw Permalink 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
set -uo pipefail
# ============================================================
# LiveServer-M1 — Universal Setup / Update / Doctor Script
# ============================================================
# Usage:
# ./setup.sh — интерактивное меню
# ./setup.sh dev — обновить + запустить dev-сервер (auto LAN, kill old)
# ./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}dev${NC} Обновить + запустить dev-сервер (auto LAN, kill old)"
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 sh -c 'echo | nc -w 2 127.0.0.1 6432' &>/dev/null; then
log_ok "PgBouncer: порт 6432 отвечает"
else
log_warn "PgBouncer: порт 6432 не отвечает"
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 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 через docker exec (не зависит от пробросa портов)
if docker compose exec -T minio curl -sf http://localhost:9000/minio/health/live &>/dev/null; then
log_ok "MinIO health: live"
else
log_warn "MinIO health endpoint не отвечает"
issues=$((issues + 1))
fi
# Проверить/создать bucket через docker exec mc
if docker compose exec -T minio mc alias set local http://localhost:9000 "$minio_user" "$minio_pass" &>/dev/null; then
if docker compose exec -T minio mc ls "local/${bucket}" &>/dev/null; then
log_ok "MinIO: bucket '${bucket}' существует"
else
log_warn "MinIO: bucket '${bucket}' не найден"
issues=$((issues + 1))
log_fix "Создаю bucket..."
if docker compose exec -T minio mc mb "local/${bucket}" &>/dev/null; then
log_ok "Bucket '${bucket}' создан"
fixed=$((fixed + 1))
else
log_warn "Не удалось создать bucket"
fi
fi
else
log_warn "MinIO: mc недоступен в контейнере — проверка bucket пропущена"
issues=$((issues + 1))
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/7 — Проверка и установка зависимостей"
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/7 — Режим запуска"
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/7 — Настройка окружения"
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/7 — Генерация .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/7 — Установка и настройка"
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 sh -c 'echo | nc -w 2 127.0.0.1 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: First admin ---
log_step "Шаг 6/7 — Первый администратор"
echo ""
echo -e " ${BOLD}Создай аккаунт администратора для входа в систему.${NC}"
echo ""
local ADMIN_NAME ADMIN_EMAIL ADMIN_PASS
ask "Имя администратора" "Admin" ADMIN_NAME
ask "Email" "" ADMIN_EMAIL
ask_secret "Пароль (мин. 8 символов)" "" ADMIN_PASS
if [[ -n "$ADMIN_EMAIL" && ${#ADMIN_PASS} -ge 8 ]]; then
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 [[ -n "$HASH" ]]; then
local USER_ID ACCOUNT_ID
USER_ID=$(node -e "process.stdout.write(require('crypto').randomBytes(12).toString('hex'))" 2>/dev/null)
ACCOUNT_ID=$(node -e "process.stdout.write(require('crypto').randomBytes(12).toString('hex'))" 2>/dev/null)
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
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
log_ok "Администратор создан: ${ADMIN_EMAIL}"
else
log_warn "Не удалось создать админа — создай позже: ./setup.sh admin"
fi
else
log_warn "bcryptjs не готов — создай админа позже: ./setup.sh admin"
fi
else
if [[ -z "$ADMIN_EMAIL" ]]; then
log_info "Пропущено — создай позже: ./setup.sh admin"
else
log_err "Пароль должен быть мин. 8 символов — создай позже: ./setup.sh admin"
fi
fi
# --- Step 7: Done ---
log_step "Шаг 7/7 — Готово!"
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 ""
}
# ============================================================
# COMMAND: dev — обновить + запустить dev-сервер
# ============================================================
cmd_dev() {
log_step "Dev Server — обновление и запуск"
# 0. Создать docker-compose.override.yml если нет (проброс портов для dev)
if [[ ! -f docker-compose.override.yml ]]; then
log_info "Создаю docker-compose.override.yml (проброс портов для dev)..."
cat > docker-compose.override.yml <<'OVERRIDE'
# Локальная разработка — без Traefik, прямые порты
# Подхватывается автоматически при `docker compose up`
# Для прода: docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
services:
postgres:
ports:
- "5432:5432"
minio:
ports:
- "9000:9000"
- "9001:9001"
redis:
ports:
- "6379:6379"
pgbouncer:
ports:
- "6432:6432"
OVERRIDE
log_ok "docker-compose.override.yml создан"
fi
# 1. Убить старые процессы Next.js + освободить порт 3000
log_step "1/8 — Очистка старых процессов"
local killed=0
# Убить все процессы next
for pid in $(pgrep -f "next dev" 2>/dev/null || true); do
kill -9 "$pid" 2>/dev/null && ((killed++)) || true
done
for pid in $(pgrep -f "next-server" 2>/dev/null || true); do
kill -9 "$pid" 2>/dev/null && ((killed++)) || true
done
# Убить всё на портах 3000-3005
for p in $(seq 3000 3005); do
local port_pid
port_pid=$(lsof -ti :"$p" 2>/dev/null || true)
if [[ -n "$port_pid" ]]; then
kill -9 $port_pid 2>/dev/null && ((killed++)) || true
fi
done
# Удалить lock-файл Next.js
rm -rf .next/dev 2>/dev/null || true
if [[ $killed -gt 0 ]]; then
log_ok "Убито процессов: $killed"
sleep 1
else
log_ok "Старых процессов нет"
fi
# 2. Git pull (без интерактива — stash автоматически)
log_step "2/8 — Обновление кода"
if [[ -d .git ]]; then
if ! git diff --quiet 2>/dev/null || ! git diff --cached --quiet 2>/dev/null; then
git stash push -m "auto-stash before dev $(date +%Y%m%d_%H%M%S)" 2>&1
log_ok "Изменения сохранены в stash"
fi
git pull --rebase 2>&1 | tail -5
log_ok "git pull завершён"
fi
# 3. npm install
log_step "3/8 — Зависимости"
npm install 2>&1 | tail -3
log_ok "npm install завершён"
# 4. Prisma generate (не требует БД)
log_step "4/8 — Prisma generate"
npx prisma generate 2>&1 | tail -2
log_ok "Prisma Client сгенерирован"
# 5. Контейнеры + ожидание здоровья
log_step "5/8 — Контейнеры"
# Пересоздать если порты не проброшены (override изменился)
docker compose up -d --force-recreate postgres minio redis pgbouncer 2>&1 | tail -5
log_ok "Инфра-контейнеры запущены"
# Ждём PostgreSQL (макс 30 сек)
local waited=0
while [[ $waited -lt 30 ]]; do
if docker compose exec -T postgres pg_isready -U postgres &>/dev/null; then
log_ok "PostgreSQL ready"
break
fi
sleep 2
waited=$((waited + 2))
done
if [[ $waited -ge 30 ]]; then
log_warn "PostgreSQL не готов за 30 сек — продолжаю"
fi
# Ждём Redis (макс 10 сек)
waited=0
while [[ $waited -lt 10 ]]; do
if docker compose exec -T redis redis-cli ping 2>/dev/null | grep -q PONG; then
log_ok "Redis ready"
break
fi
sleep 1
waited=$((waited + 1))
done
# 6. Prisma db push (теперь БД точно запущена)
log_step "6/8 — Синхронизация схемы БД"
if docker compose exec -T postgres pg_isready -U postgres &>/dev/null; then
npx prisma db push --skip-generate --accept-data-loss 2>&1 | tail -3
log_ok "Схема БД синхронизирована"
else
log_warn "PostgreSQL недоступен — миграция пропущена"
fi
# 7. MinIO bucket
log_step "7/8 — MinIO bucket"
local bucket
bucket=$(env_get S3_BUCKET)
bucket="${bucket:-liveserver}"
local minio_user
minio_user=$(env_get MINIO_ROOT_USER)
minio_user="${minio_user:-minioadmin}"
local minio_pass
minio_pass=$(env_get MINIO_ROOT_PASSWORD)
minio_pass="${minio_pass:-minioadmin}"
# Ждём MinIO (макс 15 сек)
waited=0
while [[ $waited -lt 15 ]]; do
if docker compose exec -T minio mc alias set local http://localhost:9000 "$minio_user" "$minio_pass" &>/dev/null; then
break
fi
sleep 1
waited=$((waited + 1))
done
if docker compose exec -T minio mc alias set local http://localhost:9000 "$minio_user" "$minio_pass" &>/dev/null; then
if docker compose exec -T minio mc ls "local/${bucket}" &>/dev/null; then
log_ok "Bucket '${bucket}' существует"
else
docker compose exec -T minio mc mb "local/${bucket}" &>/dev/null && \
log_ok "Bucket '${bucket}' создан" || \
log_warn "Не удалось создать bucket — создай вручную: http://localhost:9001"
fi
else
log_warn "MinIO ещё не готов — bucket создастся при следующем запуске"
fi
# 8. Очистка .next кеша
log_step "8/8 — Очистка кеша"
[[ -d ".next" ]] && rm -rf .next
log_ok "Кеш очищен"
# Автоопределение BETTER_AUTH_URL (https для dev)
if grep -q "^BETTER_AUTH_URL=" .env 2>/dev/null; then
# Обновить на https если ещё http
sed -i 's|^BETTER_AUTH_URL=http://|BETTER_AUTH_URL=https://|' .env
else
echo "BETTER_AUTH_URL=https://localhost:3000" >> .env
log_ok "BETTER_AUTH_URL=https://localhost:3000 добавлен в .env"
fi
# Проверка порта 3000
if lsof -ti :3000 &>/dev/null; then
log_warn "Порт 3000 всё ещё занят — Next.js выберет другой"
fi
# Запуск
echo ""
log_ok "${GREEN}${BOLD}Готово! Запускаю dev-сервер (HTTPS) на порту 3000...${NC}"
echo ""
exec npm run dev:https
}
# ============================================================
# MAIN — Роутер команд
# ============================================================
print_banner
CMD="${1:-}"
shift 2>/dev/null || true
case "$CMD" in
install)
cmd_install
;;
update)
cmd_update
;;
dev)
cmd_dev
;;
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}dev${NC}${GREEN}обновить + запустить dev-сервер${NC}"
echo -e " ${BOLD}2)${NC} ${CYAN}doctor${NC} — диагностика и исправление"
echo -e " ${BOLD}3)${NC} ${CYAN}update${NC} — обновить проект"
echo -e " ${BOLD}4)${NC} ${CYAN}status${NC} — статус сервисов"
echo -e " ${BOLD}5)${NC} ${CYAN}admin${NC} — создать администратора"
echo -e " ${BOLD}6)${NC} ${CYAN}restart${NC} — перезапуск контейнеров"
echo -e " ${BOLD}7)${NC} ${CYAN}install${NC} — полная переустановка"
echo -e " ${BOLD}8)${NC} ${CYAN}reset${NC}сброс всего"
echo ""
ask "Выбери действие" "1" CHOICE
case "$CHOICE" in
1|dev) cmd_dev ;;
2|doctor) cmd_doctor ;;
3|update) cmd_update ;;
4|status) cmd_status ;;
5|admin) cmd_admin ;;
6|restart) cmd_restart ;;
7|install) cmd_install ;;
8|reset) cmd_reset ;;
*) log_err "Неизвестный выбор: ${CHOICE}"; print_usage ;;
esac
fi
;;
*)
log_err "Неизвестная команда: ${CMD}"
print_usage
exit 1
;;
esac