← back to Letsbegin
app/live/page.tsx
945 lines
'use client'
import { useState, useEffect, useRef } from 'react'
import {
Activity,
Cpu,
MemoryStick,
Terminal,
GitBranch,
CheckCircle2,
Circle,
Loader2,
XCircle,
RefreshCw,
Zap,
Clock,
Server,
ChevronDown,
ChevronRight,
AlertTriangle,
Play,
Square,
Info,
Folder,
FileText,
Layers,
ArrowLeft
} from 'lucide-react'
import Link from 'next/link'
interface ClaudeProcess {
pid: number
cpu: string
memory: string
terminal: string
uptime: string
command: string
type: 'interactive' | 'ralph' | 'mcp' | 'unknown'
project?: string
status: string
}
interface StoryDetail {
id: string
title: string
status: 'done' | 'running' | 'pending' | 'fail'
time?: number
}
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: StoryDetail[]
}
interface LiveData {
timestamp: string
claudeProcesses: ClaudeProcess[]
ralphStatus: RalphStatus | null
systemStats: {
loadAvg: number[]
memUsed: number
memTotal: number
uptime: string
}
totalProcesses: number
activeRalph: boolean
}
interface RalphProject {
project: string
projectPath: string
pid: number
cpu: string
memory: string
status: 'running' | 'completed' | 'failed' | 'idle'
currentStory: string
startTime: string
progress: string
}
interface ProcessDetails {
status: string
message: string
processInfo: {
pid: number
state: string
cpu: number
memory: number
elapsed: string
command: string
} | null
detailedInfo: {
cwd?: string
cmdline?: string
openFiles?: number
threads?: number
VmRSS?: string
VmSize?: string
VmPeak?: string
ralphStatus?: {
running: boolean
currentStory: string
progress: string
lastLog: string[]
}
stuckIndicators?: string[]
}
stateDescription?: string
}
function formatElapsed(seconds: number): string {
const hrs = Math.floor(seconds / 3600)
const mins = Math.floor((seconds % 3600) / 60)
const secs = seconds % 60
if (hrs > 0) return `${hrs}h ${mins}m`
if (mins > 0) return `${mins}m ${secs}s`
return `${secs}s`
}
function ProcessTypeIcon({ type }: { type: ClaudeProcess['type'] }) {
switch (type) {
case 'interactive':
return <Terminal className="w-4 h-4 text-blue-400" />
case 'ralph':
return <Zap className="w-4 h-4 text-yellow-400" />
case 'mcp':
return <Server className="w-4 h-4 text-purple-400" />
default:
return <Circle className="w-4 h-4 text-gray-400" />
}
}
function StoryStatusIcon({ status }: { status: StoryDetail['status'] }) {
switch (status) {
case 'done':
return <CheckCircle2 className="w-5 h-5 text-green-500" />
case 'running':
return <Loader2 className="w-5 h-5 text-yellow-500 animate-spin" />
case 'fail':
return <XCircle className="w-5 h-5 text-red-500" />
default:
return <Circle className="w-5 h-5 text-gray-500" />
}
}
function StateIndicator({ state }: { state: string }) {
const colors: Record<string, string> = {
'R': 'bg-green-500',
'S': 'bg-blue-500',
'D': 'bg-red-500',
'T': 'bg-yellow-500',
'Z': 'bg-gray-500',
}
return (
<span className={`inline-block w-2 h-2 rounded-full ${colors[state] || 'bg-gray-400'}`} />
)
}
export default function LiveDashboard() {
const [data, setData] = useState<LiveData | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [autoRefresh, setAutoRefresh] = useState(true)
const [selectedPid, setSelectedPid] = useState<number | null>(null)
const [processDetails, setProcessDetails] = useState<ProcessDetails | null>(null)
const [checkingProcess, setCheckingProcess] = useState(false)
const [restartingProcess, setRestartingProcess] = useState(false)
const [showProcessTable, setShowProcessTable] = useState(true)
const [allProjects, setAllProjects] = useState<RalphProject[]>([])
const [bottomBarExpanded, setBottomBarExpanded] = useState(true)
const logsEndRef = useRef<HTMLDivElement>(null)
const fetchProjects = async () => {
try {
const res = await fetch('/api/processes')
if (!res.ok) return
const json = await res.json()
setAllProjects(json.processes || [])
} catch (err) {
// Silent fail for projects bar
}
}
const fetchData = async () => {
try {
const res = await fetch('/api/live')
if (!res.ok) throw new Error('Failed to fetch')
const json = await res.json()
setData(json)
setError(null)
} catch (err) {
setError('Failed to fetch live data')
} finally {
setLoading(false)
}
}
const checkProcess = async (pid: number) => {
setCheckingProcess(true)
try {
const res = await fetch('/api/live/check', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ pid })
})
const json = await res.json()
setProcessDetails(json)
} catch (err) {
setProcessDetails({
status: 'error',
message: 'Failed to check process',
processInfo: null,
detailedInfo: {}
})
} finally {
setCheckingProcess(false)
}
}
const restartProcess = async (pid: number) => {
setRestartingProcess(true)
try {
const res = await fetch('/api/live/check', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ pid, action: 'restart' })
})
const json = await res.json()
setProcessDetails(json)
// Refresh the main data after restart
setTimeout(fetchData, 2000)
} catch (err) {
setProcessDetails({
status: 'error',
message: 'Failed to restart process',
processInfo: null,
detailedInfo: {}
})
} finally {
setRestartingProcess(false)
}
}
useEffect(() => {
fetchData()
fetchProjects()
if (autoRefresh) {
const interval = setInterval(() => {
fetchData()
fetchProjects()
}, 3000)
return () => clearInterval(interval)
}
}, [autoRefresh])
useEffect(() => {
if (selectedPid) {
checkProcess(selectedPid)
}
}, [selectedPid])
useEffect(() => {
logsEndRef.current?.scrollIntoView({ behavior: 'smooth' })
}, [data?.ralphStatus?.logs])
if (loading && !data) {
return (
<div className="min-h-screen bg-gray-950 flex items-center justify-center">
<Loader2 className="w-8 h-8 text-blue-500 animate-spin" />
</div>
)
}
return (
<div className="min-h-screen bg-gray-950 text-gray-100 p-6">
{/* Header */}
<header className="mb-8">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Link
href="/"
className="p-2 rounded-lg hover:bg-gray-800 transition-colors"
>
<ArrowLeft className="w-5 h-5 text-gray-400" />
</Link>
<Activity className="w-8 h-8 text-blue-500" />
<h1 className="text-2xl font-bold">Claude Live Dashboard</h1>
<span className="px-2 py-1 text-xs rounded-full bg-green-500/20 text-green-400 flex items-center gap-1">
<span className="w-2 h-2 rounded-full bg-green-500 animate-pulse" />
Live
</span>
</div>
<div className="flex items-center gap-4">
<button
onClick={() => setAutoRefresh(!autoRefresh)}
className={`px-3 py-1.5 rounded-lg text-sm flex items-center gap-2 transition-colors ${
autoRefresh
? 'bg-blue-500/20 text-blue-400 border border-blue-500/30'
: 'bg-gray-800 text-gray-400 border border-gray-700'
}`}
>
<RefreshCw className={`w-4 h-4 ${autoRefresh ? 'animate-spin' : ''}`} />
Auto-refresh {autoRefresh ? 'ON' : 'OFF'}
</button>
<span className="text-sm text-gray-500">
{data?.timestamp ? new Date(data.timestamp).toLocaleTimeString() : ''}
</span>
</div>
</div>
</header>
{error && (
<div className="mb-6 p-4 rounded-lg bg-red-500/20 border border-red-500/30 text-red-400">
{error}
</div>
)}
{/* System Stats */}
<div className="grid grid-cols-4 gap-4 mb-8">
<div className="p-4 rounded-xl bg-gray-900 border border-gray-800">
<div className="flex items-center gap-2 text-gray-400 text-sm mb-2">
<Cpu className="w-4 h-4" />
CPU Load
</div>
<div className="text-2xl font-bold">
{data?.systemStats?.loadAvg?.[0]?.toFixed(2) || '0.00'}
</div>
<div className="text-xs text-gray-500 mt-1">
5m: {data?.systemStats?.loadAvg?.[1]?.toFixed(2)} | 15m: {data?.systemStats?.loadAvg?.[2]?.toFixed(2)}
</div>
</div>
<div className="p-4 rounded-xl bg-gray-900 border border-gray-800">
<div className="flex items-center gap-2 text-gray-400 text-sm mb-2">
<MemoryStick className="w-4 h-4" />
Memory
</div>
<div className="text-2xl font-bold">
{data?.systemStats?.memUsed || 0} MB
</div>
<div className="text-xs text-gray-500 mt-1">
of {data?.systemStats?.memTotal || 0} MB ({Math.round((data?.systemStats?.memUsed || 0) / (data?.systemStats?.memTotal || 1) * 100)}%)
</div>
</div>
<div className="p-4 rounded-xl bg-gray-900 border border-gray-800">
<div className="flex items-center gap-2 text-gray-400 text-sm mb-2">
<Terminal className="w-4 h-4" />
Claude Processes
</div>
<div className="text-2xl font-bold text-blue-400">
{data?.totalProcesses || 0}
</div>
<div className="text-xs text-gray-500 mt-1">
Active instances
</div>
</div>
<div className="p-4 rounded-xl bg-gray-900 border border-gray-800">
<div className="flex items-center gap-2 text-gray-400 text-sm mb-2">
<Clock className="w-4 h-4" />
System Uptime
</div>
<div className="text-lg font-bold">
{data?.systemStats?.uptime || 'Unknown'}
</div>
</div>
</div>
{/* Collapsible Claude Processes Table */}
<div className="mb-8 rounded-xl bg-gray-900 border border-gray-800 overflow-hidden">
<button
onClick={() => setShowProcessTable(!showProcessTable)}
className="w-full px-4 py-3 border-b border-gray-800 flex items-center gap-2 hover:bg-gray-800/50 transition-colors"
>
{showProcessTable ? (
<ChevronDown className="w-5 h-5 text-gray-400" />
) : (
<ChevronRight className="w-5 h-5 text-gray-400" />
)}
<Terminal className="w-5 h-5 text-blue-400" />
<h2 className="font-semibold">Claude Processes</h2>
<span className="ml-auto px-2 py-0.5 text-xs rounded-full bg-blue-500/20 text-blue-400">
{data?.claudeProcesses?.length || 0} running
</span>
</button>
{showProcessTable && (
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead className="bg-gray-800/50">
<tr>
<th className="px-4 py-2 text-left text-gray-400 font-medium">Select</th>
<th className="px-4 py-2 text-left text-gray-400 font-medium">PID</th>
<th className="px-4 py-2 text-left text-gray-400 font-medium">Type</th>
<th className="px-4 py-2 text-left text-gray-400 font-medium">Terminal</th>
<th className="px-4 py-2 text-left text-gray-400 font-medium">CPU %</th>
<th className="px-4 py-2 text-left text-gray-400 font-medium">Mem %</th>
<th className="px-4 py-2 text-left text-gray-400 font-medium">Uptime</th>
<th className="px-4 py-2 text-left text-gray-400 font-medium">Command</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-800">
{data?.claudeProcesses?.map((proc) => (
<tr
key={proc.pid}
className={`hover:bg-gray-800/50 transition-colors cursor-pointer ${
selectedPid === proc.pid ? 'bg-blue-500/10 border-l-2 border-blue-500' : ''
}`}
onClick={() => setSelectedPid(proc.pid)}
>
<td className="px-4 py-3">
<input
type="radio"
name="selectedProcess"
checked={selectedPid === proc.pid}
onChange={() => setSelectedPid(proc.pid)}
className="w-4 h-4 text-blue-500 bg-gray-800 border-gray-600 focus:ring-blue-500"
/>
</td>
<td className="px-4 py-3 font-mono text-gray-300">{proc.pid}</td>
<td className="px-4 py-3">
<span className={`inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full text-xs ${
proc.type === 'interactive' ? 'bg-blue-500/20 text-blue-400' :
proc.type === 'ralph' ? 'bg-yellow-500/20 text-yellow-400' :
proc.type === 'mcp' ? 'bg-purple-500/20 text-purple-400' :
'bg-gray-500/20 text-gray-400'
}`}>
<ProcessTypeIcon type={proc.type} />
{proc.type}
</span>
</td>
<td className="px-4 py-3 text-gray-400">{proc.terminal}</td>
<td className="px-4 py-3 text-gray-300">{proc.cpu}</td>
<td className="px-4 py-3 text-gray-300">{proc.memory}</td>
<td className="px-4 py-3 text-gray-400">{proc.uptime}</td>
<td className="px-4 py-3 text-gray-500 truncate max-w-xs">{proc.command}</td>
</tr>
))}
{(!data?.claudeProcesses || data.claudeProcesses.length === 0) && (
<tr>
<td colSpan={8} className="px-4 py-8 text-center text-gray-500">
No Claude processes running
</td>
</tr>
)}
</tbody>
</table>
</div>
)}
</div>
{/* Selected Process Details */}
{selectedPid && (
<div className="mb-8 rounded-xl bg-gray-900 border border-gray-800 overflow-hidden">
<div className="px-4 py-3 border-b border-gray-800 flex items-center justify-between">
<div className="flex items-center gap-2">
<Info className="w-5 h-5 text-blue-400" />
<h2 className="font-semibold">Process Details - PID {selectedPid}</h2>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => checkProcess(selectedPid)}
disabled={checkingProcess}
className="px-3 py-1.5 rounded-lg text-sm flex items-center gap-2 bg-blue-500/20 text-blue-400 border border-blue-500/30 hover:bg-blue-500/30 transition-colors disabled:opacity-50"
>
{checkingProcess ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<RefreshCw className="w-4 h-4" />
)}
Check Status
</button>
<button
onClick={() => restartProcess(selectedPid)}
disabled={restartingProcess}
className="px-3 py-1.5 rounded-lg text-sm flex items-center gap-2 bg-red-500/20 text-red-400 border border-red-500/30 hover:bg-red-500/30 transition-colors disabled:opacity-50"
>
{restartingProcess ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<Square className="w-4 h-4" />
)}
Kill & Restart
</button>
<button
onClick={() => {
setSelectedPid(null)
setProcessDetails(null)
}}
className="px-3 py-1.5 rounded-lg text-sm bg-gray-800 text-gray-400 hover:bg-gray-700 transition-colors"
>
Close
</button>
</div>
</div>
{checkingProcess && !processDetails ? (
<div className="px-4 py-8 flex items-center justify-center">
<Loader2 className="w-6 h-6 text-blue-500 animate-spin" />
</div>
) : processDetails ? (
<div className="p-4">
{/* Status Banner */}
<div className={`mb-4 p-4 rounded-lg border ${
processDetails.status === 'running' ? 'bg-green-500/10 border-green-500/30' :
processDetails.status === 'stuck' ? 'bg-yellow-500/10 border-yellow-500/30' :
processDetails.status === 'dead' ? 'bg-red-500/10 border-red-500/30' :
processDetails.status === 'killed' || processDetails.status === 'restarted' ? 'bg-blue-500/10 border-blue-500/30' :
'bg-gray-500/10 border-gray-500/30'
}`}>
<div className="flex items-center gap-2">
{processDetails.status === 'running' && <CheckCircle2 className="w-5 h-5 text-green-500" />}
{processDetails.status === 'stuck' && <AlertTriangle className="w-5 h-5 text-yellow-500" />}
{processDetails.status === 'dead' && <XCircle className="w-5 h-5 text-red-500" />}
{(processDetails.status === 'killed' || processDetails.status === 'restarted') && <CheckCircle2 className="w-5 h-5 text-blue-500" />}
<span className={`font-medium ${
processDetails.status === 'running' ? 'text-green-400' :
processDetails.status === 'stuck' ? 'text-yellow-400' :
processDetails.status === 'dead' ? 'text-red-400' :
'text-blue-400'
}`}>
{processDetails.message}
</span>
</div>
{processDetails.stateDescription && (
<div className="mt-2 text-sm text-gray-400">
State: {processDetails.stateDescription}
</div>
)}
</div>
{/* Stuck Indicators */}
{processDetails.detailedInfo?.stuckIndicators && processDetails.detailedInfo.stuckIndicators.length > 0 && (
<div className="mb-4 p-4 rounded-lg bg-yellow-500/10 border border-yellow-500/30">
<div className="flex items-center gap-2 mb-2">
<AlertTriangle className="w-5 h-5 text-yellow-500" />
<span className="font-medium text-yellow-400">Potential Issues Detected</span>
</div>
<ul className="list-disc list-inside text-sm text-yellow-300">
{processDetails.detailedInfo.stuckIndicators.map((indicator, i) => (
<li key={i}>{indicator}</li>
))}
</ul>
</div>
)}
{/* Process Info Grid */}
{processDetails.processInfo && (
<div className="grid grid-cols-3 gap-4 mb-4">
<div className="p-3 rounded-lg bg-gray-800/50">
<div className="text-xs text-gray-500 mb-1">State</div>
<div className="flex items-center gap-2">
<StateIndicator state={processDetails.processInfo.state} />
<span className="font-mono text-gray-300">{processDetails.processInfo.state}</span>
</div>
</div>
<div className="p-3 rounded-lg bg-gray-800/50">
<div className="text-xs text-gray-500 mb-1">CPU</div>
<div className="font-mono text-gray-300">{processDetails.processInfo.cpu}%</div>
</div>
<div className="p-3 rounded-lg bg-gray-800/50">
<div className="text-xs text-gray-500 mb-1">Memory</div>
<div className="font-mono text-gray-300">{processDetails.processInfo.memory}%</div>
</div>
<div className="p-3 rounded-lg bg-gray-800/50">
<div className="text-xs text-gray-500 mb-1">Elapsed</div>
<div className="font-mono text-gray-300">{processDetails.processInfo.elapsed}</div>
</div>
<div className="p-3 rounded-lg bg-gray-800/50">
<div className="text-xs text-gray-500 mb-1">Open Files</div>
<div className="font-mono text-gray-300">{processDetails.detailedInfo?.openFiles || 'N/A'}</div>
</div>
<div className="p-3 rounded-lg bg-gray-800/50">
<div className="text-xs text-gray-500 mb-1">Threads</div>
<div className="font-mono text-gray-300">{processDetails.detailedInfo?.threads || 'N/A'}</div>
</div>
</div>
)}
{/* Memory Details */}
{(processDetails.detailedInfo?.VmRSS || processDetails.detailedInfo?.VmSize) && (
<div className="mb-4 p-4 rounded-lg bg-gray-800/50">
<div className="flex items-center gap-2 mb-3">
<MemoryStick className="w-4 h-4 text-purple-400" />
<span className="font-medium text-gray-300">Memory Details</span>
</div>
<div className="grid grid-cols-3 gap-4 text-sm">
<div>
<div className="text-gray-500">Resident (VmRSS)</div>
<div className="font-mono text-gray-300">{processDetails.detailedInfo.VmRSS || 'N/A'}</div>
</div>
<div>
<div className="text-gray-500">Virtual (VmSize)</div>
<div className="font-mono text-gray-300">{processDetails.detailedInfo.VmSize || 'N/A'}</div>
</div>
<div>
<div className="text-gray-500">Peak (VmPeak)</div>
<div className="font-mono text-gray-300">{processDetails.detailedInfo.VmPeak || 'N/A'}</div>
</div>
</div>
</div>
)}
{/* Working Directory */}
{processDetails.detailedInfo?.cwd && (
<div className="mb-4 p-4 rounded-lg bg-gray-800/50">
<div className="flex items-center gap-2 mb-2">
<Folder className="w-4 h-4 text-blue-400" />
<span className="font-medium text-gray-300">Working Directory</span>
</div>
<code className="text-sm text-gray-400 break-all">{processDetails.detailedInfo.cwd}</code>
</div>
)}
{/* Command Line */}
{processDetails.detailedInfo?.cmdline && (
<div className="mb-4 p-4 rounded-lg bg-gray-800/50">
<div className="flex items-center gap-2 mb-2">
<FileText className="w-4 h-4 text-green-400" />
<span className="font-medium text-gray-300">Command Line</span>
</div>
<code className="text-sm text-gray-400 break-all">{processDetails.detailedInfo.cmdline}</code>
</div>
)}
{/* Ralph Status (if applicable) */}
{processDetails.detailedInfo?.ralphStatus && (
<div className="p-4 rounded-lg bg-yellow-500/10 border border-yellow-500/30">
<div className="flex items-center gap-2 mb-3">
<Zap className="w-4 h-4 text-yellow-400" />
<span className="font-medium text-yellow-400">Ralph Automation Status</span>
</div>
<div className="grid grid-cols-3 gap-4 text-sm mb-3">
<div>
<div className="text-gray-500">Running</div>
<div className={processDetails.detailedInfo.ralphStatus.running ? 'text-green-400' : 'text-gray-400'}>
{processDetails.detailedInfo.ralphStatus.running ? 'Yes' : 'No'}
</div>
</div>
<div>
<div className="text-gray-500">Current Story</div>
<div className="text-gray-300">{processDetails.detailedInfo.ralphStatus.currentStory || 'N/A'}</div>
</div>
<div>
<div className="text-gray-500">Progress</div>
<div className="text-gray-300">{processDetails.detailedInfo.ralphStatus.progress}</div>
</div>
</div>
{processDetails.detailedInfo.ralphStatus.lastLog && processDetails.detailedInfo.ralphStatus.lastLog.length > 0 && (
<div>
<div className="text-gray-500 text-sm mb-1">Recent Logs</div>
<div className="bg-black/30 rounded p-2 font-mono text-xs">
{processDetails.detailedInfo.ralphStatus.lastLog.map((log, i) => (
<div key={i} className="text-gray-400">{log}</div>
))}
</div>
</div>
)}
</div>
)}
</div>
) : (
<div className="px-4 py-8 text-center text-gray-500">
Click "Check Status" to load process details
</div>
)}
</div>
)}
{/* Ralph Status */}
<div className="grid grid-cols-1 gap-6">
<div className="rounded-xl bg-gray-900 border border-gray-800 overflow-hidden">
<div className="px-4 py-3 border-b border-gray-800 flex items-center gap-2">
<Zap className="w-5 h-5 text-yellow-400" />
<h2 className="font-semibold">Ralph Automation</h2>
{data?.ralphStatus?.runningStory && (
<span className="ml-auto px-2 py-0.5 text-xs rounded-full bg-yellow-500/20 text-yellow-400 flex items-center gap-1">
<Loader2 className="w-3 h-3 animate-spin" />
Running
</span>
)}
</div>
{data?.ralphStatus ? (
<div>
{/* Project Info */}
<div className="px-4 py-3 border-b border-gray-800 bg-gray-800/30">
<div className="flex items-center gap-2 mb-2">
<GitBranch className="w-4 h-4 text-purple-400" />
<span className="font-medium text-purple-400">{data.ralphStatus.project}</span>
</div>
<div className="text-xs text-gray-500 font-mono truncate">
{data.ralphStatus.branch}
</div>
<div className="flex items-center gap-4 mt-2 text-sm">
<span className="text-gray-400">
Progress: <span className="text-green-400 font-bold">{data.ralphStatus.progress}</span>
</span>
<span className="text-gray-400">
Elapsed: <span className="text-blue-400">{formatElapsed(data.ralphStatus.elapsed)}</span>
</span>
</div>
</div>
{/* Story List */}
<div className="divide-y divide-gray-800 max-h-[300px] overflow-y-auto">
{data.ralphStatus.storyDetails.map((story) => (
<div
key={story.id}
className={`px-4 py-2 flex items-center gap-3 ${
story.status === 'running' ? 'bg-yellow-500/10' : ''
}`}
>
<StoryStatusIcon status={story.status} />
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="font-mono text-sm text-gray-300">{story.id}</span>
{story.status === 'running' && (
<span className="text-xs text-yellow-400">← Current</span>
)}
</div>
<div className="text-xs text-gray-500 truncate">{story.title}</div>
</div>
{story.time && (
<span className="text-xs text-gray-500">{formatElapsed(story.time)}</span>
)}
</div>
))}
</div>
{/* Logs - Detailed View */}
<div className="border-t border-gray-800">
<div className="px-4 py-2 text-xs text-gray-500 bg-gray-800/30 flex items-center justify-between">
<div className="flex items-center gap-2">
<span>Live Logs</span>
<span className="flex items-center gap-1 px-2 py-0.5 rounded-full bg-green-500/20 text-green-400">
<span className="w-1.5 h-1.5 rounded-full bg-green-500 animate-pulse" />
LIVE
</span>
</div>
<span className="text-gray-600">{data.ralphStatus.logs.length} lines</span>
</div>
<div className="px-4 py-2 min-h-[300px] max-h-[500px] overflow-y-auto bg-black/50 font-mono text-xs">
{data.ralphStatus.logs.map((log, i) => {
const isSuccess = log.includes('✓') || log.includes('Completed') || log.includes('PASS') || log.includes('success')
const isError = log.includes('Error') || log.includes('Failed') || log.includes('FAIL') || log.includes('❌')
const isWarning = log.includes('Warning') || log.includes('⚠')
const isRunning = log.includes('Running') || log.includes('Starting') || log.includes('...')
const isFile = log.includes('.ts') || log.includes('.tsx') || log.includes('.js')
const isCommand = log.startsWith('$') || log.startsWith('npm') || log.startsWith('git')
const isStory = log.includes('US-') || log.includes('Story')
return (
<div key={i} className={`py-0.5 leading-relaxed ${
isSuccess ? 'text-green-400' :
isError ? 'text-red-400 font-medium' :
isWarning ? 'text-yellow-400' :
isStory ? 'text-purple-400 font-medium' :
isRunning ? 'text-blue-400' :
isFile ? 'text-cyan-400' :
isCommand ? 'text-orange-400' :
'text-gray-400'
}`}>
{log}
</div>
)
})}
{data.ralphStatus.running && (
<div className="flex items-center gap-2 mt-2 text-blue-400">
<span className="inline-block w-2 h-3 bg-blue-400 animate-pulse" />
<span className="animate-pulse">Processing...</span>
</div>
)}
<div ref={logsEndRef} />
</div>
{/* Legend */}
<div className="px-4 py-2 border-t border-gray-800 bg-gray-900/50 flex items-center gap-3 text-xs text-gray-500">
<span>Legend:</span>
<span className="text-green-400">● Success</span>
<span className="text-red-400">● Error</span>
<span className="text-yellow-400">● Warning</span>
<span className="text-purple-400">● Story</span>
<span className="text-blue-400">● Running</span>
<span className="text-cyan-400">● Files</span>
</div>
</div>
</div>
) : (
<div className="px-4 py-8 text-center text-gray-500">
No Ralph automation running
</div>
)}
</div>
</div>
{/* Spacer for fixed bottom bar */}
<div className={bottomBarExpanded ? "h-48" : "h-12"} />
{/* Fixed Bottom Bar - Collapsible All Processes */}
<div className="fixed bottom-0 left-0 right-0 bg-gray-900/95 backdrop-blur-sm border-t border-gray-700 z-50">
{/* Header - Always visible */}
<button
onClick={() => setBottomBarExpanded(!bottomBarExpanded)}
className="w-full px-4 py-2 flex items-center justify-between hover:bg-gray-800/50 transition-colors"
>
<div className="flex items-center gap-2 text-xs text-gray-400">
<Layers className="w-4 h-4" />
<span className="font-medium">All Recent Processes</span>
<span className="text-gray-600">
({allProjects.length} projects, {data?.claudeProcesses?.length || 0} claude)
</span>
{allProjects.some(p => p.status === 'running') && (
<span className="flex items-center gap-1 text-yellow-400">
<Loader2 className="w-3 h-3 animate-spin" />
<span>Active</span>
</span>
)}
</div>
<div className="flex items-center gap-2">
{bottomBarExpanded ? (
<ChevronDown className="w-4 h-4 text-gray-400" />
) : (
<ChevronRight className="w-4 h-4 text-gray-400" />
)}
</div>
</button>
{/* Expanded Content */}
{bottomBarExpanded && (
<div className="px-4 pb-3 max-h-[300px] overflow-y-auto">
{/* Ralphy Boy Projects */}
<div className="mb-3">
<div className="text-xs text-gray-500 mb-2 flex items-center gap-2">
<Zap className="w-3 h-3" />
Ralphy Boy Projects
</div>
<div className="flex gap-2 overflow-x-auto pb-2">
{allProjects.length === 0 ? (
<div className="text-sm text-gray-600">No projects tracked</div>
) : (
allProjects.map((proj) => (
<div
key={proj.project}
className={`flex-shrink-0 px-3 py-2 rounded-lg border transition-all ${
proj.status === 'running'
? 'bg-yellow-900/30 border-yellow-600/50 shadow-lg shadow-yellow-900/20'
: proj.status === 'completed'
? 'bg-green-900/20 border-green-700/40'
: proj.status === 'failed'
? 'bg-red-900/20 border-red-700/40'
: 'bg-gray-800/50 border-gray-700/50'
}`}
>
<div className="flex items-center gap-2 mb-1">
{proj.status === 'running' ? (
<Loader2 className="w-4 h-4 text-yellow-400 animate-spin" />
) : proj.status === 'completed' ? (
<CheckCircle2 className="w-4 h-4 text-green-400" />
) : proj.status === 'failed' ? (
<XCircle className="w-4 h-4 text-red-400" />
) : (
<Circle className="w-4 h-4 text-gray-500" />
)}
<span className="font-medium text-sm text-white truncate max-w-[120px]">
{proj.project}
</span>
</div>
<div className="flex items-center gap-2 text-xs">
<span className={`font-mono ${
proj.status === 'running' ? 'text-yellow-300' :
proj.status === 'completed' ? 'text-green-300' :
proj.status === 'failed' ? 'text-red-300' : 'text-gray-400'
}`}>
{proj.progress}
</span>
{proj.status === 'running' && proj.currentStory && (
<span className="text-blue-300 truncate max-w-[80px]">{proj.currentStory}</span>
)}
</div>
</div>
))
)}
</div>
</div>
{/* Claude Processes */}
<div>
<div className="text-xs text-gray-500 mb-2 flex items-center gap-2">
<Terminal className="w-3 h-3" />
Claude Processes
</div>
<div className="flex gap-2 overflow-x-auto pb-2">
{!data?.claudeProcesses?.length ? (
<div className="text-sm text-gray-600">No claude processes</div>
) : (
data.claudeProcesses.slice(0, 10).map((proc) => (
<div
key={proc.pid}
onClick={() => setSelectedPid(proc.pid)}
className={`flex-shrink-0 px-3 py-2 rounded-lg border cursor-pointer transition-all hover:border-blue-500/50 ${
proc.type === 'ralph'
? 'bg-yellow-900/20 border-yellow-700/40'
: proc.type === 'interactive'
? 'bg-blue-900/20 border-blue-700/40'
: proc.type === 'mcp'
? 'bg-purple-900/20 border-purple-700/40'
: 'bg-gray-800/50 border-gray-700/50'
} ${selectedPid === proc.pid ? 'ring-2 ring-blue-500' : ''}`}
>
<div className="flex items-center gap-2 mb-1">
<ProcessTypeIcon type={proc.type} />
<span className="font-medium text-sm text-white truncate max-w-[100px]">
{proc.project || proc.type}
</span>
</div>
<div className="flex items-center gap-2 text-xs text-gray-400">
<span>PID: {proc.pid}</span>
<span className="text-gray-600">|</span>
<span>{proc.cpu}% CPU</span>
<span className="text-gray-600">|</span>
<span>{proc.uptime}</span>
</div>
</div>
))
)}
</div>
</div>
</div>
)}
</div>
</div>
)
}