Files
trading_bot_v4/app/cluster/page.tsx
mindesbunister 8a3141e793 feat: Add cluster page navigation
- Add EPYC Cluster card to landing page (first position, purple/pink gradient)
- Add back button to cluster page (animated left arrow, links to dashboard)
- Update landing page grid layout (lg:grid-cols-3 xl:grid-cols-4 for 7 cards)
- Complete bidirectional navigation: dashboard ↔ cluster monitoring

Navigation features:
- Cluster card: 🖥️ icon, "Monitor distributed parameter exploration" description
- Back button: Animated hover effect (arrow slides left, color transitions)
- Responsive grid: 2 cols (mobile), 3 cols (tablet), 4 cols (desktop)
- Consistent styling with existing navigation cards
2025-11-30 13:18:03 +01:00

285 lines
11 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
combinationsPerChunk: number
totalChunks: number
chunksCompleted: number
currentChunk: string
progress: 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 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)
}
}
useEffect(() => {
fetchStatus()
const interval = setInterval(fetchStatus, 30000) // Refresh every 30s
return () => clearInterval(interval)
}, [])
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>
<button
onClick={fetchStatus}
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded text-sm"
>
🔄 Refresh
</button>
</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>
{/* 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-3 gap-4 mb-4">
<div>
<div className="text-gray-400 text-sm">Total Space</div>
<div className="text-lg font-bold">{status.exploration.totalCombinations.toLocaleString()}</div>
</div>
<div>
<div className="text-gray-400 text-sm">Chunks Completed</div>
<div className="text-lg font-bold">{status.exploration.chunksCompleted} / {status.exploration.totalChunks}</div>
</div>
<div>
<div className="text-gray-400 text-sm">Current Chunk</div>
<div className="text-lg font-bold font-mono text-sm">{status.exploration.currentChunk}</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 * 100}%` }}
/>
</div>
<div className="text-right text-sm text-gray-400 mt-1">
{(status.exploration.progress * 100).toFixed(2)}% complete
</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>
)
}