Files
Learn_System/tools/control-panel.ps1
Maxim Dolgolyov 205290139d feat(control-panel): сброс системы «чистый запуск» (с бэкапом и подтверждением)
Пункт [Z] в control-panel.ps1: предпросмотр → бэкап БД → подтверждение вводом
«СБРОС» → очистка. Скрипт backend/scripts/reset-system.js (dry-run по умолчанию,
выполнение только с --apply --confirm=RESET):
• сохраняет одного админа (min id), переназначает ему авторский контент
  (courses/tests/flashcard_decks/custom_sims/шаблоны/библиотека/lab-ссылки/board);
• стирает всех остальных пользователей + классы/задания/сессии/геймификацию/
  уведомления/прогресс/историю классрума/доступы/логи;
• сохраняет контент: учебники, вопросы, темы, уроки, exam-prep, симуляции,
  биохимия, красная книга, магазин/достижения-определения, роли/права, app_settings;
• обнуляет игровые счётчики админа; классифицирует ВСЕ 119 таблиц, неизвестные не трогает;
• FK off + транзакция + foreign_key_check + VACUUM.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 11:32:44 +03:00

457 lines
27 KiB
PowerShell
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.
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