Implement API health check badge in NavBar, enhance PlatformLineChart with metric filtering, and refactor PlatformSummary for improved layout and styling. Update dashboard to prioritize Platform Summary tab.

This commit is contained in:
2025-11-08 04:29:50 +07:00
parent 7b8d435521
commit 83d13bde74
4 changed files with 502 additions and 364 deletions

View File

@@ -9,6 +9,12 @@ import Logo from '../../../assets/img/logo.png'
import {Loader} from '../../atoms'
import useCookie from '../../../hooks/useCookie'
interface HealthCheckResponse {
status: string
totalDuration?: string
entries?: Record<string, any>
}
const navigation = [
{ href: '/bots', name: 'Strategies' },
{ href: '/backtest', name: 'Backtests' },
@@ -18,13 +24,83 @@ const navigation = [
{ href: '/admin', name: 'Admin' },
]
// API Health Check Badge Component
const ApiHealthBadge = () => {
const { apiUrl } = useApiUrlStore()
const { data: healthData, isLoading, error } = useQuery<HealthCheckResponse>({
queryKey: ['apiHealth', apiUrl],
queryFn: async () => {
try {
const response = await fetch(`${apiUrl}/health`)
if (!response.ok) throw new Error('Health check failed')
const data = await response.json()
// Handle .NET health check response format
// The status might be in the root or we need to check entries
return data
} catch (error) {
console.error('Health check error:', error)
return { status: 'Unhealthy' }
}
},
refetchInterval: 30000, // Refetch every 30 seconds
retry: 1,
staleTime: 20000,
})
const getHealthStatus = () => {
if (isLoading) return { color: 'badge-ghost', label: '...', status: 'Checking...' }
if (error) return { color: 'badge-error', label: '●', status: 'Health Check Failed' }
// Check status field (could be "Healthy", "Degraded", "Unhealthy")
const status = healthData?.status || 'Unknown'
const statusLower = status.toLowerCase()
// Also check entries if status is not directly available
let overallStatus = statusLower
if (healthData?.entries) {
const entries = Object.values(healthData.entries)
const hasUnhealthy = entries.some((entry: any) => entry?.status?.toLowerCase() === 'unhealthy')
const hasDegraded = entries.some((entry: any) => entry?.status?.toLowerCase() === 'degraded')
if (hasUnhealthy) {
overallStatus = 'unhealthy'
} else if (hasDegraded) {
overallStatus = 'degraded'
} else if (entries.length > 0) {
overallStatus = 'healthy'
}
}
switch (overallStatus) {
case 'healthy':
return { color: 'badge-success', label: '●', status: 'API Status: Healthy' }
case 'degraded':
return { color: 'badge-warning', label: '●', status: 'API Status: Degraded' }
case 'unhealthy':
return { color: 'badge-error', label: '●', status: 'API Status: Unhealthy' }
default:
return { color: 'badge-ghost', label: '?', status: 'API Status: Unknown' }
}
}
const healthStatus = getHealthStatus()
return (
<div className="tooltip tooltip-bottom" data-tip={healthStatus.status}>
<div className={`badge ${healthStatus.color} badge-xs cursor-help p-0.5`}>
<span className="text-[8px] leading-none">{healthStatus.label}</span>
</div>
</div>
)
}
// Global Loader Component
const GlobalLoader = () => {
const isFetching = useIsFetching()
return isFetching ? (
<div className="flex items-center gap-2">
<Loader size="xs"></Loader>
<span className="text-xs text-base-content/70 hidden lg:inline">Loading...</span>
</div>
) : null
}
@@ -278,27 +354,28 @@ export default function NavBar() {
<>
{/* Navbar */}
<div className="navbar bg-base-300 shadow-lg border-b border-base-content/10 w-full relative z-50">
{/* Navbar Start - Mobile Menu Button and Logo */}
<div className="navbar-start">
<button
className="btn btn-ghost lg:hidden"
onClick={toggleSidebar}
aria-label="Open sidebar"
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
{/* Navbar Start - Mobile Menu Button and Logo */}
<div className="navbar-start flex items-center gap-2">
<button
className="btn btn-ghost lg:hidden"
onClick={toggleSidebar}
aria-label="Open sidebar"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 6h16M4 12h16M4 18h16" />
</svg>
</button>
<Link className="btn btn-ghost text-xl" to="/">
<img src={Logo} className="h-8 w-8 object-contain" alt="logo" />
</Link>
</div>
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 6h16M4 12h16M4 18h16" />
</svg>
</button>
<Link className="btn btn-ghost text-xl" to="/">
<img src={Logo} className="h-8 w-8 object-contain" alt="logo" />
</Link>
<ApiHealthBadge />
</div>
{/* Navbar Center - Desktop Navigation */}
<div className="navbar-center hidden lg:flex">
@@ -326,7 +403,6 @@ export default function NavBar() {
<div className="navbar-end gap-2">
<GlobalLoader />
<div className="hidden md:flex items-center gap-2">
<span className="text-xs opacity-70 hidden xl:inline">Environment:</span>
<EnvironmentDropdown isInSidebar={false} />
</div>
{/* Show environment badge on mobile */}
@@ -394,9 +470,6 @@ export default function NavBar() {
{authenticated && (
<li>
<div className="px-4 py-2">
<div className="flex items-center justify-between mb-2">
<span className="text-sm opacity-70">Environment:</span>
</div>
<EnvironmentDropdown isInSidebar={true} />
</div>
</li>

View File

@@ -14,12 +14,15 @@ interface CurrentPlatformData {
lastUpdated?: Date | string
}
type MetricType = 'agents' | 'strategies' | 'volume' | 'pnl' | 'openInterest' | 'positionCount'
interface PlatformLineChartProps {
dailySnapshots: DailySnapshot[]
currentData?: CurrentPlatformData
title?: string
height?: number
width?: number
metrics?: MetricType[] // Filter which metrics to display
}
function PlatformLineChart({
@@ -27,7 +30,8 @@ function PlatformLineChart({
currentData,
title = "Platform Metrics Over Time",
height = 400,
width = 800
width = 800,
metrics = ['agents', 'strategies', 'volume', 'pnl', 'openInterest', 'positionCount'] // Default: show all
}: PlatformLineChartProps) {
const theme = useTheme().themeProperty()
@@ -78,13 +82,15 @@ function PlatformLineChart({
return `$${value.toFixed(0)}`
}
const plotData = [
const allPlotData = [
{
key: 'agents' as MetricType,
type: 'scatter' as const,
mode: 'lines+markers' as const,
name: 'Total Agents',
x: dates,
y: agents,
yaxis: 'y' as const,
line: {
color: theme.primary,
width: 2
@@ -98,11 +104,13 @@ function PlatformLineChart({
(currentSnapshot ? '<br><i>Current Data</i>' : '') + '<extra></extra>'
},
{
key: 'strategies' as MetricType,
type: 'scatter' as const,
mode: 'lines+markers' as const,
name: 'Active Strategies',
x: dates,
y: strategies,
yaxis: 'y' as const,
line: {
color: theme.success,
width: 2
@@ -116,12 +124,13 @@ function PlatformLineChart({
(currentSnapshot ? '<br><i>Current Data</i>' : '') + '<extra></extra>'
},
{
key: 'volume' as MetricType,
type: 'scatter' as const,
mode: 'lines+markers' as const,
name: 'Total Volume',
x: dates,
y: volume,
yaxis: 'y2',
yaxis: 'y2' as const,
line: {
color: theme.info,
width: 2
@@ -135,12 +144,13 @@ function PlatformLineChart({
(currentSnapshot ? '<br><i>Current Data</i>' : '') + '<extra></extra>'
},
{
key: 'pnl' as MetricType,
type: 'scatter' as const,
mode: 'lines+markers' as const,
name: 'Platform PnL',
x: dates,
y: pnl,
yaxis: 'y2',
yaxis: 'y2' as const,
line: {
color: pnl.some(p => p >= 0) ? theme.success : theme.error,
width: 2
@@ -154,12 +164,13 @@ function PlatformLineChart({
(currentSnapshot ? '<br><i>Current Data</i>' : '') + '<extra></extra>'
},
{
key: 'openInterest' as MetricType,
type: 'scatter' as const,
mode: 'lines+markers' as const,
name: 'Open Interest',
x: dates,
y: openInterest,
yaxis: 'y2',
yaxis: 'y2' as const,
line: {
color: theme.warning,
width: 2
@@ -173,11 +184,13 @@ function PlatformLineChart({
(currentSnapshot ? '<br><i>Current Data</i>' : '') + '<extra></extra>'
},
{
key: 'positionCount' as MetricType,
type: 'scatter' as const,
mode: 'lines+markers' as const,
name: 'Position Count',
x: dates,
y: positionCount,
yaxis: 'y' as const,
line: {
color: theme.secondary,
width: 2
@@ -192,6 +205,11 @@ function PlatformLineChart({
}
]
// Filter plot data based on metrics prop and remove key property
const plotData = allPlotData
.filter(item => metrics.includes(item.key))
.map(({ key, ...rest }) => rest)
const layout = {
title: {
text: title,
@@ -214,7 +232,7 @@ function PlatformLineChart({
},
yaxis: {
title: {
text: 'Count',
text: plotData.some(d => d.yaxis === 'y') ? 'Count' : '',
font: {
color: theme['base-content'] || '#ffffff'
}
@@ -223,11 +241,12 @@ function PlatformLineChart({
gridcolor: theme.neutral || '#333333',
showgrid: true,
zeroline: false,
side: 'left' as const
side: 'left' as const,
visible: plotData.some(d => d.yaxis === 'y')
},
yaxis2: {
title: {
text: 'Amount ($)',
text: plotData.some(d => d.yaxis === 'y2') ? 'Amount ($)' : '',
font: {
color: theme['base-content'] || '#ffffff'
}
@@ -238,7 +257,8 @@ function PlatformLineChart({
zeroline: false,
side: 'right' as const,
overlaying: 'y' as const,
tickformat: '$,.0f'
tickformat: '$,.0f',
visible: plotData.some(d => d.yaxis === 'y2')
},
legend: {
font: {

View File

@@ -13,43 +13,44 @@ import PlatformSummary from './platformSummary'
const tabs: ITabsType = [
{
Component: Monitoring,
Component: PlatformSummary,
index: 1,
label: 'Platform Summary',
},
{
Component: Monitoring,
index: 2,
label: 'Monitoring',
},
{
Component: Analytics,
index: 2,
index: 3,
label: 'Analytics',
},
{
Component: BestAgents,
index: 3,
index: 4,
label: 'Best Agents',
},
{
Component: AgentSearch,
index: 4,
index: 5,
label: 'Agent Search',
},
{
Component: AgentIndex,
index: 5,
index: 6,
label: 'Agent Index',
},
{
Component: AgentStrategy,
index: 6,
label: 'Agent Strategy',
},
{
Component: PlatformSummary,
index: 7,
label: 'Platform Summary',
label: 'Agent Strategy',
},
]
const Dashboard: React.FC = () => {
// Platform Summary is now the first tab (index 1)
const [selectedTab, setSelectedTab] = useState<number>(tabs[0].index)
useEffect(() => {}, [])

View File

@@ -77,19 +77,19 @@ function PlatformSummary({index}: { index: number }) {
const formatChangeIndicator = (change: number) => {
if (change > 0) {
return (
<span className="text-green-500">
<span className="text-success">
+{change} Today
</span>
)
} else if (change < 0) {
return (
<span className="text-red-500">
<span className="text-error">
{change} Today
</span>
)
}
return (
<span className="text-gray-500">
<span className="text-base-content/60">
No change
</span>
)
@@ -130,284 +130,307 @@ function PlatformSummary({index}: { index: number }) {
return (
<div className="p-6 bg-base-100 min-h-screen">
{/* Subtle refetching indicator */}
{isFetching && data && (
<div className="fixed top-4 right-4 z-50">
<div className="bg-blue-500 text-white px-3 py-1 rounded-full text-sm flex items-center gap-2">
<div className="loading loading-spinner loading-xs"></div>
Updating...
</div>
</div>
)}
{/* Header */}
<div className="mb-8">
<h1 className="text-4xl font-bold text-white mb-2">
{formatNumber(platformData?.totalActiveStrategies || 0)} Strategies Deployed
</h1>
<div className="text-lg">
{platformData && formatChangeIndicator(changesToday.strategiesChange)}
</div>
</div>
{/* Main Stats Grid */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
{/* Total Volume Traded */}
<div className="bg-base-200 rounded-lg p-6">
<h3 className="text-lg font-semibold text-gray-400 mb-2">Total Volume Traded</h3>
<div className="text-3xl font-bold text-white mb-1">
{formatCurrency(platformData?.totalPlatformVolume || 0)}
</div>
<div
className={`text-sm ${changesToday.volumeChange >= 0 ? 'text-green-500' : 'text-red-500'}`}>
{changesToday.volumeChange >= 0 ? '+' : ''}{formatCurrency(changesToday.volumeChange)} Today
<span className="ml-2 text-gray-400">
<div className="card bg-base-200">
<div className="card-body">
<h3 className="card-title text-base-content/70">Total Volume Traded</h3>
<div className="text-3xl font-bold text-base-content mb-1">
{formatCurrency(platformData?.totalPlatformVolume || 0)}
</div>
<div
className={`text-sm ${changesToday.volumeChange >= 0 ? 'text-success' : 'text-error'}`}>
{changesToday.volumeChange >= 0 ? '+' : ''}{formatCurrency(changesToday.volumeChange)} Today
<span className="ml-2 text-base-content/60">
({formatPercentageChange(platformData?.totalPlatformVolume || 0, changesToday.volumeChange)})
</span>
</div>
{/* Volume chart using daily snapshots */}
<div className="mt-4 h-16 flex items-end gap-1">
{platformData?.dailySnapshots && platformData.dailySnapshots.length > 0 ? (
<>
{/* Historical data bars */}
{[...platformData.dailySnapshots]
.sort((a, b) => new Date(a.date || 0).getTime() - new Date(b.date || 0).getTime())
.slice(-7) // Last 7 days
.map((snapshot, index) => {
const maxVolume = Math.max(...platformData.dailySnapshots?.map(s => s.totalVolume || 0) || [0], platformData?.totalPlatformVolume || 0)
const height = maxVolume > 0 ? ((snapshot.totalVolume || 0) / maxVolume) * 100 : 0
return (
<div
key={`historical-${index}`}
className="flex-1 bg-green-500 rounded-sm opacity-60 hover:opacity-80 transition-opacity"
style={{ height: `${Math.max(height, 4)}%` }}
title={`${new Date(snapshot.date || 0).toLocaleDateString()}: ${formatCurrency(snapshot.totalVolume || 0)}`}
/>
)
})}
{/* Current data bar */}
<div
className="flex-1 bg-blue-500 rounded-sm opacity-80 hover:opacity-100 transition-opacity border-2 border-blue-300"
style={{
height: `${(() => {
</div>
{/* Volume chart using daily snapshots */}
<div className="mt-4 h-16 flex items-end gap-1">
{platformData?.dailySnapshots && platformData.dailySnapshots.length > 0 ? (
<>
{/* Historical data bars */}
{[...platformData.dailySnapshots]
.sort((a, b) => new Date(a.date || 0).getTime() - new Date(b.date || 0).getTime())
.slice(-7) // Last 7 days
.map((snapshot, index) => {
const maxVolume = Math.max(...platformData.dailySnapshots?.map(s => s.totalVolume || 0) || [0], platformData?.totalPlatformVolume || 0)
const height = maxVolume > 0 ? ((platformData?.totalPlatformVolume || 0) / maxVolume) * 100 : 0
return Math.max(height, 4)
})()}%`
}}
title={`Current Total: ${formatCurrency(platformData?.totalPlatformVolume || 0)}`}
/>
</>
) : (
<div className="w-full h-8 bg-green-500 rounded opacity-20"></div>
)}
const height = maxVolume > 0 ? ((snapshot.totalVolume || 0) / maxVolume) * 100 : 0
return (
<div
key={`historical-${index}`}
className="flex-1 bg-success rounded-sm opacity-60 hover:opacity-80 transition-opacity"
style={{ height: `${Math.max(height, 4)}%` }}
title={`${new Date(snapshot.date || 0).toLocaleDateString()}: ${formatCurrency(snapshot.totalVolume || 0)}`}
/>
)
})}
{/* Current data bar */}
<div
className="flex-1 bg-primary rounded-sm opacity-80 hover:opacity-100 transition-opacity border-2 border-primary/50"
style={{
height: `${(() => {
const maxVolume = Math.max(...platformData.dailySnapshots?.map(s => s.totalVolume || 0) || [0], platformData?.totalPlatformVolume || 0)
const height = maxVolume > 0 ? ((platformData?.totalPlatformVolume || 0) / maxVolume) * 100 : 0
return Math.max(height, 4)
})()}%`
}}
title={`Current Total: ${formatCurrency(platformData?.totalPlatformVolume || 0)}`}
/>
</>
) : (
<div className="w-full h-8 bg-success rounded opacity-20"></div>
)}
</div>
</div>
</div>
{/* Top 3 Most Profitable */}
<div className="bg-base-200 rounded-lg p-6">
<div className="flex items-center gap-2 mb-4">
<span className="text-2xl">🤑</span>
<h3 className="text-lg font-semibold text-gray-400">Top 3 Most Profitable</h3>
</div>
<div className="space-y-3">
{topStrategies?.topStrategies?.slice(0, 3).map((strategy, index) => (
<div key={index} className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-blue-500 rounded-full flex items-center justify-center">
<span className="text-xs font-bold text-white">
{strategy.strategyName?.charAt(0) || 'S'}
</span>
</div>
<div>
<div className="text-sm text-white font-medium">
{strategy.strategyName || '[Strategy Name]'}
<div className="card bg-base-200">
<div className="card-body">
<div className="flex items-center gap-2 mb-4">
<span className="text-2xl">🤑</span>
<h3 className="card-title text-base-content/70">Top 3 Most Profitable</h3>
</div>
<div className="space-y-3">
{topStrategies?.topStrategies?.slice(0, 3).map((strategy, index) => (
<div key={index} className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="avatar placeholder">
<div className="bg-primary text-primary-content rounded-full w-8">
<span className="text-xs font-bold">
{strategy.strategyName?.charAt(0) || 'S'}
</span>
</div>
</div>
<div className="text-xs text-gray-400">📧</div>
<div>
<div className="text-sm text-base-content font-medium">
{strategy.strategyName || '[Strategy Name]'}
</div>
<div className="text-xs text-base-content/60">📧</div>
</div>
</div>
<div
className={`text-sm font-bold ${strategy.netPnL && strategy.netPnL >= 0 ? 'text-success' : 'text-error'}`}>
{strategy.netPnL && strategy.netPnL >= 0 ? '+' : ''}{formatCurrency(strategy.netPnL || 0)}
</div>
</div>
<div
className={`text-sm font-bold ${strategy.netPnL && strategy.netPnL >= 0 ? 'text-green-500' : 'text-red-500'}`}>
{strategy.netPnL && strategy.netPnL >= 0 ? '+' : ''}{formatCurrency(strategy.netPnL || 0)}
</div>
</div>
)) || (
<div className="text-gray-500 text-sm">No profitable strategies found</div>
)}
)) || (
<div className="text-base-content/60 text-sm">No profitable strategies found</div>
)}
</div>
</div>
</div>
{/* Top 3 Rising (by ROI) */}
<div className="bg-base-200 rounded-lg p-6">
<div className="flex items-center gap-2 mb-4">
<span className="text-2xl">📈</span>
<h3 className="text-lg font-semibold text-gray-400">Top 3 by ROI</h3>
</div>
<div className="space-y-3">
{topStrategiesByRoi?.topStrategiesByRoi?.slice(0, 3).map((strategy, index) => (
<div key={index} className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-green-500 rounded-full flex items-center justify-center">
<span className="text-xs font-bold text-white">
{strategy.strategyName?.charAt(0) || 'S'}
</span>
</div>
<div>
<div className="text-sm text-white font-medium">
{strategy.strategyName || '[Strategy Name]'}
<div className="card bg-base-200">
<div className="card-body">
<div className="flex items-center gap-2 mb-4">
<span className="text-2xl">📈</span>
<h3 className="card-title text-base-content/70">Top 3 by ROI</h3>
</div>
<div className="space-y-3">
{topStrategiesByRoi?.topStrategiesByRoi?.slice(0, 3).map((strategy, index) => (
<div key={index} className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="avatar placeholder">
<div className="bg-success text-success-content rounded-full w-8">
<span className="text-xs font-bold">
{strategy.strategyName?.charAt(0) || 'S'}
</span>
</div>
</div>
<div className="text-xs text-gray-400">
Vol: {formatCurrency(strategy.volume || 0)}
<div>
<div className="text-sm text-base-content font-medium">
{strategy.strategyName || '[Strategy Name]'}
</div>
<div className="text-xs text-base-content/60">
Vol: {formatCurrency(strategy.volume || 0)}
</div>
</div>
</div>
<div className="text-right">
<div
className={`text-sm font-bold ${(strategy.roi || 0) >= 0 ? 'text-success' : 'text-error'}`}>
{(strategy.roi || 0) >= 0 ? '+' : ''}{strategy.roi?.toFixed(2) || 0}%
</div>
<div
className={`text-xs ${(strategy.netPnL || 0) >= 0 ? 'text-success' : 'text-error'}`}>
{(strategy.netPnL || 0) >= 0 ? '+' : ''}{formatCurrency(strategy.netPnL || 0)}
</div>
</div>
</div>
<div className="text-right">
<div
className={`text-sm font-bold ${(strategy.roi || 0) >= 0 ? 'text-green-500' : 'text-red-500'}`}>
{(strategy.roi || 0) >= 0 ? '+' : ''}{strategy.roi?.toFixed(2) || 0}%
</div>
<div
className={`text-xs ${(strategy.netPnL || 0) >= 0 ? 'text-green-400' : 'text-red-400'}`}>
{(strategy.netPnL || 0) >= 0 ? '+' : ''}{formatCurrency(strategy.netPnL || 0)}
</div>
</div>
</div>
)) || (
<div className="text-gray-500 text-sm">No ROI data available</div>
)}
)) || (
<div className="text-base-content/60 text-sm">No ROI data available</div>
)}
</div>
</div>
</div>
{/* Top 3 Agents by PnL */}
<div className="bg-base-200 rounded-lg p-6">
<div className="flex items-center gap-2 mb-4">
<span className="text-2xl">👥</span>
<h3 className="text-lg font-semibold text-gray-400">Top 3 Agents by PnL</h3>
</div>
<div className="space-y-3">
{topAgentsByPnL?.slice(0, 3).map((agent, index) => (
<div key={index} className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-purple-500 rounded-full flex items-center justify-center">
<span className="text-xs font-bold text-white">
{agent.agentName?.charAt(0) || 'A'}
</span>
</div>
<div>
<div className="text-sm text-white font-medium">
{agent.agentName || '[Agent Name]'}
<div className="card bg-base-200">
<div className="card-body">
<div className="flex items-center gap-2 mb-4">
<span className="text-2xl">👥</span>
<h3 className="card-title text-base-content/70">Top 3 Agents by PnL</h3>
</div>
<div className="space-y-3">
{topAgentsByPnL?.slice(0, 3).map((agent, index) => (
<div key={index} className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="avatar placeholder">
<div className="bg-secondary text-secondary-content rounded-full w-8">
<span className="text-xs font-bold">
{agent.agentName?.charAt(0) || 'A'}
</span>
</div>
</div>
<div className="text-xs text-gray-400">
{agent.activeStrategiesCount || 0} strategies
<div>
<div className="text-sm text-base-content font-medium">
{agent.agentName || '[Agent Name]'}
</div>
<div className="text-xs text-base-content/60">
{agent.activeStrategiesCount || 0} strategies
</div>
</div>
</div>
<div className="text-right">
<div
className={`text-sm font-bold ${(agent.netPnL || 0) >= 0 ? 'text-success' : 'text-error'}`}>
{(agent.netPnL || 0) >= 0 ? '+' : ''}{formatCurrency(agent.netPnL || 0)}
</div>
<div className="text-xs text-base-content/60">
{(agent.totalROI || 0).toFixed(2)}% ROI
</div>
</div>
</div>
<div className="text-right">
<div
className={`text-sm font-bold ${(agent.netPnL || 0) >= 0 ? 'text-green-500' : 'text-red-500'}`}>
{(agent.netPnL || 0) >= 0 ? '+' : ''}{formatCurrency(agent.netPnL || 0)}
</div>
<div className="text-xs text-gray-400">
{(agent.totalROI || 0).toFixed(2)}% ROI
</div>
</div>
</div>
)) || (
<div className="text-gray-500 text-sm">No agent data available</div>
)}
)) || (
<div className="text-base-content/60 text-sm">No agent data available</div>
)}
</div>
</div>
</div>
</div>
{/* Platform Summary Stats */}
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-7 gap-6 mb-8">
<div className="bg-base-200 rounded-lg p-6">
<h3 className="text-lg font-semibold text-gray-400 mb-2">Total Agents</h3>
<div className="text-3xl font-bold text-white mb-1">
{formatNumber(platformData?.totalAgents || 0)}
</div>
<div
className={`text-sm ${changesToday.agentsChange >= 0 ? 'text-green-500' : 'text-red-500'}`}>
{formatChangeIndicator(changesToday.agentsChange)}
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-8 gap-6 mb-8">
<div className="card bg-base-200">
<div className="card-body">
<h3 className="card-title text-base-content/70 text-base">Total Agents</h3>
<div className="text-3xl font-bold text-base-content mb-1">
{formatNumber(platformData?.totalAgents || 0)}
</div>
<div
className={`text-sm ${changesToday.agentsChange >= 0 ? 'text-success' : 'text-error'}`}>
{formatChangeIndicator(changesToday.agentsChange)}
</div>
</div>
</div>
<div className="bg-base-200 rounded-lg p-6">
<h3 className="text-lg font-semibold text-gray-400 mb-2">Active Strategies</h3>
<div className="text-3xl font-bold text-white mb-1">
{formatNumber(platformData?.totalActiveStrategies || 0)}
</div>
<div
className={`text-sm ${changesToday.strategiesChange >= 0 ? 'text-green-500' : 'text-red-500'}`}>
{formatChangeIndicator(changesToday.strategiesChange)}
<div className="card bg-base-200">
<div className="card-body">
<h3 className="card-title text-base-content/70 text-base">Strategies Deployed</h3>
<div className="text-3xl font-bold text-base-content mb-1">
{formatNumber(platformData?.totalActiveStrategies || 0)}
</div>
<div
className={`text-sm ${changesToday.strategiesChange >= 0 ? 'text-success' : 'text-error'}`}>
{formatChangeIndicator(changesToday.strategiesChange)}
</div>
</div>
</div>
<div className="bg-base-200 rounded-lg p-6">
<h3 className="text-lg font-semibold text-gray-400 mb-2">Total Platform PnL</h3>
<div
className={`text-3xl font-bold ${(platformData?.totalPlatformPnL || 0) >= 0 ? 'text-green-500' : 'text-red-500'}`}>
{(platformData?.totalPlatformPnL || 0) >= 0 ? '+' : ''}{formatCurrency(platformData?.totalPlatformPnL || 0)}
</div>
<div className="text-sm text-gray-400 mb-1">
Fees: {formatCurrency(platformData?.totalPlatformFees || 0)}
</div>
<div
className={`text-sm ${changesToday.pnLChange >= 0 ? 'text-green-500' : 'text-red-500'}`}>
{changesToday.pnLChange >= 0 ? '+' : ''}{formatCurrency(changesToday.pnLChange)} Today
<div className="card bg-base-200">
<div className="card-body">
<h3 className="card-title text-base-content/70 text-base">Active Strategies</h3>
<div className="text-3xl font-bold text-base-content mb-1">
{formatNumber(platformData?.totalActiveStrategies || 0)}
</div>
<div
className={`text-sm ${changesToday.strategiesChange >= 0 ? 'text-success' : 'text-error'}`}>
{formatChangeIndicator(changesToday.strategiesChange)}
</div>
</div>
</div>
<div className="bg-base-200 rounded-lg p-6">
<h3 className="text-lg font-semibold text-gray-400 mb-2">Open Interest</h3>
<div className="text-3xl font-bold text-white mb-1">
{formatCurrency(platformData?.openInterest || 0)}
</div>
<div
className={`text-sm ${changesToday.openInterestChange >= 0 ? 'text-green-500' : 'text-red-500'}`}>
{changesToday.openInterestChange >= 0 ? '+' : ''}{formatCurrency(changesToday.openInterestChange)} Today
<div className="card bg-base-200">
<div className="card-body">
<h3 className="card-title text-base-content/70 text-base">Total Platform PnL</h3>
<div
className={`text-3xl font-bold ${(platformData?.totalPlatformPnL || 0) >= 0 ? 'text-success' : 'text-error'}`}>
{(platformData?.totalPlatformPnL || 0) >= 0 ? '+' : ''}{formatCurrency(platformData?.totalPlatformPnL || 0)}
</div>
<div className="text-sm text-base-content/60 mb-1">
Fees: {formatCurrency(platformData?.totalPlatformFees || 0)}
</div>
<div
className={`text-sm ${changesToday.pnLChange >= 0 ? 'text-success' : 'text-error'}`}>
{changesToday.pnLChange >= 0 ? '+' : ''}{formatCurrency(changesToday.pnLChange)} Today
</div>
</div>
</div>
<div className="bg-base-200 rounded-lg p-6">
<h3 className="text-lg font-semibold text-gray-400 mb-2">Total Positions</h3>
<div className="text-3xl font-bold text-white mb-1">
{formatNumber(platformData?.totalPositionCount || 0)}
</div>
<div
className={`text-sm ${changesToday.positionCountChange >= 0 ? 'text-green-500' : 'text-red-500'}`}>
{formatChangeIndicator(changesToday.positionCountChange)}
<div className="card bg-base-200">
<div className="card-body">
<h3 className="card-title text-base-content/70 text-base">Open Interest</h3>
<div className="text-3xl font-bold text-base-content mb-1">
{formatCurrency(platformData?.openInterest || 0)}
</div>
<div
className={`text-sm ${changesToday.openInterestChange >= 0 ? 'text-success' : 'text-error'}`}>
{changesToday.openInterestChange >= 0 ? '+' : ''}{formatCurrency(changesToday.openInterestChange)} Today
</div>
</div>
</div>
<div className="bg-base-200 rounded-lg p-6">
<h3 className="text-lg font-semibold text-gray-400 mb-2">Long Positions</h3>
<div className="text-3xl font-bold text-green-400 mb-1">
{formatNumber(platformData?.positionCountByDirection?.Long || 0)}
</div>
<div className="text-sm text-gray-400">
{(() => {
const longCount = platformData?.positionCountByDirection?.Long || 0;
const shortCount = platformData?.positionCountByDirection?.Short || 0;
const total = longCount + shortCount;
if (total === 0) return '0%';
return ((longCount / total) * 100).toFixed(1) + '%';
})()} of total positions
<div className="card bg-base-200">
<div className="card-body">
<h3 className="card-title text-base-content/70 text-base">Total Positions</h3>
<div className="text-3xl font-bold text-base-content mb-1">
{formatNumber(platformData?.totalPositionCount || 0)}
</div>
<div
className={`text-sm ${changesToday.positionCountChange >= 0 ? 'text-success' : 'text-error'}`}>
{formatChangeIndicator(changesToday.positionCountChange)}
</div>
</div>
</div>
<div className="bg-base-200 rounded-lg p-6">
<h3 className="text-lg font-semibold text-gray-400 mb-2">Short Positions</h3>
<div className="text-3xl font-bold text-red-400 mb-1">
{formatNumber(platformData?.positionCountByDirection?.Short || 0)}
<div className="card bg-base-200">
<div className="card-body">
<h3 className="card-title text-base-content/70 text-base">Long Positions</h3>
<div className="text-3xl font-bold text-success mb-1">
{formatNumber(platformData?.positionCountByDirection?.Long || 0)}
</div>
<div className="text-sm text-base-content/60">
{(() => {
const longCount = platformData?.positionCountByDirection?.Long || 0;
const shortCount = platformData?.positionCountByDirection?.Short || 0;
const total = longCount + shortCount;
if (total === 0) return '0%';
return ((longCount / total) * 100).toFixed(1) + '%';
})()} of total positions
</div>
</div>
<div className="text-sm text-gray-400">
{(() => {
const longCount = platformData?.positionCountByDirection?.Long || 0;
const shortCount = platformData?.positionCountByDirection?.Short || 0;
const total = longCount + shortCount;
if (total === 0) return '0%';
return ((shortCount / total) * 100).toFixed(1) + '%';
})()} of total positions
</div>
<div className="card bg-base-200">
<div className="card-body">
<h3 className="card-title text-base-content/70 text-base">Short Positions</h3>
<div className="text-3xl font-bold text-error mb-1">
{formatNumber(platformData?.positionCountByDirection?.Short || 0)}
</div>
<div className="text-sm text-base-content/60">
{(() => {
const longCount = platformData?.positionCountByDirection?.Long || 0;
const shortCount = platformData?.positionCountByDirection?.Short || 0;
const total = longCount + shortCount;
if (total === 0) return '0%';
return ((shortCount / total) * 100).toFixed(1) + '%';
})()} of total positions
</div>
</div>
</div>
</div>
@@ -417,131 +440,152 @@ function PlatformSummary({index}: { index: number }) {
</div>
{/* Platform Metrics Chart */}
<div className="mb-8">
{/* Platform Metrics Charts */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
{/* Trading Metrics Chart */}
<PlatformLineChart
dailySnapshots={platformData?.dailySnapshots || []}
currentData={{
totalAgents: platformData?.totalAgents,
totalActiveStrategies: platformData?.totalActiveStrategies,
totalPlatformVolume: platformData?.totalPlatformVolume,
totalPlatformPnL: platformData?.totalPlatformPnL,
openInterest: platformData?.openInterest,
totalPositionCount: platformData?.totalPositionCount,
lastUpdated: platformData?.lastUpdated
}}
title="Platform Metrics Over Time"
title="Trading Metrics"
height={400}
metrics={['volume', 'pnl', 'openInterest', 'positionCount']}
/>
{/* User-Centric Metrics Chart */}
<PlatformLineChart
dailySnapshots={platformData?.dailySnapshots || []}
currentData={{
totalAgents: platformData?.totalAgents,
totalActiveStrategies: platformData?.totalActiveStrategies,
lastUpdated: platformData?.lastUpdated
}}
title="User-Centric Metrics"
height={400}
metrics={['agents', 'strategies']}
/>
</div>
{/* Volume and Positions by Asset */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
{/* Volume by Asset */}
<div className="bg-base-200 rounded-lg p-6">
<h3 className="text-lg font-semibold text-gray-400 mb-4">Volume by Asset</h3>
<div className="space-y-3 max-h-80 overflow-y-auto">
{platformData?.volumeByAsset && Object.keys(platformData.volumeByAsset).length > 0 ? (
Object.entries(platformData.volumeByAsset)
.sort(([, a], [, b]) => b - a)
.slice(0, 10)
.map(([asset, volume]) => (
<div key={asset} className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div
className="w-8 h-8 bg-blue-500 rounded-full flex items-center justify-center">
<span className="text-xs font-bold text-white">
{asset.substring(0, 2)}
</span>
<div className="card bg-base-200">
<div className="card-body">
<h3 className="card-title text-base-content/70">Volume by Asset</h3>
<div className="space-y-3 max-h-80 overflow-y-auto">
{platformData?.volumeByAsset && Object.keys(platformData.volumeByAsset).length > 0 ? (
Object.entries(platformData.volumeByAsset)
.sort(([, a], [, b]) => b - a)
.slice(0, 10)
.map(([asset, volume]) => (
<div key={asset} className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="avatar placeholder">
<div className="bg-primary text-primary-content rounded-full w-8">
<span className="text-xs font-bold">
{asset.substring(0, 2)}
</span>
</div>
</div>
<span className="text-base-content font-medium">{asset}</span>
</div>
<span className="text-white font-medium">{asset}</span>
</div>
<div className="text-right">
<div className="text-white font-semibold">
{formatCurrency(volume)}
<div className="text-right">
<div className="text-base-content font-semibold">
{formatCurrency(volume)}
</div>
</div>
</div>
</div>
))
) : (
<div className="text-gray-500 text-sm">No volume data available</div>
)}
))
) : (
<div className="text-base-content/60 text-sm">No volume data available</div>
)}
</div>
</div>
</div>
{/* Positions by Asset */}
<div className="bg-base-200 rounded-lg p-6">
<h3 className="text-lg font-semibold text-gray-400 mb-4">Positions by Asset</h3>
<div className="space-y-3 max-h-80 overflow-y-auto">
{platformData?.positionCountByAsset && Object.keys(platformData.positionCountByAsset).length > 0 ? (
Object.entries(platformData.positionCountByAsset)
.sort(([, a], [, b]) => b - a)
.slice(0, 10)
.map(([asset, count]) => (
<div key={asset} className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div
className="w-8 h-8 bg-purple-500 rounded-full flex items-center justify-center">
<span className="text-xs font-bold text-white">
{asset.substring(0, 2)}
</span>
<div className="card bg-base-200">
<div className="card-body">
<h3 className="card-title text-base-content/70">Positions by Asset</h3>
<div className="space-y-3 max-h-80 overflow-y-auto">
{platformData?.positionCountByAsset && Object.keys(platformData.positionCountByAsset).length > 0 ? (
Object.entries(platformData.positionCountByAsset)
.sort(([, a], [, b]) => b - a)
.slice(0, 10)
.map(([asset, count]) => (
<div key={asset} className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="avatar placeholder">
<div className="bg-secondary text-secondary-content rounded-full w-8">
<span className="text-xs font-bold">
{asset.substring(0, 2)}
</span>
</div>
</div>
<span className="text-base-content font-medium">{asset}</span>
</div>
<span className="text-white font-medium">{asset}</span>
</div>
<div className="text-right">
<div className="text-white font-semibold">
{formatNumber(count)} positions
</div>
<div className="text-xs text-gray-400">
{platformData?.totalPositionCount ?
(count / platformData.totalPositionCount * 100).toFixed(1) : 0}% of
total
<div className="text-right">
<div className="text-base-content font-semibold">
{formatNumber(count)} positions
</div>
<div className="text-xs text-base-content/60">
{platformData?.totalPositionCount ?
(count / platformData.totalPositionCount * 100).toFixed(1) : 0}% of
total
</div>
</div>
</div>
</div>
))
) : (
<div className="text-gray-500 text-sm">No position data available</div>
)}
))
) : (
<div className="text-base-content/60 text-sm">No position data available</div>
)}
</div>
</div>
</div>
</div>
{/* Data Freshness Indicator */}
<div className="bg-base-200 rounded-lg p-4">
<div className="flex items-center justify-between text-sm text-gray-400">
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<span>Last updated: {platformData?.lastUpdated ? new Date(platformData.lastUpdated).toLocaleString() : 'Unknown'}</span>
{isFetching && (
<div className="flex items-center gap-1 text-blue-400">
<div className="loading loading-spinner loading-xs"></div>
<span>Refreshing...</span>
</div>
)}
<div className="card bg-base-200">
<div className="card-body">
<div className="flex items-center justify-between text-sm text-base-content/60">
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<span>Last updated: {platformData?.lastUpdated ? new Date(platformData.lastUpdated).toLocaleString() : 'Unknown'}</span>
{isFetching && (
<div className="flex items-center gap-1 text-primary">
<div className="loading loading-spinner loading-xs"></div>
<span>Refreshing...</span>
</div>
)}
</div>
<button
onClick={handleForceRefresh}
disabled={isFetching}
className="btn btn-xs btn-outline btn-primary disabled:loading"
title="Force refresh data"
>
{isFetching ? (
<>
<span className="loading loading-spinner loading-xs"></span>
Refreshing...
</>
) : (
<>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-3 h-3">
<path strokeLinecap="round" strokeLinejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99" />
</svg>
Refresh
</>
)}
</button>
</div>
<button
onClick={handleForceRefresh}
disabled={isFetching}
className="btn btn-xs btn-outline btn-primary disabled:loading"
title="Force refresh data"
>
{isFetching ? (
<>
<span className="loading loading-spinner loading-xs"></span>
Refreshing...
</>
) : (
<>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-3 h-3">
<path strokeLinecap="round" strokeLinejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99" />
</svg>
Refresh
</>
)}
</button>
<span>Last snapshot: {platformData?.lastSnapshot ? new Date(platformData.lastSnapshot).toLocaleString() : 'Unknown'}</span>
</div>
<span>Last snapshot: {platformData?.lastSnapshot ? new Date(platformData.lastSnapshot).toLocaleString() : 'Unknown'}</span>
</div>
</div>
</div>