Files
trading_bot_v4/app/cluster/page.tsx
mindesbunister db33af9f17 fix: Stop button database reset + UI state display (DATABASE-FIRST ARCHITECTURE)
CRITICAL FIXES:
1. Stop button now resets database FIRST (before pkill)
   - Database cleanup happens even if coordinator crashed
   - Prevents stale 'running' chunks blocking restart
   - Uses Node.js sqlite library (not CLI - Docker compatible)

2. UI enhancement - 4-state display
   -  Processing (running > 0)
   -  Pending (pending > 0, running = 0)
   -  Complete (all completed)
   - ⏸️ Idle (no work queued) [NEW]
   - Shows pending chunk count when present

TECHNICAL DETAILS:
- Replaced sqlite3 CLI calls with proper Node.js API
- Fixed permissions: chown 1001:1001 cluster/ for container write
- Database-first logic: reset → pkill → verify
- Detailed logging for each operation step

FILES CHANGED:
- app/api/cluster/control/route.ts (database operations refactored)
- app/cluster/page.tsx (4-state UI display)

VERIFIED:
- Stop button successfully reset 3 'running' chunks → 'pending'
- UI correctly shows Idle state after Stop
- Container logs show detailed operation flow
- Database operations work in Docker environment

DEPLOYMENT:
- Container rebuilt with fixed code
- Tested with real stale database (3 running chunks)
- All operations working correctly
2025-12-01 11:34:47 +01:00

362 lines
14 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 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 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()
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>
<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>
{/* 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>
)
}