← 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 })
  }
}