param( [switch]$Start, # неинтерактивно: запустить и выйти [switch]$Stop # неинтерактивно: остановить и выйти ) # LearnSpace — панель управления сервером (консоль-меню). # Запуск: двойной клик по control-panel.bat. Сервер работает в фоне и переживает # закрытие панели. Живые логи открываются в отдельном окне. $ErrorActionPreference = 'Continue' try { chcp 65001 > $null } catch {} try { [Console]::OutputEncoding = [System.Text.UTF8Encoding]::new() } catch {} $root = Split-Path -Parent $PSScriptRoot $backend = Join-Path $root 'backend' $logsDir = Join-Path $backend 'logs' $log = Join-Path $logsDir 'server.log' $errlog = Join-Path $logsDir 'server.err.log' Set-Location $backend # Порт читаем из .env (PORT=...), иначе 3000. (node -e с кавычками PS 5.1 ломает — не используем.) function Get-Port { $envf = Join-Path $backend '.env' if (Test-Path $envf) { $m = Select-String -Path $envf -Pattern '^\s*PORT\s*=\s*(\d+)' | Select-Object -First 1 if ($m) { return [int]$m.Matches[0].Groups[1].Value } } return 3000 } $script:Port = Get-Port # Путь к БД (из .env DB_PATH, иначе стандартный) и папка бэкапов. function Get-DbPath { $envf = Join-Path $backend '.env' if (Test-Path $envf) { $m = Select-String -Path $envf -Pattern '^\s*DB_PATH\s*=\s*(.+)$' | Select-Object -First 1 if ($m) { $v = $m.Matches[0].Groups[1].Value.Trim(); if ($v) { return $v } } } return (Join-Path $backend 'data\learnspace.db') } $script:DbPath = Get-DbPath $script:BackupDir = Join-Path $backend 'data\backups' $script:NodeVer = '?' try { $script:NodeVer = (& node -v) } catch {} # Текущая версия кода (git) — для шапки и отката при обновлении. $script:GitRev = '?'; $script:GitSubj = '' try { $script:GitRev = (& git -C $root rev-parse --short HEAD 2>$null) } catch {} try { $script:GitSubj = (& git -C $root log -1 --format=%s 2>$null) } catch {} $script:KeepBackups = 10 # сколько последних бэкапов хранить (авто-прунинг) $script:LastBackup = $null # путь к последнему сделанному бэкапу (для отката) # Сводка .env (кэш). $script:EnvSum = @{ origin = '—'; jwt = 'нет'; llm = 'нет' } function Load-EnvSummary { $f = Join-Path $backend '.env' if (-not (Test-Path $f)) { return } foreach ($line in (Get-Content $f)) { if ($line -match '^\s*CLIENT_ORIGIN\s*=\s*(.+)$') { $script:EnvSum.origin = $matches[1].Trim() } if ($line -match '^\s*JWT_SECRET\s*=\s*(\S.*)$') { $script:EnvSum.jwt = 'задан' } if ($line -match '^\s*ASSISTANT_LLM_KEY\s*=\s*(\S.*)$') { $script:EnvSum.llm = 'задан' } } } Load-EnvSummary function Server-Proc { try { $c = Get-NetTCPConnection -LocalPort $script:Port -State Listen -ErrorAction Stop | Select-Object -First 1 if ($c) { return Get-Process -Id $c.OwningProcess -ErrorAction SilentlyContinue } } catch {} return $null } # Полный статус (процесс + health + БД), кэшируется в $script:Stat. $script:Stat = $null function Refresh-Status { $proc = Server-Proc $s = @{ running = ($null -ne $proc); procId = '-'; uptime = '-'; health = '—'; healthMs = $null; dbSize = '—'; users = '?'; migr = '-'; at = (Get-Date -Format 'HH:mm:ss'); backups = 0 } try { $s.backups = @(Get-ChildItem $script:BackupDir -Filter 'learnspace-*.db' -ErrorAction SilentlyContinue).Count } catch {} if ($proc) { $s.procId = $proc.Id try { $u = (Get-Date) - $proc.StartTime; $s.uptime = ('{0}ч {1}м' -f [int]$u.TotalHours, $u.Minutes) } catch {} $sw = [System.Diagnostics.Stopwatch]::StartNew() try { Invoke-WebRequest ("http://localhost:{0}/api/health" -f $script:Port) -UseBasicParsing -TimeoutSec 2 | Out-Null $sw.Stop(); $s.health = 'healthy'; $s.healthMs = [int]$sw.ElapsedMilliseconds } catch { $sw.Stop(); $s.health = 'не отвечает' } } if (Test-Path $script:DbPath) { $s.dbSize = ('{0:N1} МБ' -f ((Get-Item $script:DbPath).Length / 1MB)) try { $out = & node scripts/db-status.js "$($script:DbPath)" 2>$null $parts = "$out".Split('|'); if ($parts.Count -ge 2) { $s.users = $parts[0]; $s.migr = $parts[1] } } catch {} } $script:Stat = $s } function Start-Server { if (Server-Proc) { Write-Host ' Сервер уже работает.' -ForegroundColor Yellow; return } New-Item -ItemType Directory -Force -Path $logsDir | Out-Null Write-Host ' Применяю миграции (идемпотентно)...' -ForegroundColor DarkGray try { & node src/db/migrations-runner.js | Out-Host } catch { Write-Host (' Миграции: ' + $_.Exception.Message) -ForegroundColor Red } Remove-Item $log, $errlog -ErrorAction SilentlyContinue Start-Process -FilePath node -ArgumentList 'src/server.js' -WorkingDirectory $backend -WindowStyle Hidden -RedirectStandardOutput $log -RedirectStandardError $errlog | Out-Null for ($i = 0; $i -lt 14; $i++) { Start-Sleep -Milliseconds 600; if (Server-Proc) { break } } if (Server-Proc) { Write-Host (' Сервер запущен на http://localhost:' + $script:Port) -ForegroundColor Green } else { Write-Host ' Не удалось запустить. Последние строки ошибок:' -ForegroundColor Red if (Test-Path $errlog) { Get-Content $errlog -Tail 14 -ErrorAction SilentlyContinue } } } function Stop-Server { $p = Server-Proc if (-not $p) { Write-Host ' Сервер не запущен.' -ForegroundColor Yellow; return } Stop-Process -Id $p.Id -Force -ErrorAction SilentlyContinue Start-Sleep -Milliseconds 600 Write-Host (' Сервер остановлен (PID ' + $p.Id + ').') -ForegroundColor Green } function Open-Logs { if (-not (Test-Path $log)) { Write-Host ' Логов нет: сервер не запускался через панель. Запустите его пунктом [1] или [3].' -ForegroundColor Yellow return } $tail = Join-Path $PSScriptRoot 'tail-logs.ps1' Start-Process powershell -ArgumentList '-NoProfile', '-ExecutionPolicy', 'Bypass', '-NoExit', '-File', $tail, '-Path', $log Write-Host ' Логи открыты в отдельном окне (ERROR — красным, WARN — жёлтым).' -ForegroundColor Green } function Show-Errors { $lines = @() if (Test-Path $errlog) { $lines += (Get-Content $errlog -Tail 20 | Where-Object { $_ -and ($_ -notmatch 'ExperimentalWarning|trace-warnings') }) } if (Test-Path $log) { $lines += (Get-Content $log -Tail 300 | Select-String -Pattern 'ERROR|FATAL|✗|Unhandled|Error:' | Select-Object -Last 15 | ForEach-Object { $_.Line }) } Write-Host '' if (-not $lines -or $lines.Count -eq 0) { Write-Host ' Ошибок в логах не найдено.' -ForegroundColor Green; return } $lines | ForEach-Object { Write-Host (' ' + $_) -ForegroundColor Red } } # Скопировать БД (+wal/shm) из произвольного файла-источника в боевую БД. # Перед перезаписью кладёт страховочную копию *.pre-restore. Возвращает $true при успехе. function Copy-DbFrom($src) { if (-not (Test-Path $src)) { return $false } if (Test-Path $script:DbPath) { Copy-Item $script:DbPath ($script:DbPath + '.pre-restore') -Force } Copy-Item $src $script:DbPath -Force foreach ($ext in '-wal', '-shm') { $sp = $script:DbPath + $ext; if (Test-Path $sp) { Remove-Item $sp -Force } $bsrc = $src + $ext; if (Test-Path $bsrc) { Copy-Item $bsrc $sp -Force } } return $true } # Авто-прунинг: оставить только $KeepBackups последних бэкапов, остальные (+wal/shm) удалить. function Prune-Backups { if (-not (Test-Path $script:BackupDir)) { return } $all = @(Get-ChildItem $script:BackupDir -Filter 'learnspace-*.db' -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending) if ($all.Count -le $script:KeepBackups) { return } $old = $all[$script:KeepBackups..($all.Count - 1)] $n = 0 foreach ($f in $old) { Remove-Item $f.FullName -Force -ErrorAction SilentlyContinue foreach ($ext in '-wal', '-shm') { $sp = $f.FullName + $ext; if (Test-Path $sp) { Remove-Item $sp -Force -ErrorAction SilentlyContinue } } $n++ } if ($n -gt 0) { Write-Host (' Старых бэкапов удалено: ' + $n + ' (храним последних ' + $script:KeepBackups + ').') -ForegroundColor DarkGray } } function Backup-Db { if (-not (Test-Path $script:DbPath)) { Write-Host ' БД не найдена.' -ForegroundColor Yellow; return } New-Item -ItemType Directory -Force -Path $script:BackupDir | Out-Null $ts = Get-Date -Format 'yyyyMMdd-HHmmss' $dest = Join-Path $script:BackupDir ("learnspace-$ts.db") Copy-Item $script:DbPath $dest -Force foreach ($ext in '-wal', '-shm') { $sp = $script:DbPath + $ext; if (Test-Path $sp) { Copy-Item $sp ($dest + $ext) -Force } } $script:LastBackup = $dest $sz = '{0:N1} МБ' -f ((Get-Item $dest).Length / 1MB) Write-Host (' Бэкап создан: ' + (Split-Path $dest -Leaf) + " ($sz)") -ForegroundColor Green Write-Host (' Папка: ' + $script:BackupDir) -ForegroundColor DarkGray if (Server-Proc) { Write-Host ' (сервер работает — для 100% консистентности лучше делать на остановленном)' -ForegroundColor DarkGray } Prune-Backups } function Reset-System { Write-Host '' Write-Host ' ============================================================' -ForegroundColor Red Write-Host ' ВНИМАНИЕ: ЧИСТЫЙ ЗАПУСК - НЕОБРАТИМАЯ ОЧИСТКА' -ForegroundColor Red Write-Host ' ============================================================' -ForegroundColor Red Write-Host ' УДАЛЯТСЯ: все пользователи (кроме одного админа), классы,' -ForegroundColor Yellow Write-Host ' задания, сессии, геймификация, уведомления, прогресс, история.' -ForegroundColor Yellow Write-Host ' СОХРАНЯТСЯ: учебники, вопросы, тесты, курсы, уроки, exam-prep,' -ForegroundColor Gray Write-Host ' симуляции, настройки/права и один админ (контент переходит ему).' -ForegroundColor Gray Write-Host '' Write-Host ' План (предпросмотр, без изменений):' -ForegroundColor Cyan try { & node scripts/reset-system.js | Out-Host } catch { Write-Host (' Ошибка плана: ' + $_.Exception.Message) -ForegroundColor Red; return } Write-Host '' $ans = (Read-Host ' Для подтверждения введите СБРОС (иначе отмена)').Trim().ToUpper() if ($ans -ne 'СБРОС' -and $ans -ne 'RESET') { Write-Host ' Отменено.' -ForegroundColor Yellow; return } if (Server-Proc) { Write-Host ' Сервер работает - остановите его ([2]) перед сбросом для надёжности.' -ForegroundColor Yellow } Write-Host ' Шаг 1/2: бэкап БД...' -ForegroundColor Cyan Backup-Db Write-Host ' Шаг 2/2: очистка...' -ForegroundColor Cyan try { & node scripts/reset-system.js --apply --confirm=RESET | Out-Host } catch { Write-Host (' Ошибка сброса: ' + $_.Exception.Message) -ForegroundColor Red; return } Write-Host ' Готово. Перезапустите сервер ([3]). Бэкап сохранён в data\backups.' -ForegroundColor Green } function Restore-Db { if (-not (Test-Path $script:BackupDir)) { Write-Host ' Папки бэкапов нет — сначала сделайте бэкап ([B]).' -ForegroundColor Yellow; return } $files = @(Get-ChildItem $script:BackupDir -Filter 'learnspace-*.db' -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending) if ($files.Count -eq 0) { Write-Host ' Бэкапов нет.' -ForegroundColor Yellow; return } Write-Host '' for ($i = 0; $i -lt $files.Count; $i++) { Write-Host (' [{0}] {1} ({2:N1} МБ, {3:yyyy-MM-dd HH:mm})' -f ($i + 1), $files[$i].Name, ($files[$i].Length / 1MB), $files[$i].LastWriteTime) } $sel = Read-Host ' Номер бэкапа (Enter = отмена)' if (-not ($sel -match '^\d+$')) { Write-Host ' Отменено.'; return } $idx = [int]$sel - 1 if ($idx -lt 0 -or $idx -ge $files.Count) { Write-Host ' Неверный номер.' -ForegroundColor Yellow; return } if ((Read-Host (' Перезаписать ТЕКУЩУЮ БД из ' + $files[$idx].Name + '? Текущая будет утеряна [y/N]')) -notmatch '^[YyНн]') { Write-Host ' Отменено.'; return } $wasRunning = [bool](Server-Proc) if ($wasRunning) { Stop-Server } if (Copy-DbFrom $files[$idx].FullName) { Write-Host ' БД восстановлена (прежняя сохранена рядом как *.pre-restore).' -ForegroundColor Green } else { Write-Host ' Не удалось скопировать бэкап.' -ForegroundColor Red } if ($wasRunning) { Start-Server } } # Обслуживание БД: бэкап -> проверка целостности -> WAL-checkpoint -> VACUUM (через node:sqlite). function Maintain-Db { if (-not (Test-Path $script:DbPath)) { Write-Host ' БД не найдена.' -ForegroundColor Yellow; return } Write-Host ' Будет: бэкап -> integrity_check -> WAL checkpoint -> VACUUM (компактизация).' -ForegroundColor Gray $wasRunning = [bool](Server-Proc) if ($wasRunning) { if ((Read-Host ' Сервер работает. Остановить на время обслуживания? [Y/n]') -match '^[NnНн]') { Write-Host ' Продолжаю на работающем сервере (VACUUM может быть неполным).' -ForegroundColor Yellow $wasRunning = $false # не перезапускаем, раз не останавливали } else { Stop-Server } } Backup-Db $env:DB_PATH = $script:DbPath try { & node scripts/db-maintain.js | Out-Host } catch { Write-Host (' Ошибка: ' + $_.Exception.Message) -ForegroundColor Red } finally { Remove-Item Env:DB_PATH -ErrorAction SilentlyContinue } if ($wasRunning) { Start-Server } } # Обновление из git-репозитория: бэкап -> pull -> npm install -> миграции -> рестарт -> health. # При неудаче миграций/health — предлагает откат (git reset + восстановление БД из бэкапа). function Update-FromRepo { if (-not (Get-Command git -ErrorAction SilentlyContinue)) { Write-Host ' git не найден в PATH.' -ForegroundColor Red; return } if (-not (Test-Path (Join-Path $root '.git'))) { Write-Host ' Это не git-репозиторий.' -ForegroundColor Red; return } $prev = (& git -C $root rev-parse --short HEAD 2>$null) Write-Host (' Текущая версия: ' + $prev + ' «' + $script:GitSubj + '»') -ForegroundColor Gray Write-Host ' План: бэкап БД -> git pull -> npm install -> миграции -> рестарт -> health.' -ForegroundColor Gray if ((Read-Host ' Обновить сейчас? [y/N]') -notmatch '^[YyНн]') { Write-Host ' Отменено.'; return } $wasRunning = [bool](Server-Proc) Backup-Db Write-Host ' git pull --ff-only ...' -ForegroundColor DarkGray $pull = (& git -C $root pull --ff-only 2>&1 | Out-String) Write-Host (' ' + $pull.Trim()) -ForegroundColor Gray if ($LASTEXITCODE -ne 0) { Write-Host ' git pull не удался (разрешите конфликты вручную). БД не тронута, обновление прервано.' -ForegroundColor Red; return } $new = (& git -C $root rev-parse --short HEAD 2>$null) if ($new -eq $prev) { Write-Host ' Уже последняя версия — обновлять нечего.' -ForegroundColor Green; return } Write-Host (' Обновлено до ' + $new + '. Устанавливаю зависимости (npm install)...') -ForegroundColor DarkGray & npm install --prefix $backend | Out-Host if ($wasRunning) { Stop-Server } Write-Host ' Применяю миграции...' -ForegroundColor DarkGray $migOk = $true try { & node src/db/migrations-runner.js | Out-Host; if ($LASTEXITCODE -ne 0) { $migOk = $false } } catch { $migOk = $false } Start-Server; Start-Sleep -Seconds 1; Refresh-Status $healthy = ($script:Stat.running -and $script:Stat.health -eq 'healthy') if ($migOk -and $healthy) { Write-Host (' Готово. Версия ' + $new + ', сервер healthy.') -ForegroundColor Green; $script:GitRev = $new; return } Write-Host ' ПРОБЛЕМА после обновления (миграции или health не прошли).' -ForegroundColor Red if ((Read-Host (' Откатить код к ' + $prev + ' и восстановить БД из свежего бэкапа? [y/N]')) -match '^[YyНн]') { if (Server-Proc) { Stop-Server } & git -C $root reset --hard $prev | Out-Host if ($script:LastBackup) { [void](Copy-DbFrom $script:LastBackup) } & npm install --prefix $backend | Out-Host Start-Server Write-Host (' Откат выполнен: код ' + $prev + ', БД из бэкапа.') -ForegroundColor Yellow $script:GitRev = $prev } else { Write-Host ' Откат пропущен — разберитесь вручную (логи: пункт [E]).' -ForegroundColor Yellow } } function Create-Admin { $em = Read-Host ' Email админа' if (-not $em.Trim()) { Write-Host ' Отменено.'; return } $nm = Read-Host ' Имя (Enter = «Администратор»)' $sec = Read-Host ' Пароль (мин. 8 символов)' -AsSecureString $pw = [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($sec)) if ($pw.Length -lt 8) { Write-Host ' Пароль слишком короткий (мин. 8).' -ForegroundColor Yellow; return } $env:ADMIN_EMAIL = $em.Trim(); $env:ADMIN_PASSWORD = $pw; $env:ADMIN_NAME = $nm.Trim(); $env:DB_PATH = $script:DbPath try { & node scripts/create-admin.js } catch { Write-Host (' Ошибка: ' + $_.Exception.Message) -ForegroundColor Red } finally { Remove-Item Env:ADMIN_EMAIL, Env:ADMIN_PASSWORD, Env:ADMIN_NAME -ErrorAction SilentlyContinue } } function Watchdog { $checks = 0; $restarts = 0; $lastEvent = 'мониторинг запущен' $exit = $false while (-not $exit) { $p = Server-Proc $checks++ if (-not $p) { $restarts++ $lastEvent = (Get-Date -Format 'HH:mm:ss') + ' — упал, перезапуск #' + $restarts Start-Server | Out-Null $p = Server-Proc } Clear-Host Write-Host '' B-Top B-Line 'СТОРОЖ · авто-перезапуск при падении' Cyan B-Mid if ($p) { $up = '-'; try { $u = (Get-Date) - $p.StartTime; $up = ('{0}ч {1}м' -f [int]$u.TotalHours, $u.Minutes) } catch {} B-Status Green ('РАБОТАЕТ PID ' + $p.Id + ' · аптайм ' + $up) Green } else { B-Status Red 'НЕ ЗАПУЩЕН — не удалось поднять (см. логи)' Red } B-Line ('Проверок: ' + $checks + ' · перезапусков: ' + $restarts) Gray B-Line ('Последняя проверка: ' + (Get-Date -Format 'HH:mm:ss')) DarkGray B-Line ('Событие: ' + $lastEvent) DarkGray B-Bot Write-Host '' Write-Host ' Проверка каждые ~5 с. Нажмите любую клавишу для выхода в меню.' -ForegroundColor DarkGray for ($i = 0; $i -lt 10; $i++) { if ([Console]::KeyAvailable) { $exit = $true; break }; Start-Sleep -Milliseconds 500 } } [void][Console]::ReadKey($true) } function Run-Cmd($title, $block) { Write-Host '' Write-Host (' >>> ' + $title) -ForegroundColor Cyan Write-Host '' & $block Write-Host '' [void](Read-Host ' [Enter] вернуться в меню') } # ── Рисование рамки/дашборда ── $script:W = 62 # внутренняя ширина рамки (символов) function B-Top { Write-Host (' ╔' + ('═' * ($script:W + 2)) + '╗') -ForegroundColor DarkCyan } function B-Mid { Write-Host (' ╠' + ('═' * ($script:W + 2)) + '╣') -ForegroundColor DarkCyan } function B-Bot { Write-Host (' ╚' + ('═' * ($script:W + 2)) + '╝') -ForegroundColor DarkCyan } function B-Line($text, $color) { $t = [string]$text if ($t.Length -gt $script:W) { $t = $t.Substring(0, $script:W) } Write-Host ' ║ ' -ForegroundColor DarkCyan -NoNewline Write-Host $t.PadRight($script:W) -ForegroundColor $color -NoNewline Write-Host ' ║' -ForegroundColor DarkCyan } # Строка с цветным маркером ● слева (статус). Маркер + пробел = 2 столбца. function B-Status($markerColor, $text, $textColor) { $avail = $script:W - 2 $t = [string]$text if ($t.Length -gt $avail) { $t = $t.Substring(0, $avail) } Write-Host ' ║ ' -ForegroundColor DarkCyan -NoNewline Write-Host '●' -ForegroundColor $markerColor -NoNewline Write-Host ' ' -NoNewline Write-Host $t.PadRight($avail) -ForegroundColor $textColor -NoNewline Write-Host ' ║' -ForegroundColor DarkCyan } # Заголовок секции меню (две колонки выравниваются под пунктами). function Menu-Head($left, $right) { Write-Host (' ' + $left.PadRight(34) + $right) -ForegroundColor DarkCyan } # Пункт меню в две колонки: ключ голубой, подпись серая. function Menu-Row($lk, $ll, $rk, $rl) { Write-Host ' ' -NoNewline Write-Host $lk -ForegroundColor Cyan -NoNewline Write-Host ((' ' + $ll).PadRight(31)) -ForegroundColor Gray -NoNewline if ($rk) { Write-Host $rk -ForegroundColor Cyan -NoNewline Write-Host (' ' + $rl) -ForegroundColor Gray } else { Write-Host '' } } # ── Неинтерактивные режимы (для start/stop-server.bat) ── if ($Stop) { Stop-Server; Start-Sleep -Milliseconds 800; exit } if ($Start) { Start-Server; Start-Sleep -Milliseconds 800; exit } # ── Меню ── Refresh-Status $run = $true while ($run) { Clear-Host $s = $script:Stat $migrShort = $s.migr; if ($s.migr -match '^(\d+)') { $migrShort = $matches[1] } Write-Host '' B-Top B-Line 'LearnSpace · Панель управления сервером' Cyan B-Mid if ($s.running) { B-Status Green ('РАБОТАЕТ PID ' + $s.procId + ' · порт ' + $script:Port + ' · аптайм ' + $s.uptime) Green $hc = if ($s.health -eq 'healthy') { 'Green' } else { 'Yellow' } $hm = if ($s.healthMs -ne $null) { " ($($s.healthMs) ms)" } else { '' } B-Line ('Health: ' + $s.health + $hm) $hc } else { B-Status DarkGray 'ОСТАНОВЛЕН' Yellow } B-Line ('БД ' + $s.dbSize + ' · пользователей: ' + $s.users + ' · миграция: ' + $migrShort + ' · бэкапов: ' + $s.backups) Gray B-Line ('Node ' + $script:NodeVer + ' · JWT: ' + $script:EnvSum.jwt + ' · LLM: ' + $script:EnvSum.llm + ' · обновлено ' + $s.at) DarkGray B-Line ('URL ' + $script:EnvSum.origin) DarkGray $verLine = 'Версия ' + $script:GitRev; if ($script:GitSubj) { $verLine += ' · ' + $script:GitSubj } B-Line $verLine DarkGray B-Bot Write-Host '' Menu-Head 'СЕРВЕР' 'ДАННЫЕ И ОБСЛУЖИВАНИЕ' Menu-Row '[1]' 'Запустить' '[B]' 'Бэкап БД' Menu-Row '[2]' 'Остановить' '[R]' 'Восстановить БД' Menu-Row '[3]' 'Перезапустить' '[M]' 'Обслуживание БД (vacuum)' Menu-Row '[4]' 'Живые логи (окно)' '[U]' 'Обновить из репозитория' Menu-Row '[5]' 'Применить миграции' '[A]' 'Создать админа' Menu-Row ' ' '' '[W]' 'Сторож (авто-рестарт)' Menu-Row ' ' '' '[E]' 'Ошибки в логах' Menu-Row ' ' '' '[Z]' 'Сброс системы (чистый запуск)' Write-Host '' Menu-Head 'ДИАГНОСТИКА И ПРОЧЕЕ' '' Write-Host ' ' -NoNewline Write-Host '[6]' -ForegroundColor Cyan -NoNewline; Write-Host ' Тесты ' -ForegroundColor Gray -NoNewline Write-Host '[7]' -ForegroundColor Cyan -NoNewline; Write-Host ' Lint роутов ' -ForegroundColor Gray -NoNewline Write-Host '[8]' -ForegroundColor Cyan -NoNewline; Write-Host ' Сайт ' -ForegroundColor Gray -NoNewline Write-Host '[9]' -ForegroundColor Cyan -NoNewline; Write-Host ' Обновить ' -ForegroundColor Gray -NoNewline Write-Host '[0]' -ForegroundColor Cyan -NoNewline; Write-Host ' Выход' -ForegroundColor Gray Write-Host '' Write-Host ' ▶ ' -ForegroundColor Cyan -NoNewline $c = (Read-Host 'Выбор').Trim().ToUpper() switch -Regex ($c) { '^1$' { Start-Server; Refresh-Status; Start-Sleep 1 } '^2$' { Stop-Server; Refresh-Status; Start-Sleep 1 } '^3$' { Stop-Server; Start-Sleep -Milliseconds 800; Start-Server; Refresh-Status; Start-Sleep 1 } '^4$' { Open-Logs; Start-Sleep 1 } '^5$' { Run-Cmd 'Применение миграций' { & node src/db/migrations-runner.js | Out-Host }; Refresh-Status } '^6$' { Run-Cmd 'Тесты (npm test)' { & npm test } } '^7$' { Run-Cmd 'Проверка авторизации роутов' { & npm run lint:routes } } '^8$' { Start-Process ('http://localhost:' + $script:Port) } '^9$' { Refresh-Status } '^(B|И)$' { Run-Cmd 'Бэкап БД' { Backup-Db } } '^(R|К)$' { Run-Cmd 'Восстановление БД' { Restore-Db }; Refresh-Status } '^(A|Ф)$' { Run-Cmd 'Создать администратора' { Create-Admin } } '^(M|Ь)$' { Run-Cmd 'Обслуживание БД (integrity + vacuum)' { Maintain-Db }; Refresh-Status } '^(U|Г)$' { Run-Cmd 'Обновление из репозитория' { Update-FromRepo }; Refresh-Status } '^(W|Ц)$' { Watchdog; Refresh-Status } '^(E|У)$' { Run-Cmd 'Ошибки в логах' { Show-Errors } } '^(Z|Я)$' { Reset-System; Refresh-Status; Start-Sleep 1 } '^0$' { $run = $false } default { } } } Write-Host '' Write-Host ' Панель закрыта. Сервер, если запущен, продолжает работать в фоне.' -ForegroundColor Cyan Write-Host ' Остановить позже: stop-server.bat или пункт [2] панели.' -ForegroundColor DarkGray