← back to Letsbegin
components/ProcessStatusBar.tsx
789 lines
'use client'
import { useState, useEffect } from 'react'
interface RalphProcess {
project: string
projectPath: string
pid: number
cpu: string
memory: string
status: 'running' | 'idle' | 'completed' | 'failed' | 'paused'
currentStory: string
startTime: string
progress: string
errorLogs?: string[]
port?: number
}
// Project port mappings
const PROJECT_PORTS: Record<string, number> = {
'portfolio': 7899,
'letsbegin': 7300,
'test': 7300,
'angels-flowers': 3000,
'watches': 3000,
'dear-bubbe-nextjs': 3011,
'bubbe-ai': 3011,
}
interface ProcessDetails {
project: string
projectPath: string
status: string
logs: string[]
stories: { id: string; title: string; status: string; notes?: string }[]
progress: string
startTime: string
pid: number
cpu: string
memory: string
port: number | null
}
export default function ProcessStatusBar() {
const [processes, setProcesses] = useState<RalphProcess[]>([])
const [isExpanded, setIsExpanded] = useState(false)
const [lastUpdate, setLastUpdate] = useState<Date>(new Date())
const [loadingAction, setLoadingAction] = useState<string | null>(null)
const [errorModal, setErrorModal] = useState<{ project: string; projectPath: string; analysis: string; isFixing?: boolean; fixAttempt?: number } | null>(null)
const [detailModal, setDetailModal] = useState<ProcessDetails | null>(null)
const [loadingDetails, setLoadingDetails] = useState(false)
useEffect(() => {
fetchProcesses()
const interval = setInterval(fetchProcesses, 3000) // Update every 3 seconds
return () => clearInterval(interval)
}, [])
async function fetchProcesses() {
try {
const res = await fetch('/api/processes')
if (res.ok) {
const data = await res.json()
setProcesses(data.processes || [])
setLastUpdate(new Date())
}
} catch (err) {
console.error('Failed to fetch processes:', err)
}
}
async function handleStart(projectPath: string, project: string) {
setLoadingAction(`start-${project}`)
try {
const res = await fetch('/api/processes/start', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ projectPath, project })
})
if (res.ok) {
fetchProcesses()
} else {
const data = await res.json()
alert(`Failed to start: ${data.error || 'Unknown error'}`)
}
} catch (err) {
console.error('Failed to start process:', err)
alert('Failed to start process')
} finally {
setLoadingAction(null)
}
}
async function handleStop(projectPath: string, project: string, pid: number) {
setLoadingAction(`stop-${project}`)
try {
const res = await fetch('/api/ralph/control', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'stop', project, pid })
})
if (res.ok) {
fetchProcesses()
} else {
const data = await res.json()
alert(`Failed to stop: ${data.error || 'Unknown error'}`)
}
} catch (err) {
console.error('Failed to stop process:', err)
alert('Failed to stop process')
} finally {
setLoadingAction(null)
}
}
async function handlePause(project: string, pid: number) {
setLoadingAction(`pause-${project}`)
try {
const res = await fetch('/api/ralph/control', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'pause', project, pid })
})
if (res.ok) {
fetchProcesses()
} else {
const data = await res.json()
alert(`Failed to pause: ${data.error || 'Unknown error'}`)
}
} catch (err) {
console.error('Failed to pause process:', err)
alert('Failed to pause process')
} finally {
setLoadingAction(null)
}
}
async function handleResume(project: string, pid: number) {
setLoadingAction(`resume-${project}`)
try {
const res = await fetch('/api/ralph/control', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'resume', project, pid })
})
if (res.ok) {
fetchProcesses()
} else {
const data = await res.json()
alert(`Failed to resume: ${data.error || 'Unknown error'}`)
}
} catch (err) {
console.error('Failed to resume process:', err)
alert('Failed to resume process')
} finally {
setLoadingAction(null)
}
}
async function handleAnalyzeErrors(projectPath: string, project: string) {
setLoadingAction(`analyze-${project}`)
try {
const res = await fetch('/api/processes/analyze', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ projectPath, project })
})
const data = await res.json()
if (res.ok) {
setErrorModal({ project, projectPath, analysis: data.analysis, fixAttempt: 0 })
} else {
alert(`Failed to analyze: ${data.error || 'Unknown error'}`)
}
} catch (err) {
console.error('Failed to analyze errors:', err)
alert('Failed to analyze errors')
} finally {
setLoadingAction(null)
}
}
async function handleFixAndRestart(projectPath: string, project: string, analysis: string, attempt: number = 1) {
if (!errorModal) return
setErrorModal(prev => prev ? { ...prev, isFixing: true, fixAttempt: attempt } : null)
try {
const res = await fetch('/api/processes/fix', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ projectPath, project, analysis, attempt })
})
const data = await res.json()
// Update modal with fix results
const newAnalysis = `${analysis}\n\n${'='.repeat(50)}\nFIX ATTEMPT ${attempt}\n${'='.repeat(50)}\n${data.logs?.join('\n') || 'No logs'}`
if (data.success && data.restarted) {
setErrorModal(prev => prev ? {
...prev,
analysis: newAnalysis + '\n\n✅ Fixes applied and process restarted!',
isFixing: false
} : null)
// Wait and check if process is still failing
setTimeout(async () => {
await fetchProcesses()
const proc = processes.find(p => p.project === project)
if (proc?.status === 'failed' && attempt < 5) {
// Auto-retry if still failing
handleFixAndRestart(projectPath, project, newAnalysis, attempt + 1)
}
}, 10000) // Check after 10 seconds
} else {
setErrorModal(prev => prev ? {
...prev,
analysis: newAnalysis + (data.error ? `\n\n❌ Error: ${data.error}` : ''),
isFixing: false
} : null)
}
} catch (err: any) {
setErrorModal(prev => prev ? {
...prev,
analysis: `${analysis}\n\n❌ Fix failed: ${err.message}`,
isFixing: false
} : null)
}
}
async function openProcessDetails(proc: RalphProcess) {
setLoadingDetails(true)
try {
const res = await fetch(`/api/processes/details?project=${encodeURIComponent(proc.project)}&path=${encodeURIComponent(proc.projectPath)}`)
const data = await res.json()
if (res.ok) {
setDetailModal({
project: proc.project,
projectPath: proc.projectPath,
status: proc.status,
logs: data.logs || [],
stories: data.stories || [],
progress: proc.progress,
startTime: proc.startTime,
pid: proc.pid,
cpu: proc.cpu,
memory: proc.memory,
port: data.port || null
})
} else {
alert(`Failed to load details: ${data.error || 'Unknown error'}`)
}
} catch (err) {
console.error('Failed to load process details:', err)
alert('Failed to load process details')
} finally {
setLoadingDetails(false)
}
}
const runningCount = processes.filter(p => p.status === 'running').length
const pausedCount = processes.filter(p => p.status === 'paused').length
const completedCount = processes.filter(p => p.status === 'completed').length
const failedCount = processes.filter(p => p.status === 'failed').length
// If no processes, show minimal bar
if (processes.length === 0) {
return (
<div className="fixed bottom-0 left-0 right-0 z-50">
<div className="bg-[#0d0d12] border-t-4 border-[var(--border)] px-4 py-2 flex items-center justify-center">
<span className="text-sm text-[var(--foreground-tertiary)]">
🤖 No Ralphy processes running
</span>
</div>
</div>
)
}
return (
<div className="fixed bottom-0 left-0 right-0 z-50">
{/* Expanded Panel */}
{isExpanded && (
<div className="bg-[var(--surface)] border-t-2 border-[var(--primary)] max-h-64 overflow-y-auto">
<div className="p-4">
<div className="flex justify-between items-center mb-3">
<h3 className="font-bold">🤖 Ralphy Boy Processes</h3>
<span className="text-xs text-[var(--foreground-tertiary)]">
Updated: {lastUpdate.toLocaleTimeString()}
</span>
</div>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="text-left text-[var(--foreground-secondary)] border-b border-[var(--border)]">
<th className="pb-2 pr-4">Project</th>
<th className="pb-2 pr-4">Status</th>
<th className="pb-2 pr-4">Current Story</th>
<th className="pb-2 pr-4">Progress</th>
<th className="pb-2 pr-4">CPU</th>
<th className="pb-2 pr-4">Started</th>
<th className="pb-2">Actions</th>
</tr>
</thead>
<tbody>
{processes.map((proc, idx) => (
<tr
key={idx}
className="border-b border-[var(--border)]/50 hover:bg-[var(--surface-alt)] cursor-pointer"
onClick={() => openProcessDetails(proc)}
>
<td className="py-2 pr-4 font-medium">
<div className="flex items-center gap-2">
<span className={`w-2 h-2 rounded-full ${
proc.status === 'running' ? 'bg-green-500 animate-pulse' :
proc.status === 'paused' ? 'bg-yellow-500' :
proc.status === 'completed' ? 'bg-blue-500' :
proc.status === 'failed' ? 'bg-red-500' : 'bg-gray-500'
}`}></span>
<span>{proc.project}</span>
<button
onClick={(e) => {
e.stopPropagation()
const port = proc.port || PROJECT_PORTS[proc.project.toLowerCase()] || 3000
window.open(`http://45.61.58.125:${port}/`, '_blank')
}}
className="ml-1 text-[var(--primary)] hover:text-[var(--primary-hover)] transition-colors"
title={`Open http://45.61.58.125:${proc.port || PROJECT_PORTS[proc.project.toLowerCase()] || 3000}/`}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
</svg>
</button>
</div>
</td>
<td className="py-2 pr-4">
<span className={`px-2 py-0.5 rounded text-xs font-medium ${
proc.status === 'running' ? 'bg-green-500/20 text-green-400' :
proc.status === 'paused' ? 'bg-yellow-500/20 text-yellow-400' :
proc.status === 'completed' ? 'bg-blue-500/20 text-blue-400' :
proc.status === 'failed' ? 'bg-red-500/20 text-red-400' : 'bg-gray-500/20 text-gray-400'
}`}>
{proc.status.toUpperCase()}
</span>
</td>
<td className="py-2 pr-4 font-mono text-[var(--primary)]">{proc.currentStory}</td>
<td className="py-2 pr-4">
<div className="flex items-center gap-2">
<div className="w-20 h-2 bg-[var(--surface-alt)] rounded-full overflow-hidden">
<div
className={`h-full ${
proc.status === 'completed' ? 'bg-blue-500' :
proc.status === 'failed' ? 'bg-red-500' : 'bg-green-500'
}`}
style={{
width: `${(() => {
const [done, total] = proc.progress.split('/').map(Number)
return total > 0 ? (done / total) * 100 : 0
})()}%`
}}
></div>
</div>
<span className="text-xs">{proc.progress}</span>
</div>
</td>
<td className="py-2 pr-4 text-[var(--foreground-secondary)]">{proc.cpu}%</td>
<td className="py-2 pr-4 text-[var(--foreground-secondary)]">{proc.startTime}</td>
<td className="py-2">
<div className="flex items-center gap-1">
{proc.status === 'running' ? (
<>
<button
onClick={(e) => { e.stopPropagation(); handlePause(proc.project, proc.pid); }}
disabled={loadingAction === `pause-${proc.project}`}
className="px-2 py-1 text-xs font-medium rounded bg-yellow-500/20 text-yellow-400 hover:bg-yellow-500/30 border border-yellow-500/40 disabled:opacity-50"
title="Pause process"
>
{loadingAction === `pause-${proc.project}` ? '...' : '⏸'}
</button>
<button
onClick={(e) => { e.stopPropagation(); handleStop(proc.projectPath, proc.project, proc.pid); }}
disabled={loadingAction === `stop-${proc.project}`}
className="px-2 py-1 text-xs font-medium rounded bg-red-500/20 text-red-400 hover:bg-red-500/30 border border-red-500/40 disabled:opacity-50"
title="Stop process"
>
{loadingAction === `stop-${proc.project}` ? '...' : '⏹'}
</button>
</>
) : proc.status === 'paused' ? (
<>
<button
onClick={(e) => { e.stopPropagation(); handleResume(proc.project, proc.pid); }}
disabled={loadingAction === `resume-${proc.project}`}
className="px-2 py-1 text-xs font-medium rounded bg-green-500/20 text-green-400 hover:bg-green-500/30 border border-green-500/40 disabled:opacity-50"
title="Resume process"
>
{loadingAction === `resume-${proc.project}` ? '...' : '▶'}
</button>
<button
onClick={(e) => { e.stopPropagation(); handleStop(proc.projectPath, proc.project, proc.pid); }}
disabled={loadingAction === `stop-${proc.project}`}
className="px-2 py-1 text-xs font-medium rounded bg-red-500/20 text-red-400 hover:bg-red-500/30 border border-red-500/40 disabled:opacity-50"
title="Stop process"
>
{loadingAction === `stop-${proc.project}` ? '...' : '⏹'}
</button>
</>
) : (
<button
onClick={(e) => { e.stopPropagation(); handleStart(proc.projectPath, proc.project); }}
disabled={loadingAction === `start-${proc.project}`}
className="px-2 py-1 text-xs font-medium rounded bg-green-500/20 text-green-400 hover:bg-green-500/30 border border-green-500/40 disabled:opacity-50"
title="Start process"
>
{loadingAction === `start-${proc.project}` ? '...' : '▶ Start'}
</button>
)}
{proc.status === 'failed' && (
<button
onClick={(e) => { e.stopPropagation(); handleAnalyzeErrors(proc.projectPath, proc.project); }}
disabled={loadingAction === `analyze-${proc.project}`}
className="px-2 py-1 text-xs font-medium rounded bg-yellow-500/20 text-yellow-400 hover:bg-yellow-500/30 border border-yellow-500/40 disabled:opacity-50"
title="Analyze errors"
>
{loadingAction === `analyze-${proc.project}` ? '...' : '🔍'}
</button>
)}
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
)}
{/* Status Bar - Thick Line */}
<div
className="bg-[#0d0d12] border-t-4 border-[var(--primary)] px-4 py-3 flex items-center justify-between cursor-pointer hover:bg-[var(--surface)]"
onClick={() => setIsExpanded(!isExpanded)}
>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<span className="text-xl">🤖</span>
<span className="font-bold">Ralphy Processes</span>
</div>
{/* Process Pills */}
<div className="flex items-center gap-2">
{processes.map((proc, idx) => (
<div
key={idx}
className={`px-3 py-1.5 rounded-lg text-sm font-medium flex items-center gap-2 ${
proc.status === 'running'
? 'bg-green-500/20 text-green-400 border border-green-500/40'
: proc.status === 'paused'
? 'bg-yellow-500/20 text-yellow-400 border border-yellow-500/40'
: proc.status === 'completed'
? 'bg-blue-500/20 text-blue-400 border border-blue-500/40'
: proc.status === 'failed'
? 'bg-red-500/20 text-red-400 border border-red-500/40'
: 'bg-gray-500/20 text-gray-400 border border-gray-500/40'
}`}
>
<span className={`w-2 h-2 rounded-full ${
proc.status === 'running' ? 'bg-green-500 animate-pulse' :
proc.status === 'paused' ? 'bg-yellow-500' :
proc.status === 'completed' ? 'bg-blue-500' :
proc.status === 'failed' ? 'bg-red-500' : 'bg-gray-500'
}`}></span>
<span className="font-semibold">{proc.project}</span>
<span className="text-xs opacity-75">{proc.currentStory}</span>
<span className="text-xs opacity-50">{proc.progress}</span>
</div>
))}
</div>
</div>
<div className="flex items-center gap-4">
{/* Summary Stats */}
<div className="flex items-center gap-3 text-sm">
{runningCount > 0 && (
<div className="flex items-center gap-1">
<span className="text-green-400 animate-pulse">●</span>
<span className="text-green-400 font-medium">{runningCount} Running</span>
</div>
)}
{pausedCount > 0 && (
<div className="flex items-center gap-1">
<span className="text-yellow-400">●</span>
<span className="text-yellow-400">{pausedCount} Paused</span>
</div>
)}
{completedCount > 0 && (
<div className="flex items-center gap-1">
<span className="text-blue-400">●</span>
<span className="text-blue-400">{completedCount} Done</span>
</div>
)}
{failedCount > 0 && (
<div className="flex items-center gap-1">
<span className="text-red-400">●</span>
<span className="text-red-400">{failedCount} Failed</span>
</div>
)}
</div>
{/* Expand/Collapse Icon */}
<svg
className={`w-5 h-5 text-[var(--foreground-secondary)] transition-transform ${isExpanded ? 'rotate-180' : ''}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 15l7-7 7 7" />
</svg>
</div>
</div>
{/* Error Analysis Modal */}
{errorModal && (
<div className="fixed inset-0 bg-black/60 flex items-center justify-center z-[100]" onClick={() => !errorModal.isFixing && setErrorModal(null)}>
<div
className="bg-[var(--surface)] border border-[var(--border)] rounded-xl max-w-3xl w-full mx-4 max-h-[85vh] overflow-hidden"
onClick={(e) => e.stopPropagation()}
>
<div className="p-4 border-b border-[var(--border)] flex items-center justify-between">
<div className="flex items-center gap-3">
<h3 className="font-bold text-lg flex items-center gap-2">
<span>🔍</span>
Error Analysis: {errorModal.project}
</h3>
{errorModal.isFixing && (
<span className="px-2 py-1 text-xs rounded-full bg-yellow-500/20 text-yellow-400 flex items-center gap-1">
<svg className="w-3 h-3 animate-spin" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Fixing (Attempt {errorModal.fixAttempt})...
</span>
)}
{errorModal.fixAttempt && errorModal.fixAttempt > 0 && !errorModal.isFixing && (
<span className="px-2 py-1 text-xs rounded-full bg-blue-500/20 text-blue-400">
Attempt {errorModal.fixAttempt}/5
</span>
)}
</div>
<button
onClick={() => !errorModal.isFixing && setErrorModal(null)}
className="text-[var(--foreground-secondary)] hover:text-[var(--foreground)] text-xl disabled:opacity-50"
disabled={errorModal.isFixing}
>
×
</button>
</div>
<div className="p-4 overflow-y-auto max-h-[60vh]">
<pre className="whitespace-pre-wrap text-sm font-mono bg-[var(--background)] p-4 rounded-lg">
{errorModal.analysis}
</pre>
</div>
<div className="p-4 border-t border-[var(--border)] flex items-center justify-between">
<div className="text-xs text-[var(--foreground-tertiary)]">
Auto-fix uses AI to diagnose and repair issues (max 5 attempts)
</div>
<div className="flex items-center gap-2">
<button
onClick={() => {
navigator.clipboard.writeText(errorModal.analysis)
alert('Copied to clipboard!')
}}
className="px-4 py-2 text-sm rounded bg-[var(--surface-alt)] hover:bg-[var(--border)]"
disabled={errorModal.isFixing}
>
Copy
</button>
<button
onClick={() => handleFixAndRestart(
errorModal.projectPath,
errorModal.project,
errorModal.analysis,
(errorModal.fixAttempt || 0) + 1
)}
disabled={errorModal.isFixing || (errorModal.fixAttempt || 0) >= 5}
className="px-4 py-2 text-sm rounded bg-green-500/20 text-green-400 hover:bg-green-500/30 border border-green-500/40 disabled:opacity-50 flex items-center gap-2"
>
{errorModal.isFixing ? (
<>
<svg className="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Fixing...
</>
) : (
<>🔧 Fix & Restart</>
)}
</button>
<button
onClick={() => setErrorModal(null)}
className="px-4 py-2 text-sm rounded bg-[var(--surface-alt)] hover:bg-[var(--border)]"
disabled={errorModal.isFixing}
>
Close
</button>
</div>
</div>
</div>
</div>
)}
{/* Process Details Modal */}
{detailModal && (
<div className="fixed inset-0 bg-black/60 flex items-center justify-center z-[100]" onClick={() => setDetailModal(null)}>
<div
className="bg-[var(--surface)] border border-[var(--border)] rounded-xl max-w-4xl w-full mx-4 max-h-[85vh] overflow-hidden"
onClick={(e) => e.stopPropagation()}
>
{/* Header */}
<div className="p-4 border-b border-[var(--border)] flex items-center justify-between">
<div className="flex items-center gap-3">
<h3 className="font-bold text-lg">📋 {detailModal.project}</h3>
<span className={`px-2 py-0.5 rounded text-xs font-medium ${
detailModal.status === 'running' ? 'bg-green-500/20 text-green-400' :
detailModal.status === 'paused' ? 'bg-yellow-500/20 text-yellow-400' :
detailModal.status === 'completed' ? 'bg-blue-500/20 text-blue-400' :
detailModal.status === 'failed' ? 'bg-red-500/20 text-red-400' : 'bg-gray-500/20 text-gray-400'
}`}>
{detailModal.status.toUpperCase()}
</span>
</div>
<button
onClick={() => setDetailModal(null)}
className="text-[var(--foreground-secondary)] hover:text-[var(--foreground)] text-2xl leading-none"
>
×
</button>
</div>
{/* Process Info Bar */}
<div className="px-4 py-2 bg-[var(--background)] border-b border-[var(--border)] flex items-center gap-6 text-sm">
<div>
<span className="text-[var(--foreground-tertiary)]">PID:</span>{' '}
<span className="font-mono">{detailModal.pid || 'N/A'}</span>
</div>
<div>
<span className="text-[var(--foreground-tertiary)]">CPU:</span>{' '}
<span className="font-mono">{detailModal.cpu}%</span>
</div>
<div>
<span className="text-[var(--foreground-tertiary)]">Memory:</span>{' '}
<span className="font-mono">{detailModal.memory}</span>
</div>
<div>
<span className="text-[var(--foreground-tertiary)]">Progress:</span>{' '}
<span className="font-mono text-[var(--primary)]">{detailModal.progress}</span>
</div>
<div>
<span className="text-[var(--foreground-tertiary)]">Started:</span>{' '}
<span>{detailModal.startTime}</span>
</div>
<div>
<span className="text-[var(--foreground-tertiary)]">Port:</span>{' '}
<span className="font-mono text-[var(--primary)]">
{detailModal.port || PROJECT_PORTS[detailModal.project.toLowerCase()] || 'N/A'}
</span>
</div>
<button
onClick={() => {
const port = detailModal.port || PROJECT_PORTS[detailModal.project.toLowerCase()] || 3000
window.open(`http://45.61.58.125:${port}/`, '_blank')
}}
className="ml-auto px-3 py-1 text-xs rounded bg-[var(--primary)]/20 text-[var(--primary)] hover:bg-[var(--primary)]/30 flex items-center gap-1"
>
Open App ↗
</button>
</div>
{/* Content - Two Columns */}
<div className="flex h-[60vh]">
{/* Stories Panel */}
<div className="w-1/2 border-r border-[var(--border)] flex flex-col">
<div className="p-3 border-b border-[var(--border)] font-medium text-sm">
📝 User Stories ({detailModal.stories.length})
</div>
<div className="flex-1 overflow-y-auto p-2">
{detailModal.stories.length === 0 ? (
<p className="text-[var(--foreground-tertiary)] text-sm p-2">No stories found</p>
) : (
<div className="space-y-2">
{detailModal.stories.map((story, idx) => (
<div
key={idx}
className={`p-2 rounded border ${
story.status === 'DONE' || story.status === 'pass' ? 'border-green-500/30 bg-green-500/10' :
story.status === 'RUNNING' || story.status === 'running' ? 'border-yellow-500/30 bg-yellow-500/10' :
story.status === 'FAIL' || story.status === 'fail' ? 'border-red-500/30 bg-red-500/10' :
'border-[var(--border)] bg-[var(--surface-alt)]'
}`}
>
<div className="flex items-center justify-between">
<span className="font-mono text-sm text-[var(--primary)]">{story.id}</span>
<span className={`text-xs px-1.5 py-0.5 rounded ${
story.status === 'DONE' || story.status === 'pass' ? 'bg-green-500/20 text-green-400' :
story.status === 'RUNNING' || story.status === 'running' ? 'bg-yellow-500/20 text-yellow-400' :
story.status === 'FAIL' || story.status === 'fail' ? 'bg-red-500/20 text-red-400' :
'bg-gray-500/20 text-gray-400'
}`}>
{story.status}
</span>
</div>
<div className="text-xs text-[var(--foreground-secondary)] mt-1 truncate">{story.title}</div>
{story.notes && (
<div className="text-xs text-[var(--foreground-tertiary)] mt-1 italic">{story.notes}</div>
)}
</div>
))}
</div>
)}
</div>
</div>
{/* Logs Panel */}
<div className="w-1/2 flex flex-col">
<div className="p-3 border-b border-[var(--border)] font-medium text-sm flex items-center justify-between">
<span>📺 Live Logs</span>
<button
onClick={() => {
navigator.clipboard.writeText(detailModal.logs.join('\n'))
alert('Logs copied!')
}}
className="text-xs text-[var(--foreground-tertiary)] hover:text-[var(--foreground)]"
>
Copy
</button>
</div>
<div className="flex-1 overflow-y-auto p-2 bg-[var(--background)] font-mono text-xs">
{detailModal.logs.length === 0 ? (
<p className="text-[var(--foreground-tertiary)] p-2">No logs available</p>
) : (
detailModal.logs.map((log, idx) => (
<div
key={idx}
className={`py-0.5 ${
log.includes('Error') || log.includes('FAIL') ? 'text-red-400' :
log.includes('✓') || log.includes('DONE') || log.includes('pass') ? 'text-green-400' :
log.includes('Running') || log.includes('RUNNING') ? 'text-yellow-400' :
'text-[var(--foreground-secondary)]'
}`}
>
{log}
</div>
))
)}
</div>
</div>
</div>
{/* Footer */}
<div className="p-3 border-t border-[var(--border)] flex items-center justify-between">
<span className="text-xs text-[var(--foreground-tertiary)]">
Path: {detailModal.projectPath}
</span>
<button
onClick={() => setDetailModal(null)}
className="px-4 py-2 text-sm rounded bg-[var(--surface-alt)] hover:bg-[var(--border)]"
>
Close
</button>
</div>
</div>
</div>
)}
{/* Loading Overlay */}
{loadingDetails && (
<div className="fixed inset-0 bg-black/40 flex items-center justify-center z-[100]">
<div className="bg-[var(--surface)] border border-[var(--border)] rounded-lg px-6 py-4 flex items-center gap-3">
<svg className="w-5 h-5 animate-spin text-[var(--primary)]" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<span>Loading process details...</span>
</div>
</div>
)}
</div>
)
}