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:
+139
-23
@@ -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 или пун
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
param([string]$Path)
|
||||
# LearnSpace — просмотр живых логов сервера с раскраской уровней.
|
||||
# Запускается панелью (control-panel.ps1, пункт «Живые логи») в отдельном окне.
|
||||
# ERROR/FATAL — красным, WARN — жёлтым, успех/INFO — зелёным/серым.
|
||||
|
||||
try { chcp 65001 > $null } catch {}
|
||||
try { [Console]::OutputEncoding = [System.Text.UTF8Encoding]::new() } catch {}
|
||||
try { $Host.UI.RawUI.WindowTitle = 'LearnSpace — живые логи' } catch {}
|
||||
|
||||
Write-Host 'Живые логи (закройте окно, чтобы прекратить). ERROR — красным, WARN — жёлтым.' -ForegroundColor Cyan
|
||||
Write-Host ''
|
||||
|
||||
if (-not $Path -or -not (Test-Path $Path)) {
|
||||
Write-Host ('Файл логов не найден: ' + $Path) -ForegroundColor Red
|
||||
Write-Host 'Запустите сервер через панель — лог появится в backend/logs/server.log.' -ForegroundColor DarkGray
|
||||
return
|
||||
}
|
||||
|
||||
Get-Content -Path $Path -Wait -Tail 60 | ForEach-Object {
|
||||
$line = $_
|
||||
$c = 'Gray'
|
||||
if ($line -match '(?i)(\berror\b|\bfatal\b|\bunhandled\b|\bexception\b|error:|✗|✖)') { $c = 'Red' }
|
||||
elseif ($line -match '(?i)(\bwarn\b|\bwarning\b|\bdeprecat)') { $c = 'Yellow' }
|
||||
elseif ($line -match '(?i)(\blistening\b|server running|\bstarted\b|\bready\b|✓)') { $c = 'Green' }
|
||||
elseif ($line -match '(?i)(\binfo\b|\bdebug\b)') { $c = 'DarkGray' }
|
||||
Write-Host $line -ForegroundColor $c
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user