← back to Dear Bubbe Nextjs
app/api/health-check/route.ts
246 lines
import { NextRequest, NextResponse } from 'next/server'
import { exec } from 'child_process'
import { promisify } from 'util'
import fs from 'fs'
import path from 'path'
const execAsync = promisify(exec)
interface HealthCheckResult {
status: 'healthy' | 'warning' | 'critical'
services: {
name: string
status: 'online' | 'offline' | 'error'
details: string
}[]
systemInfo: {
uptime: number
memoryUsage: NodeJS.MemoryUsage
timestamp: string
nodeVersion: string
platform: string
}
suggestions: string[]
}
// Check if PM2 is running
async function checkPM2(): Promise<{ status: 'online' | 'offline' | 'error', details: string }> {
try {
const { stdout } = await execAsync('pm2 status')
if (stdout.includes('bubbe-ai') && stdout.includes('online')) {
return { status: 'online', details: 'PM2 running with bubbe-ai online' }
} else if (stdout.includes('bubbe-ai')) {
return { status: 'error', details: 'bubbe-ai process found but not online' }
} else {
return { status: 'offline', details: 'bubbe-ai process not found in PM2' }
}
} catch (error) {
return { status: 'error', details: `PM2 check failed: ${error instanceof Error ? error.message : String(error)}` }
}
}
// Check disk space
async function checkDiskSpace(): Promise<{ status: 'online' | 'offline' | 'error', details: string }> {
try {
const { stdout } = await execAsync('df -h /')
const lines = stdout.split('\n')
const rootLine = lines.find(line => line.includes('/'))
if (rootLine) {
const parts = rootLine.split(/\s+/)
const usedPercentage = parts[4]
const usedPercent = parseInt(usedPercentage.replace('%', ''))
if (usedPercent > 90) {
return { status: 'error', details: `Disk usage critical: ${usedPercentage}` }
} else if (usedPercent > 80) {
return { status: 'offline', details: `Disk usage high: ${usedPercentage}` }
} else {
return { status: 'online', details: `Disk usage normal: ${usedPercentage}` }
}
}
return { status: 'error', details: 'Could not parse disk usage' }
} catch (error) {
return { status: 'error', details: `Disk check failed: ${error instanceof Error ? error.message : String(error)}` }
}
}
// Check database file exists and is readable
async function checkDatabase(): Promise<{ status: 'online' | 'offline' | 'error', details: string }> {
try {
const dbPath = path.join(process.cwd(), 'bubbe.db')
const memoryPath = path.join(process.cwd(), 'user-memories')
const dbExists = fs.existsSync(dbPath)
const memoryExists = fs.existsSync(memoryPath)
if (dbExists && memoryExists) {
// Check if memory directory has files
const memoryFiles = fs.readdirSync(memoryPath)
return {
status: 'online',
details: `Database online, ${memoryFiles.length} user memories stored`
}
} else if (dbExists) {
return { status: 'offline', details: 'Database exists but memory directory missing' }
} else {
return { status: 'error', details: 'Database file not found' }
}
} catch (error) {
return { status: 'error', details: `Database check failed: ${error instanceof Error ? error.message : String(error)}` }
}
}
// Check API endpoints
async function checkAPIs(): Promise<{ status: 'online' | 'offline' | 'error', details: string }> {
try {
// Test core API endpoints
const testEndpoints = [
'http://localhost:3011/api/stats',
'http://localhost:3011/api/user-profile'
]
let onlineCount = 0
for (const endpoint of testEndpoints) {
try {
const response = await fetch(endpoint, { method: 'GET' })
if (response.ok) {
onlineCount++
}
} catch (e) {
// Endpoint failed
}
}
if (onlineCount === testEndpoints.length) {
return { status: 'online', details: `All ${testEndpoints.length} API endpoints responding` }
} else if (onlineCount > 0) {
return { status: 'offline', details: `${onlineCount}/${testEndpoints.length} API endpoints responding` }
} else {
return { status: 'error', details: 'No API endpoints responding' }
}
} catch (error) {
return { status: 'error', details: `API check failed: ${error instanceof Error ? error.message : String(error)}` }
}
}
export async function GET(request: NextRequest) {
try {
const startTime = Date.now()
// Run all health checks in parallel
const [pm2Status, diskStatus, dbStatus, apiStatus] = await Promise.all([
checkPM2(),
checkDiskSpace(),
checkDatabase(),
checkAPIs()
])
const services = [
{ name: 'PM2 Process', ...pm2Status },
{ name: 'Disk Space', ...diskStatus },
{ name: 'Database', ...dbStatus },
{ name: 'API Endpoints', ...apiStatus }
]
// Determine overall status
const hasError = services.some(s => s.status === 'error')
const hasWarning = services.some(s => s.status === 'offline')
let overallStatus: 'healthy' | 'warning' | 'critical'
if (hasError) {
overallStatus = 'critical'
} else if (hasWarning) {
overallStatus = 'warning'
} else {
overallStatus = 'healthy'
}
// Generate suggestions
const suggestions: string[] = []
if (pm2Status.status !== 'online') {
suggestions.push('Restart PM2 process: pm2 restart bubbe-ai')
}
if (diskStatus.status !== 'online') {
suggestions.push('Clean up disk space or increase storage capacity')
}
if (dbStatus.status !== 'online') {
suggestions.push('Check database permissions and file integrity')
}
if (apiStatus.status !== 'online') {
suggestions.push('Restart application server and check network connectivity')
}
const result: HealthCheckResult = {
status: overallStatus,
services,
systemInfo: {
uptime: process.uptime(),
memoryUsage: process.memoryUsage(),
timestamp: new Date().toISOString(),
nodeVersion: process.version,
platform: process.platform
},
suggestions
}
const responseTime = Date.now() - startTime
console.log(`[HEALTH] Health check completed in ${responseTime}ms, status: ${overallStatus}`)
return NextResponse.json(result)
} catch (error) {
console.error('[HEALTH] Health check failed:', error)
return NextResponse.json({
status: 'critical',
services: [],
systemInfo: {
uptime: process.uptime(),
memoryUsage: process.memoryUsage(),
timestamp: new Date().toISOString(),
nodeVersion: process.version,
platform: process.platform
},
suggestions: ['Health check system encountered an error']
}, { status: 500 })
}
}
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const { action } = body
if (action === 'fix') {
const fixes = []
// Attempt automatic fixes
try {
// Fix 1: Restart PM2 if needed
await execAsync('pm2 restart bubbe-ai')
fixes.push('PM2 process restarted')
} catch (e) {
fixes.push('PM2 restart failed')
}
try {
// Fix 2: Clear temporary files
await execAsync('npm run purge-cache')
fixes.push('Cache cleared')
} catch (e) {
fixes.push('Cache clear failed')
}
return NextResponse.json({
success: true,
message: 'Auto-fix completed',
fixes
})
}
return NextResponse.json({ error: 'Unknown action' }, { status: 400 })
} catch (error) {
console.error('[HEALTH] Auto-fix failed:', error)
return NextResponse.json({ error: 'Auto-fix failed' }, { status: 500 })
}
}