← back to Letsbegin

app/api/live/route.ts

242 lines

import { NextResponse } from 'next/server'
import { exec } from 'child_process'
import { promisify } from 'util'
import * as fs from 'fs'
import * as path from 'path'

const execAsync = promisify(exec)

interface ClaudeProcess {
  pid: number
  cpu: string
  memory: string
  terminal: string
  uptime: string
  command: string
  type: 'interactive' | 'ralph' | 'mcp' | 'unknown'
  project?: string
  status: string
}

interface RalphStatus {
  running: boolean
  project: string
  projectPath: string
  branch: string
  currentStory: string
  progress: string
  completedStories: string[]
  pendingStories: string[]
  runningStory: string | null
  logs: string[]
  elapsed: number
  storyDetails: Array<{
    id: string
    title: string
    status: 'done' | 'running' | 'pending' | 'fail'
    time?: number
  }>
}

export async function GET() {
  try {
    // Get all Claude processes
    const { stdout: psOutput } = await execAsync(
      `ps aux | grep -E "claude|anthropic" | grep -v grep || true`
    )

    const claudeProcesses: ClaudeProcess[] = []
    const lines = psOutput.trim().split('\n').filter(Boolean)

    for (const line of lines) {
      const parts = line.split(/\s+/)
      if (parts.length < 11) continue

      const pid = parseInt(parts[1])
      const cpu = parts[2]
      const mem = parts[3]
      const terminal = parts[6]
      const command = parts.slice(10).join(' ')

      // Determine type
      let type: ClaudeProcess['type'] = 'unknown'
      let project = ''

      if (command.includes('--claude-in-chrome-mcp')) {
        type = 'mcp'
      } else if (parts[0] === 'ralph') {
        type = 'ralph'
        // Try to get project from ralph
        try {
          const { stdout } = await execAsync(`ls -la /proc/${pid}/cwd 2>/dev/null || true`)
          const match = stdout.match(/-> (.+)/)
          if (match) project = match[1]
        } catch {}
      } else if (command === 'claude' || command.includes('claude-code')) {
        type = 'interactive'
      }

      // Get uptime
      let uptime = 'unknown'
      try {
        const { stdout } = await execAsync(`ps -o etime= -p ${pid} 2>/dev/null || true`)
        uptime = stdout.trim() || 'unknown'
      } catch {}

      claudeProcesses.push({
        pid,
        cpu,
        memory: mem,
        terminal,
        uptime,
        command: command.slice(0, 100),
        type,
        project,
        status: 'running'
      })
    }

    // Get Ralph status for all projects with progress.txt
    const ralphProjects: RalphStatus[] = []

    // Scan for projects with Ralph progress files
    const projectDirs = [
      '/root/Projects/jill-website',
      '/root/Projects/angels-flowers',
      '/root/Projects/Designer-Wallcoverings',
      '/root/Projects/dear-bubbe-nextjs',
      '/root/Projects/Letsbegin'
    ]

    for (const projectPath of projectDirs) {
      try {
        const progressPath = path.join(projectPath, 'scripts/ralph/progress.txt')
        const prdPath = path.join(projectPath, 'scripts/ralph/prd.json')

        if (!fs.existsSync(progressPath)) continue

        const progressContent = fs.readFileSync(progressPath, 'utf-8')
        const lines = progressContent.split('\n')

        // Parse progress.txt header
        let branch = ''
        let startTime = ''
        const branchMatch = progressContent.match(/# Branch: (.+)/)
        const startMatch = progressContent.match(/# Started: (.+)/)
        if (branchMatch) branch = branchMatch[1]
        if (startMatch) startTime = startMatch[1]

        // Parse story statuses directly from progress.txt
        const storyResults: Record<string, 'done' | 'running' | 'pending' | 'fail'> = {}
        const storyDetails: RalphStatus['storyDetails'] = []

        for (const line of lines) {
          const match = line.match(/^(US-\d+):\s*(DONE|PASS|FAIL|RUNNING|PENDING)\s*-?\s*(.*)/)
          if (match) {
            const storyId = match[1]
            const status = match[2]
            const title = match[3] || storyId

            let mappedStatus: 'done' | 'running' | 'pending' | 'fail' = 'pending'
            if (status === 'DONE' || status === 'PASS') mappedStatus = 'done'
            else if (status === 'FAIL') mappedStatus = 'fail'
            else if (status === 'RUNNING') mappedStatus = 'running'

            storyResults[storyId] = mappedStatus
            storyDetails.push({ id: storyId, title, status: mappedStatus })
          }
        }

        // If PRD exists, get full titles
        if (fs.existsSync(prdPath)) {
          try {
            const prd = JSON.parse(fs.readFileSync(prdPath, 'utf-8'))
            for (const story of prd.userStories || []) {
              const existing = storyDetails.find(s => s.id === story.id)
              if (existing) {
                existing.title = story.title
              }
            }
          } catch {}
        }

        const completed = storyDetails.filter(s => s.status === 'done').length
        const total = storyDetails.length
        const running = storyDetails.find(s => s.status === 'running')
        const isRunning = running !== undefined

        // Check if Ralph process is actually running for this project
        let hasActiveProcess = false
        try {
          const { stdout } = await execAsync(`ps aux | grep ralph | grep -v grep | grep "${path.basename(projectPath)}" || true`)
          hasActiveProcess = stdout.trim().length > 0
        } catch {}

        // Also check the log file for recent activity
        const logPath = '/tmp/ralph-output.log'
        let logLines: string[] = []
        if (fs.existsSync(logPath)) {
          const logContent = fs.readFileSync(logPath, 'utf-8')
          logLines = logContent.split('\n').slice(-50)
        }

        ralphProjects.push({
          running: isRunning || hasActiveProcess,
          project: path.basename(projectPath),
          projectPath,
          branch,
          currentStory: running?.id || storyDetails[storyDetails.length - 1]?.id || '',
          progress: `${completed}/${total}`,
          completedStories: storyDetails.filter(s => s.status === 'done').map(s => s.id),
          pendingStories: storyDetails.filter(s => s.status === 'pending').map(s => s.id),
          runningStory: running?.id || null,
          logs: logLines,
          elapsed: startTime ? Math.floor((Date.now() - new Date(startTime).getTime()) / 1000) : 0,
          storyDetails
        })
      } catch (err) {
        console.error(`Failed to read Ralph status for ${projectPath}:`, err)
      }
    }

    // Use the first project as primary ralph status (for backwards compatibility)
    const ralphStatus = ralphProjects.length > 0 ? ralphProjects[0] : null

    // Get system stats
    let systemStats = {
      loadAvg: [0, 0, 0],
      memUsed: 0,
      memTotal: 0,
      uptime: ''
    }

    try {
      const { stdout: loadAvg } = await execAsync('cat /proc/loadavg')
      const loads = loadAvg.trim().split(' ')
      systemStats.loadAvg = [parseFloat(loads[0]), parseFloat(loads[1]), parseFloat(loads[2])]

      const { stdout: memInfo } = await execAsync('free -m | grep Mem')
      const memParts = memInfo.trim().split(/\s+/)
      systemStats.memTotal = parseInt(memParts[1])
      systemStats.memUsed = parseInt(memParts[2])

      const { stdout: uptime } = await execAsync('uptime -p')
      systemStats.uptime = uptime.trim()
    } catch {}

    return NextResponse.json({
      timestamp: new Date().toISOString(),
      claudeProcesses,
      ralphStatus,
      ralphProjects,  // All projects with Ralph progress
      systemStats,
      totalProcesses: claudeProcesses.length,
      activeRalph: ralphProjects.some(p => p.running)
    })

  } catch (error) {
    console.error('Live API error:', error)
    return NextResponse.json({ error: 'Failed to get live status' }, { status: 500 })
  }
}