feat(panel): обновление из репо, обслуживание БД, авто-прунинг, цветные логи и Сторож

- [U] Обновление из репозитория: бэкап -> git pull --ff-only -> npm install -> миграции
  -> рестарт -> health-check; при провале миграций/health предлагает откат (git reset --hard
  + восстановление БД из свежего бэкапа). Текущая версия (git short-hash + subject) в шапке.
- [M] Обслуживание БД: backend/scripts/db-maintain.js (node:sqlite) — integrity_check ->
  WAL checkpoint(TRUNCATE) -> VACUUM; VACUUM пропускается на битой БД. Авто-бэкап + стоп/старт.
- Авто-прунинг бэкапов: Backup-Db хранит последних 10 (Prune-Backups), Copy-DbFrom вынесен
  общим (реюз в Restore-Db и откате обновления), запоминается путь последнего бэкапа.
- Живые логи: отдельный tools/tail-logs.ps1 — раскраска уровней (ERROR/FATAL красным,
  WARN жёлтым, успех зелёным) вместо сырого tail; вынос из inline-команды (PS 5.1 quoting).
- Экран «Сторож»: дашборд в рамке с перерисовкой — статус-маркер, счётчики проверок/
  перезапусков, последнее событие; выход по клавише.
Все .ps1 — UTF-8 BOM, парсинг OK; db-maintain протестирован на копии БД (10.7->10.5 МБ);
рендер-смоук подтвердил выравнивание.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-06-19 23:10:52 +03:00
parent 27f51f1a61
commit 2e9a0ebfb1
3 changed files with 213 additions and 23 deletions
+139 -23
View File
@@ -42,6 +42,14 @@ $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 {
@@ -118,9 +126,9 @@ function Open-Logs {
Write-Host ' Логов нет: сервер не запускался через панель. Запустите его пунктом [1] или [3].' -ForegroundColor Yellow
return
}
$cmd = "`$Host.UI.RawUI.WindowTitle='LearnSpace — живые логи'; Write-Host 'Живые логи (закройте окно, чтобы прекратить просмотр):' -ForegroundColor Cyan; Get-Content -Path '$log' -Wait -Tail 50"
Start-Process powershell -ArgumentList '-NoProfile', '-NoExit', '-Command', $cmd
Write-Host ' Логи открыты в отдельном окне.' -ForegroundColor Green
$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 {
@@ -132,6 +140,34 @@ function Show-Errors {
$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
@@ -139,10 +175,12 @@ function Backup-Db {
$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 Restore-Db {
@@ -160,16 +198,68 @@ function Restore-Db {
if ((Read-Host (' Перезаписать ТЕКУЩУЮ БД из ' + $files[$idx].Name + '? Текущая будет утеряна [y/N]')) -notmatch '^[YyНн]') { Write-Host ' Отменено.'; return }
$wasRunning = [bool](Server-Proc)
if ($wasRunning) { Stop-Server }
if (Test-Path $script:DbPath) { Copy-Item $script:DbPath ($script:DbPath + '.pre-restore') -Force } # страховка
Copy-Item $files[$idx].FullName $script:DbPath -Force
foreach ($ext in '-wal', '-shm') {
$sp = $script:DbPath + $ext; if (Test-Path $sp) { Remove-Item $sp -Force }
$bsrc = $files[$idx].FullName + $ext; if (Test-Path $bsrc) { Copy-Item $bsrc $sp -Force }
}
Write-Host ' БД восстановлена (прежняя сохранена рядом как *.pre-restore).' -ForegroundColor Green
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 }
@@ -183,16 +273,35 @@ function Create-Admin {
}
function Watchdog {
Clear-Host
Write-Host ' Режим «Сторож»: проверка каждые ~8 с, авто-перезапуск при падении.' -ForegroundColor Cyan
Write-Host ' Нажмите любую клавишу для выхода в меню.' -ForegroundColor DarkGray
Write-Host ''
while (-not [Console]::KeyAvailable) {
$t = Get-Date -Format 'HH:mm:ss'
$checks = 0; $restarts = 0; $lastEvent = 'мониторинг запущен'
$exit = $false
while (-not $exit) {
$p = Server-Proc
if ($p) { Write-Host (" [$t] работает (PID $($p.Id))") -ForegroundColor DarkGray }
else { Write-Host (" [$t] СЕРВЕР УПАЛ — поднимаю...") -ForegroundColor Yellow; Start-Server }
for ($i = 0; $i -lt 16; $i++) { if ([Console]::KeyAvailable) { break }; Start-Sleep -Milliseconds 500 }
$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)
}
@@ -268,14 +377,18 @@ while ($run) {
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-Head 'СЕРВЕР' 'ДАННЫЕ И ОБСЛУЖИВАНИЕ'
Menu-Row '[1]' 'Запустить' '[B]' 'Бэкап БД'
Menu-Row '[2]' 'Остановить' '[R]' 'Восстановить БД'
Menu-Row '[3]' 'Перезапустить' '[A]' 'Создать админа'
Menu-Row '[4]' 'Живые логи (окно)' '[W]' 'Сторож (авто-рестарт)'
Menu-Row '[5]' 'Применить миграции' '[E]' 'Ошибки в логах'
Menu-Row '[3]' 'Перезапустить' '[M]' 'Обслуживание БД (vacuum)'
Menu-Row '[4]' 'Живые логи (окно)' '[U]' 'Обновить из репозитория'
Menu-Row '[5]' 'Применить миграции' '[A]' 'Создать админа'
Menu-Row ' ' '' '[W]' 'Сторож (авто-рестарт)'
Menu-Row ' ' '' '[E]' 'Ошибки в логах'
Write-Host ''
Menu-Head 'ДИАГНОСТИКА И ПРОЧЕЕ' ''
Write-Host ' ' -NoNewline
@@ -300,6 +413,8 @@ while ($run) {
'^(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 } }
'^0$' { $run = $false }
@@ -312,3 +427,4 @@ Write-Host ' Остановить позже: stop-server.bat или пун