Files
trading_bot_v4/app/cluster/page.tsx
mindesbunister 1f83a7d7c4 feat: Add coordinator log viewer to cluster UI
- Created /api/cluster/logs endpoint to read coordinator.log
- Added real-time log display in cluster UI (updates every 3s)
- Shows last 100 lines of coordinator.log in terminal-style display
- Includes manual refresh button
- Improves debugging experience - no need to SSH for logs

User feedback: 'why dont we add the output of the log at the bottom of the page so i know whats going on'

This addresses poor visibility into coordinator errors and failures.
Next step: Fix SSH timeout issue blocking worker execution.
2025-12-01 11:49:23 +01:00

406 lines
16 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
import { useEffect, useState } from 'react'
interface ClusterStatus {
cluster: {
totalCores: number
activeCores: number
cpuUsage: number
activeWorkers: number
totalWorkers: number
workerProcesses: number
status: string
}
workers: Array<{
name: string
host: string
cpuUsage: number
loadAverage: string
activeProcesses: number
status: string
}>
exploration: {
totalCombinations: number
testedCombinations: number
progress: number
chunks: {
total: number
completed: number
running: number
pending: number
}
}
topStrategies: Array<{
rank: number
pnl_per_1k: number
win_rate: number
trades: number
profit_factor: number
max_drawdown: number
params: {
flip_threshold: number
ma_gap: number
adx_min: number
long_pos_max: number
short_pos_min: number
}
}>
recommendation: string
lastUpdate: string
}
export default function ClusterPage() {
const [status, setStatus] = useState<ClusterStatus | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [controlLoading, setControlLoading] = useState(false)
const [controlMessage, setControlMessage] = useState<string | null>(null)
const [coordinatorLog, setCoordinatorLog] = useState<string>('')
const [logLoading, setLogLoading] = useState(false)
const fetchStatus = async () => {
try {
const res = await fetch('/api/cluster/status')
if (!res.ok) throw new Error('Failed to fetch')
const data = await res.json()
setStatus(data)
setError(null)
} catch (err: any) {
setError(err.message)
} finally {
setLoading(false)
}
}
const fetchLog = async () => {
try {
setLogLoading(true)
const response = await fetch('/api/cluster/logs')
const data = await response.json()
if (data.success) {
setCoordinatorLog(data.log)
}
} catch (error) {
console.error('Failed to fetch coordinator log:', error)
} finally {
setLogLoading(false)
}
}
const handleControl = async (action: 'start' | 'stop') => {
setControlLoading(true)
setControlMessage(null)
try {
const res = await fetch('/api/cluster/control', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action })
})
const data = await res.json()
setControlMessage(data.message || (data.success ? `Cluster ${action}ed` : 'Operation failed'))
// Refresh status after control action
setTimeout(() => fetchStatus(), 2000)
} catch (err: any) {
setControlMessage(`Error: ${err.message}`)
} finally {
setControlLoading(false)
}
}
useEffect(() => {
fetchStatus()
fetchLog()
const statusInterval = setInterval(fetchStatus, 30000) // Refresh status every 30s
const logInterval = setInterval(fetchLog, 3000) // Refresh log every 3s
return () => {
clearInterval(statusInterval)
clearInterval(logInterval)
}
}, [])
if (loading) {
return (
<div className="min-h-screen bg-gray-900 text-white p-8">
<div className="max-w-7xl mx-auto">
<h1 className="text-3xl font-bold mb-8">🖥 EPYC Cluster Status</h1>
<div className="text-gray-400">Loading cluster status...</div>
</div>
</div>
)
}
if (error) {
return (
<div className="min-h-screen bg-gray-900 text-white p-8">
<div className="max-w-7xl mx-auto">
<h1 className="text-3xl font-bold mb-8">🖥 EPYC Cluster Status</h1>
<div className="bg-red-900/20 border border-red-500 rounded p-4">
<p className="text-red-400">Error: {error}</p>
</div>
</div>
</div>
)
}
if (!status) return null
const getStatusColor = (statusStr: string) => {
if (statusStr === 'active') return 'text-green-400'
if (statusStr === 'idle') return 'text-yellow-400'
return 'text-red-400'
}
const getStatusBg = (statusStr: string) => {
if (statusStr === 'active') return 'bg-green-900/20 border-green-500'
if (statusStr === 'idle') return 'bg-yellow-900/20 border-yellow-500'
return 'bg-red-900/20 border-red-500'
}
return (
<div className="min-h-screen bg-gray-900 text-white p-8">
<div className="max-w-7xl mx-auto">
{/* Back Button */}
<a
href="/"
className="inline-flex items-center text-blue-400 hover:text-blue-300 mb-6 transition-colors group"
>
<svg className="w-5 h-5 mr-2 group-hover:-translate-x-1 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
<span className="font-medium">Back to Dashboard</span>
</a>
<div className="flex justify-between items-center mb-8">
<h1 className="text-3xl font-bold">🖥 EPYC Cluster Status</h1>
<div className="flex gap-3">
{status.cluster.status === 'idle' ? (
<button
onClick={() => handleControl('start')}
disabled={controlLoading}
className="px-6 py-2 bg-green-600 hover:bg-green-700 disabled:bg-gray-600 rounded text-sm font-semibold transition-colors"
>
{controlLoading ? '⏳ Starting...' : '▶️ Start Cluster'}
</button>
) : (
<button
onClick={() => handleControl('stop')}
disabled={controlLoading}
className="px-6 py-2 bg-red-600 hover:bg-red-700 disabled:bg-gray-600 rounded text-sm font-semibold transition-colors"
>
{controlLoading ? '⏳ Stopping...' : '⏹️ Stop Cluster'}
</button>
)}
<button
onClick={fetchStatus}
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded text-sm"
>
🔄 Refresh
</button>
</div>
</div>
{/* Control Message */}
{controlMessage && (
<div className="mb-4 p-4 bg-blue-900/20 border border-blue-500 rounded">
<p className="text-blue-300">{controlMessage}</p>
</div>
)}
{/* Cluster Overview */}
<div className={`border rounded-lg p-6 mb-6 ${getStatusBg(status.cluster.status)}`}>
<h2 className="text-xl font-semibold mb-4">Cluster Overview</h2>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div>
<div className="text-gray-400 text-sm">Status</div>
<div className={`text-2xl font-bold ${getStatusColor(status.cluster.status)}`}>
{status.cluster.status.toUpperCase()}
</div>
</div>
<div>
<div className="text-gray-400 text-sm">CPU Usage</div>
<div className="text-2xl font-bold">{status.cluster.cpuUsage.toFixed(1)}%</div>
</div>
<div>
<div className="text-gray-400 text-sm">Active Cores</div>
<div className="text-2xl font-bold">{status.cluster.activeCores} / {status.cluster.totalCores}</div>
</div>
<div>
<div className="text-gray-400 text-sm">Workers</div>
<div className="text-2xl font-bold">{status.cluster.activeWorkers} / {status.cluster.totalWorkers}</div>
</div>
</div>
</div>
{/* Coordinator Log */}
<div className="bg-gray-900 rounded-lg p-6 border border-gray-800">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold">Coordinator Log</h3>
<button
onClick={fetchLog}
disabled={logLoading}
className="px-3 py-1 bg-gray-800 hover:bg-gray-700 rounded text-sm disabled:opacity-50"
>
{logLoading ? '⏳ Loading...' : '🔄 Refresh'}
</button>
</div>
<div className="bg-black rounded-lg p-4 overflow-auto max-h-96">
<pre className="text-xs text-green-400 font-mono whitespace-pre-wrap">
{coordinatorLog || 'No log output available'}
</pre>
</div>
<div className="mt-2 text-xs text-gray-500">
Updates automatically every 3 seconds
</div>
</div>
{/* Worker Details */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
{status.workers.map((worker) => (
<div key={worker.name} className={`border rounded-lg p-4 ${getStatusBg(worker.status)}`}>
<h3 className="font-semibold mb-2">{worker.name}</h3>
<div className="text-sm text-gray-400 mb-3">{worker.host}</div>
<div className="space-y-2">
<div className="flex justify-between">
<span className="text-gray-400">CPU:</span>
<span className="font-mono">{worker.cpuUsage.toFixed(1)}%</span>
</div>
<div className="flex justify-between">
<span className="text-gray-400">Load:</span>
<span className="font-mono">{worker.loadAverage}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-400">Processes:</span>
<span className="font-mono">{worker.activeProcesses}</span>
</div>
</div>
</div>
))}
</div>
{/* Exploration Progress */}
<div className="border border-blue-500 bg-blue-900/20 rounded-lg p-6 mb-6">
<h2 className="text-xl font-semibold mb-4">📊 Parameter Exploration</h2>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-4">
<div>
<div className="text-gray-400 text-sm">Total Combinations</div>
<div className="text-lg font-bold">{status.exploration.totalCombinations.toLocaleString()}</div>
</div>
<div>
<div className="text-gray-400 text-sm">Tested</div>
<div className="text-lg font-bold">{status.exploration.testedCombinations.toLocaleString()}</div>
</div>
<div>
<div className="text-gray-400 text-sm">Chunks</div>
<div className="text-lg font-bold">
{status.exploration.chunks.completed} / {status.exploration.chunks.total}
{status.exploration.chunks.running > 0 && (
<span className="text-yellow-400 ml-2">({status.exploration.chunks.running} running)</span>
)}
{status.exploration.chunks.pending > 0 && status.exploration.chunks.running === 0 && (
<span className="text-gray-400 ml-2">({status.exploration.chunks.pending} pending)</span>
)}
</div>
</div>
<div>
<div className="text-gray-400 text-sm">Status</div>
<div className="text-lg font-bold">
{status.exploration.chunks.running > 0 ? (
<span className="text-yellow-400"> Processing</span>
) : status.exploration.chunks.pending > 0 ? (
<span className="text-blue-400"> Pending</span>
) : status.exploration.chunks.completed === status.exploration.chunks.total && status.exploration.chunks.total > 0 ? (
<span className="text-green-400"> Complete</span>
) : (
<span className="text-gray-400"> Idle</span>
)}
</div>
</div>
</div>
<div className="w-full bg-gray-700 rounded-full h-4">
<div
className="bg-blue-500 h-4 rounded-full transition-all"
style={{ width: `${status.exploration.progress}%` }}
/>
</div>
<div className="text-right text-sm text-gray-400 mt-1">
{status.exploration.progress.toFixed(2)}% complete
{status.exploration.testedCombinations > 0 && (
<span className="ml-3">({status.exploration.testedCombinations.toLocaleString()} strategies tested)</span>
)}
</div>
</div>
{/* Recommendation */}
{status.recommendation && (
<div className="border border-purple-500 bg-purple-900/20 rounded-lg p-6 mb-6">
<h2 className="text-xl font-semibold mb-4">🎯 AI Recommendation</h2>
<div className="whitespace-pre-line text-gray-300 leading-relaxed">
{status.recommendation}
</div>
</div>
)}
{/* Top Strategies */}
{status.topStrategies.length > 0 && (
<div className="border border-gray-700 rounded-lg p-6">
<h2 className="text-xl font-semibold mb-4">🏆 Top Strategies</h2>
<div className="space-y-3">
{status.topStrategies.map((strategy) => (
<div key={strategy.rank} className="bg-gray-800 rounded p-4">
<div className="flex justify-between items-start mb-2">
<div className="text-lg font-semibold">#{strategy.rank}</div>
<div className="text-right">
<div className="text-2xl font-bold text-green-400">
${strategy.pnl_per_1k.toFixed(2)}
</div>
<div className="text-sm text-gray-400">per $1k</div>
</div>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 text-sm">
<div>
<span className="text-gray-400">Win Rate:</span>{' '}
<span className="font-semibold">{(strategy.win_rate * 100).toFixed(1)}%</span>
</div>
<div>
<span className="text-gray-400">Trades:</span>{' '}
<span className="font-semibold">{strategy.trades}</span>
</div>
<div>
<span className="text-gray-400">PF:</span>{' '}
<span className="font-semibold">{strategy.profit_factor.toFixed(2)}x</span>
</div>
<div>
<span className="text-gray-400">Max DD:</span>{' '}
<span className="font-semibold text-red-400">
${Math.abs(strategy.max_drawdown).toFixed(0)}
</span>
</div>
</div>
<details className="mt-3">
<summary className="cursor-pointer text-blue-400 text-sm hover:text-blue-300">
Show Parameters
</summary>
<div className="mt-2 grid grid-cols-2 md:grid-cols-3 gap-2 text-xs font-mono bg-gray-900 p-3 rounded">
<div>flip: {strategy.params.flip_threshold}</div>
<div>ma_gap: {strategy.params.ma_gap}</div>
<div>adx: {strategy.params.adx_min}</div>
<div>long_pos: {strategy.params.long_pos_max}</div>
<div>short_pos: {strategy.params.short_pos_min}</div>
</div>
</details>
</div>
))}
</div>
</div>
)}
<div className="mt-6 text-center text-sm text-gray-500">
Last updated: {new Date(status.lastUpdate).toLocaleString()}
</div>
</div>
</div>
)
}