diff --git a/ka-note/VERSION b/ka-note/VERSION index 0d512a4..168b04a 100644 --- a/ka-note/VERSION +++ b/ka-note/VERSION @@ -1 +1 @@ -1.1.101 \ No newline at end of file +1.1.103 \ No newline at end of file diff --git a/ka-note/deploy.ps1 b/ka-note/deploy.ps1 index af028ea..4cd6995 100644 --- a/ka-note/deploy.ps1 +++ b/ka-note/deploy.ps1 @@ -128,6 +128,74 @@ if ($env:KA_NOTE_DEPLOY_API_KEY) { Write-Host "=== Restart App Service ===" -ForegroundColor Cyan az webapp restart --name $APP --resource-group $RG +Write-Host "=== Post-Deploy DB Validation ===" -ForegroundColor Cyan +Write-Host " Waiting for server to boot (~20s)..." -ForegroundColor DarkGray +$bootDeadline = (Get-Date).AddSeconds(45) +$serverReady = $false +while ((Get-Date) -lt $bootDeadline) { + Start-Sleep -Seconds 3 + try { + $h = Invoke-RestMethod -Uri "$AppUrl/api/health" -Method GET -UseBasicParsing -TimeoutSec 5 + if ($h.status -eq 'ok') { $serverReady = $true; break } + } catch { } +} +if (-not $serverReady) { + Write-Host " WARNING: Server did not respond within 45s" -ForegroundColor Red +} + +$validationOk = $false +if ($serverReady -and $env:KA_NOTE_DEPLOY_API_KEY) { + $authHeader = @{ Authorization = "Bearer $env:KA_NOTE_DEPLOY_API_KEY" } + $maxRetries = 6 + for ($attempt = 1; $attempt -le $maxRetries; $attempt++) { + try { + $stats = Invoke-RestMethod -Uri "$AppUrl/api/admin/stats" -Method GET ` + -Headers $authHeader -UseBasicParsing -TimeoutSec 15 + $contextCount = $stats.contextCount + $topicCount = $stats.topicCount + Write-Host " [$attempt/$maxRetries] Contexts: $contextCount, Topics: $topicCount" -ForegroundColor DarkGray + if ($contextCount -gt 0 -and $topicCount -gt 0) { + Write-Host " DB validation passed." -ForegroundColor Green + $validationOk = $true + break + } + if ($attempt -lt $maxRetries) { Start-Sleep -Seconds 10 } + } catch { + Write-Host " [$attempt/$maxRetries] Request error: $_" -ForegroundColor Yellow + if ($attempt -lt $maxRetries) { Start-Sleep -Seconds 10 } + } + } + if (-not $validationOk) { + Write-Host " DB validation FAILED after $maxRetries attempts" -ForegroundColor Red + } +} elseif (-not $env:KA_NOTE_DEPLOY_API_KEY) { + Write-Host " KA_NOTE_DEPLOY_API_KEY not set, skipping DB validation" -ForegroundColor Yellow + $validationOk = $true +} + +if (-not $validationOk) { + $latestBackup = Get-ChildItem -Path $BackupDir -Filter "*.db" | + Sort-Object LastWriteTime -Descending | Select-Object -First 1 + Write-Host "" + Write-Host " !! DB appears empty or unreachable after deploy !!" -ForegroundColor Red + Write-Host " Pre-deploy backup: $($latestBackup.FullName)" -ForegroundColor Yellow + Write-Host "" + $answer = Read-Host " Restore backup to prod? (yes/no)" + if ($answer -eq 'yes') { + Write-Host " Uploading backup to prod..." -ForegroundColor Cyan + Invoke-WebRequest -Uri $KuduDbUrl ` + -Method PUT ` + -Headers @{ Authorization = "Bearer $KuduToken" } ` + -InFile $latestBackup.FullName -UseBasicParsing | Out-Null + Write-Host " Backup uploaded. Restarting app..." -ForegroundColor Cyan + az webapp restart --name $APP --resource-group $RG + Write-Host " Restore complete. Verify manually: $AppUrl" -ForegroundColor Green + } else { + Write-Host " Restore skipped. Manual intervention required!" -ForegroundColor Red + exit 1 + } +} + Write-Host "=== Done! $VERSION deployed ===" -ForegroundColor Green Write-Host "Check: https://$APP.azurewebsites.net" diff --git a/ka-note/server/ka-note.db-shm b/ka-note/server/ka-note.db-shm index f01edf3..33533b2 100644 Binary files a/ka-note/server/ka-note.db-shm and b/ka-note/server/ka-note.db-shm differ diff --git a/ka-note/server/ka-note.db-wal b/ka-note/server/ka-note.db-wal index 49145d7..3604182 100644 Binary files a/ka-note/server/ka-note.db-wal and b/ka-note/server/ka-note.db-wal differ diff --git a/ka-note/server/src/routes/admin.ts b/ka-note/server/src/routes/admin.ts index 12e5f68..d3134fd 100644 --- a/ka-note/server/src/routes/admin.ts +++ b/ka-note/server/src/routes/admin.ts @@ -3,7 +3,9 @@ import { vacuumPurged } from '../lib/sync-service.js'; import { checkIntegrity } from '../lib/backup-service.js'; import { handle } from '../lib/route-utils.js'; import type { AuthEnv } from '../middleware/auth.js'; -import { sqlite } from '../db/connection.js'; +import { db, sqlite } from '../db/connection.js'; +import { contexts, topics } from '../db/schema.js'; +import { isNull, count } from 'drizzle-orm'; const admin = new Hono(); @@ -19,6 +21,18 @@ admin.get('/integrity', handle('admin/integrity', async (c) => { return c.json(result, result.ok ? 200 : 500); })); +admin.get('/stats', handle('admin/stats', async (c) => { + const [{ contextCount }] = await db + .select({ contextCount: count() }) + .from(contexts) + .where(isNull(contexts.deletedAt)); + const [{ topicCount }] = await db + .select({ topicCount: count() }) + .from(topics) + .where(isNull(topics.deletedAt)); + return c.json({ contextCount, topicCount }); +})); + // Graceful shutdown: checkpoint WAL, close DB, exit. // Call from deploy pipeline before restarting the container. admin.post('/shutdown', handle('admin/shutdown', async (c) => {