learning_ai_invt_trdg/web/src/tabs/AdminTab.tsx

983 lines
62 KiB
TypeScript

import React from 'react';
import { ConfigTab } from './ConfigTab';
import { ReconciliationAuditPanel } from './ReconciliationAuditPanel';
import {
ShieldCheck, Activity, Cpu, Hexagon,
Settings2, Bug,
Wifi, WifiOff,
TrendingUp, Clock, Crosshair, Zap,
Target, ShieldAlert, BrainCircuit,
ChevronRight, Pause, Play, AlertTriangle,
Database, RefreshCcw, Heart, Info, XCircle
} from 'lucide-react';
import type { BotState } from '../hooks/useWebSocket';
import type { Socket } from 'socket.io-client';
import { useAuth } from '../components/AuthContext';
import { fetchDynamicConfigItems, upsertDynamicConfigItems } from '../lib/dynamicConfigApi';
import { getPlatformAccessToken } from '../lib/authSession';
import { createRequestId } from '../../../shared/request-id.js';
import { tradingRuntime } from '../lib/runtime';
interface AdminTabProps {
botState: BotState;
socket: Socket | null;
}
const ruleDescriptions: { [key: string]: { desc: string; category: string; icon: typeof Hexagon } } = {
'TrendBiasRule': { desc: 'Analyzes 4H and 1H trends to determine the primary market bias using EMA crossovers.', category: 'Trend', icon: TrendingUp },
'SessionRule': { desc: 'Restricts trading to major market sessions — London and New York overlap.', category: 'Filter', icon: Clock },
'ZoneRule': { desc: 'Identifies key Support & Resistance zones for high-precision entries.', category: 'Entry', icon: Crosshair },
'MomentumRule': { desc: 'Analyzes RSI divergence and EMA crossovers for short-term momentum.', category: 'Momentum', icon: Zap },
'EntryTriggerRule': { desc: 'Final confirmation logic for executing market orders based on pattern detection.', category: 'Entry', icon: Target },
'RiskManagementRule': { desc: 'Calculates dynamic stop-loss, take-profit targets, and position sizing via ATR.', category: 'Risk', icon: ShieldAlert },
'AIAnalysisRule': { desc: 'Leverages LLM models (Perplexity / OpenAI) for real-time sentiment validation.', category: 'AI', icon: BrainCircuit }
};
const categoryColors: { [key: string]: string } = {
'Trend': 'var(--bl-info-strong)',
'Filter': 'var(--bl-emphasis)',
'Entry': 'var(--bl-warning)',
'Momentum': 'var(--bl-attention)',
'Risk': 'var(--bl-danger)',
'AI': 'var(--bl-success)',
};
export const AdminTab = ({ botState, socket }: AdminTabProps) => {
const { profile } = useAuth();
const [subTab, setSubTab] = React.useState<'rules' | 'config' | 'debug' | 'health' | 'reconciliation'>('rules');
const [debugLogs, setDebugLogs] = React.useState<any[]>([]);
const logEndRef = React.useRef<HTMLDivElement>(null);
React.useEffect(() => {
if (!socket) return;
const handler = (log: any) => {
setDebugLogs(prev => [...prev, log].slice(-100));
};
socket.on('debug_log', handler);
return () => { socket.off('debug_log', handler); };
}, [socket]);
React.useEffect(() => {
logEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [debugLogs]);
const rules = botState.settings.enabledRules || [];
const [botConfig, setBotConfig] = React.useState<Record<string, any> | null>(null);
const [isControlLoading, setIsControlLoading] = React.useState(false);
const [controlError, setControlError] = React.useState<string | null>(null);
// DB Sync Controls
const [dbSyncEnabled, setDbSyncEnabled] = React.useState<boolean>(true);
const [dbSyncInterval, setDbSyncInterval] = React.useState<number>(300000);
const [isDbSyncLoading, setIsDbSyncLoading] = React.useState(false);
const observabilityHealth = (botState.health || {}) as any;
const tradingControl = botState.health?.tradingControl;
const isPaused = tradingControl?.mode === 'PAUSED';
const operationalEvents = (botState.operationalEvents ?? []).filter((event): event is NonNullable<typeof event> => Boolean(event));
const formatDuration = (ms?: number) => {
if (!ms || !Number.isFinite(ms)) return 'Idle';
if (ms >= 1000) return `${(ms / 1000).toFixed(1)}s`;
return `${Math.max(ms, 0).toFixed(0)}ms`;
};
const resolveLoopStatus = (
lastRun: number | undefined,
isHealthy: boolean,
intervalMs: number
): 'Idle' | 'Active' | 'Degraded' | 'Failing' => {
if (!lastRun) return 'Idle';
const safeIntervalMs = Math.max(1, Number(intervalMs) || 60000);
const staleThresholdMs = Math.max(safeIntervalMs * 2, 120000);
const isStale = (Date.now() - lastRun) > staleThresholdMs;
if (!isHealthy) {
return isStale ? 'Degraded' : 'Failing';
}
return isStale ? 'Degraded' : 'Active';
};
const formatTimeAgo = (lastRun?: number) => {
if (!lastRun) return 'Waiting for samples...';
const seconds = Math.floor((Date.now() - lastRun) / 1000);
if (seconds <= 0) return 'Just now';
if (seconds < 60) return `${seconds}s ago`;
return `${Math.floor(seconds / 60)}m ${seconds % 60}s ago`;
};
const getStatusColor = (status: string) => {
if (status === 'Failing') return 'bg-red-500';
if (status === 'Idle') return 'bg-zinc-600';
if (status === 'Degraded') return 'bg-orange-500 animate-pulse';
return 'bg-emerald-500 animate-pulse';
};
const getStatusTextColor = (status: string) => {
if (status === 'Failing') return 'text-red-400';
if (status === 'Idle') return 'text-zinc-500';
if (status === 'Degraded') return 'text-orange-400';
return 'text-white';
};
const tradingLoopStatus = resolveLoopStatus(
observabilityHealth.tradingLoopLastRun,
Boolean(observabilityHealth.tradingLoopHealthy),
Number(botConfig?.POLLING_INTERVAL || 30000)
);
const monitorLoopStatus = resolveLoopStatus(
observabilityHealth.monitorLoopLastRun,
Boolean(observabilityHealth.monitorLoopHealthy),
Number(botConfig?.MONITOR_INTERVAL_MS || 60000)
);
const reconciliationLoopStatus = resolveLoopStatus(
observabilityHealth.reconciliationLoopLastRun,
Boolean(observabilityHealth.reconciliationLoopHealthy),
Number(botConfig?.MONITOR_INTERVAL_MS || 60000)
);
const reconciliationMismatchCount = Number(observabilityHealth.reconciliationMismatchCount || 0);
const reconciliationNoGoTrades = Number(observabilityHealth.reconciliationNoGoTrades || 0);
const reconciliationIntegrityWatchdogTriggered = Boolean(observabilityHealth.reconciliationIntegrityWatchdogTriggered);
const hasReconciliationBacklog = reconciliationMismatchCount > 0 || reconciliationNoGoTrades > 0 || reconciliationIntegrityWatchdogTriggered;
const systemCritical = tradingLoopStatus === 'Failing' || monitorLoopStatus === 'Failing';
const systemDegraded = !systemCritical && (
tradingLoopStatus === 'Degraded'
|| monitorLoopStatus === 'Degraded'
|| reconciliationLoopStatus === 'Degraded'
|| hasReconciliationBacklog
);
const systemBadgeLabel = systemCritical ? 'Critical' : systemDegraded ? 'Degraded' : 'Healthy';
const systemBadgeDotClass = systemCritical ? 'bg-red-500' : systemDegraded ? 'bg-orange-500' : 'bg-[var(--bl-success)]';
const systemBadgeTextClass = systemCritical ? 'text-red-400' : systemDegraded ? 'text-orange-400' : 'text-[var(--bl-success)]';
const adminPanelClass = 'bg-[var(--bl-surface-strong)] border border-[var(--bl-border-subtle)]';
const adminPanelOverlayClass = 'bg-[var(--bl-surface-overlay)] border border-[var(--bl-border-subtle)]';
const adminPanelHoverClass = 'bg-[var(--bl-surface-strong)] border border-[var(--bl-border-subtle)] hover:border-[var(--bl-border-soft)]';
const adminNavigationClass = 'bg-[var(--bl-surface-overlay)] border border-[var(--bl-border-subtle)]';
const adminActiveAccentClass = 'bg-[var(--bl-success)]/10 border border-[var(--bl-success)]/15';
const handlePauseTrading = async () => {
setIsControlLoading(true);
setControlError(null);
try {
const apiUrl = tradingRuntime.tradingApiUrl;
const accessToken = await getPlatformAccessToken();
const res = await fetch(`${apiUrl}/internal/trading/pause`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json',
'x-request-id': createRequestId('web-admin')
},
body: JSON.stringify({ reason: 'Admin pause from dashboard' })
});
if (!res.ok) {
const errorData = await res.json().catch(() => ({ error: 'Unknown error' }));
throw new Error(errorData.error || `HTTP ${res.status}`);
}
} catch (err: any) {
setControlError(err.message || 'Failed to pause trading');
} finally {
setIsControlLoading(false);
}
};
const handleResumeTrading = async () => {
setIsControlLoading(true);
setControlError(null);
try {
const apiUrl = tradingRuntime.tradingApiUrl;
const accessToken = await getPlatformAccessToken();
const res = await fetch(`${apiUrl}/internal/trading/resume`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json',
'x-request-id': createRequestId('web-admin')
},
body: JSON.stringify({ reason: 'Admin resume from dashboard' })
});
if (!res.ok) {
const errorData = await res.json().catch(() => ({ error: 'Unknown error' }));
throw new Error(errorData.error || `HTTP ${res.status}`);
}
} catch (err: any) {
setControlError(err.message || 'Failed to resume trading');
} finally {
setIsControlLoading(false);
}
};
const handleClearEvents = async () => {
setIsControlLoading(true);
setControlError(null);
try {
const apiUrl = tradingRuntime.tradingApiUrl;
const accessToken = await getPlatformAccessToken();
const res = await fetch(`${apiUrl}/api/events`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json',
'x-request-id': createRequestId('web-admin')
}
});
if (!res.ok) {
const errorData = await res.json().catch(() => ({ error: 'Unknown error' }));
throw new Error(errorData.error || `HTTP ${res.status}`);
}
} catch (err: any) {
setControlError(err.message || 'Failed to clear events');
} finally {
setIsControlLoading(false);
}
};
React.useEffect(() => {
if (profile?.role !== 'admin') return;
const fetchConfig = async () => {
try {
const apiUrl = tradingRuntime.tradingApiUrl;
const accessToken = await getPlatformAccessToken().catch(() => null);
if (!accessToken) return;
const res = await fetch(`${apiUrl}/api/config`, {
headers: {
'Authorization': `Bearer ${accessToken}`,
'x-request-id': createRequestId('web-admin')
}
});
if (res.ok) {
const data = await res.json();
setBotConfig(data);
}
} catch (err) {
console.error("Failed to fetch bot config:", err);
}
};
const fetchDbSyncSettings = async () => {
try {
const data = await fetchDynamicConfigItems();
if (data) {
data.forEach((item: { key: string; value: string; }) => {
if (item.key === 'ENABLE_DB_SNAPSHOTS') {
setDbSyncEnabled(item.value === 'true');
} else if (item.key === 'DB_SNAPSHOT_INTERVAL_MS') {
setDbSyncInterval(parseInt(item.value) || 300000);
}
});
}
} catch (err) {
console.error("Failed to fetch DB sync settings:", err);
}
};
fetchConfig();
fetchDbSyncSettings();
}, [profile?.role]);
const handleUpdateDbSync = async () => {
setIsDbSyncLoading(true);
try {
const updates = [
{ key: 'ENABLE_DB_SNAPSHOTS', value: String(dbSyncEnabled), description: 'Enable/Disable bot state snapshots to database' },
{ key: 'DB_SNAPSHOT_INTERVAL_MS', value: String(dbSyncInterval), description: 'Minimum interval between database snapshots in ms' }
];
await upsertDynamicConfigItems(updates);
} catch (err: any) {
setControlError(`DB Sync Update Failed: ${err.message}`);
} finally {
setIsDbSyncLoading(false);
}
};
const subTabs = [
{ id: 'rules' as const, label: 'Strategy Pipeline', icon: Hexagon },
{ id: 'config' as const, label: 'Global Config', icon: Settings2 },
{ id: 'health' as const, label: 'System Health', icon: Heart },
{ id: 'reconciliation' as const, label: 'Recon Audit', icon: ShieldAlert },
{ id: 'debug' as const, label: 'Debug', icon: Bug },
];
const renderSubContent = () => {
switch (subTab) {
case 'config':
return <ConfigTab />;
case 'health':
return (
<div className="space-y-6">
{/* Health Summary Grid */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{/* Trading Engine Card */}
<div className={`${adminPanelClass} rounded-2xl p-5`}>
<p className="text-[9px] text-zinc-500 uppercase tracking-widest mb-1">Trading Engine</p>
<div className="flex items-center gap-2">
<div className={`w-2 h-2 rounded-full ${getStatusColor(tradingLoopStatus)}`} />
<span className={`text-sm font-bold ${getStatusTextColor(tradingLoopStatus)}`}>
{tradingLoopStatus}
</span>
</div>
<p className="text-[10px] text-zinc-600 mt-2 flex items-center justify-between">
<span>{formatTimeAgo(observabilityHealth.tradingLoopLastRun)}</span>
{tradingLoopStatus === 'Degraded' && (
<span className="text-[9px] text-orange-500/80 font-bold uppercase tracking-tighter">Stale</span>
)}
</p>
</div>
{/* Position Monitor Card */}
<div className={`${adminPanelClass} rounded-2xl p-5`}>
<p className="text-[9px] text-zinc-500 uppercase tracking-widest mb-1">Position Monitor</p>
<div className="flex items-center gap-2">
<div className={`w-2 h-2 rounded-full ${getStatusColor(monitorLoopStatus)}`} />
<span className={`text-sm font-bold ${getStatusTextColor(monitorLoopStatus)}`}>
{monitorLoopStatus}
</span>
</div>
<p className="text-[10px] text-zinc-600 mt-2 flex items-center justify-between">
<span>{formatTimeAgo(observabilityHealth.monitorLoopLastRun)}</span>
{monitorLoopStatus === 'Degraded' && (
<span className="text-[9px] text-orange-500/80 font-bold uppercase tracking-tighter">Stale</span>
)}
</p>
</div>
{/* Reconciliation Card */}
<div className={`${adminPanelClass} rounded-2xl p-5`}>
<p className="text-[9px] text-zinc-500 uppercase tracking-widest mb-1">Reconciliation</p>
<div className="flex items-center gap-2">
<div className={`w-2 h-2 rounded-full ${getStatusColor(reconciliationLoopStatus)}`} />
<span className={`text-sm font-bold ${getStatusTextColor(reconciliationLoopStatus)}`}>
{reconciliationLoopStatus}
</span>
</div>
<div className="text-[10px] text-zinc-600 mt-2 flex items-center justify-between">
<span>{formatTimeAgo(observabilityHealth.reconciliationLoopLastRun)}</span>
{reconciliationLoopStatus === 'Degraded' && (
<span className="text-[9px] text-orange-500/80 font-bold uppercase tracking-tighter">Stale</span>
)}
</div>
<p className="text-[10px] text-zinc-600 mt-1">
Mismatches: {reconciliationMismatchCount} | NO_GO: {reconciliationNoGoTrades}
</p>
{reconciliationIntegrityWatchdogTriggered && (
<p className="text-[10px] text-red-400 mt-1 uppercase tracking-wide">Integrity watchdog active</p>
)}
</div>
</div>
{/* Operational Events (Admin Error Panel) */}
<section className={`${adminPanelClass} rounded-2xl overflow-hidden`}>
<div className="px-5 py-4 border-b border-white/[0.04] flex items-center justify-between">
<div className="flex items-center gap-3">
<ShieldAlert size={14} className="text-orange-400" />
<h3 className="text-xs font-bold text-zinc-300 uppercase tracking-wider">Operational Events</h3>
<button
onClick={handleClearEvents}
disabled={isControlLoading}
className="ml-4 px-2 py-0.5 bg-zinc-800 hover:bg-zinc-700 text-zinc-400 hover:text-white rounded text-[10px] transition-colors disabled:opacity-50"
>
Clear History
</button>
</div>
<span className="text-[10px] text-zinc-500 font-mono">
Buffer: {operationalEvents.length || 0} events
</span>
</div>
{/* 24h Severity Summary Bar */}
{operationalEvents.length > 0 && (
<div className="px-5 py-3 bg-white/[0.01] border-b border-white/[0.04] flex items-center gap-6">
<div className="flex items-center gap-2">
<div className="w-1.5 h-1.5 rounded-full bg-red-500" />
<span className="text-[10px] text-zinc-400 uppercase tracking-wider font-bold">
Errors: <span className="text-red-400 text-xs ml-1">
{operationalEvents.filter(e => e.severity === 'ERROR' && (Date.now() - e.timestamp < 86400000)).length}
</span>
</span>
</div>
<div className="flex items-center gap-2">
<div className="w-1.5 h-1.5 rounded-full bg-orange-500" />
<span className="text-[10px] text-zinc-400 uppercase tracking-wider font-bold">
Warnings: <span className="text-orange-400 text-xs ml-1">
{operationalEvents.filter(e => e.severity === 'WARN' && (Date.now() - e.timestamp < 86400000)).length}
</span>
</span>
</div>
<div className="flex items-center gap-2">
<div className="w-1.5 h-1.5 rounded-full bg-blue-500" />
<span className="text-[10px] text-zinc-400 uppercase tracking-wider font-bold">
Info: <span className="text-blue-400 text-xs ml-1">
{operationalEvents.filter(e => e.severity === 'INFO' && (Date.now() - e.timestamp < 86400000)).length}
</span>
</span>
</div>
<div className="ml-auto text-[9px] text-zinc-600 font-bold uppercase tracking-widest">
Buffer Distribution
</div>
</div>
)}
<div className="max-h-[400px] overflow-y-auto custom-scrollbar">
{(operationalEvents.length === 0) ? (
<div className="py-12 flex flex-col items-center justify-center text-zinc-600">
<ShieldCheck size={24} className="opacity-20 mb-2" />
<p className="text-xs">No actionable issues detected</p>
</div>
) : (
<div className="divide-y divide-white/[0.02]">
{operationalEvents.map((event) => (
<div key={event.id} className="px-5 py-3 hover:bg-white/[0.01] transition-colors">
<div className="flex items-start justify-between gap-4">
<div className="flex items-start gap-3">
<div className="mt-0.5">
{event.severity === 'ERROR' ? (
<XCircle size={14} className="text-red-500" />
) : event.severity === 'WARN' ? (
<AlertTriangle size={14} className="text-orange-500" />
) : (
<Info size={14} className="text-blue-500" />
)}
</div>
<div>
<div className="flex items-center gap-2 flex-wrap">
<span className={`text-[10px] font-black tracking-widest ${event.severity === 'ERROR' ? 'text-red-500' :
event.severity === 'WARN' ? 'text-orange-500' : 'text-blue-500'
}`}>
{event.type}
</span>
{event.symbol && (
<span className="text-[10px] font-mono text-zinc-400 bg-zinc-800/50 px-1.5 py-0.5 rounded">
{event.symbol}
</span>
)}
{event.profileId && (
<span className="text-[10px] text-zinc-500">
profile: {event.profileId.slice(-8)}
</span>
)}
</div>
<p className="text-xs text-zinc-300 mt-1 leading-relaxed">{event.message}</p>
</div>
</div>
<span className="text-[10px] font-mono text-zinc-600 whitespace-nowrap">
{new Date(event.timestamp).toLocaleTimeString()}
</span>
</div>
</div>
))}
</div>
)}
</div>
</section>
{/* Detailed Metrics */}
<section className={`${adminPanelClass} rounded-2xl p-5`}>
<div className="flex items-center gap-3 mb-6">
<Cpu size={14} className="text-blue-400" />
<h3 className="text-xs font-bold text-zinc-300 uppercase tracking-wider">Performance Telemetry</h3>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-6">
<div>
<div className="flex items-center justify-between mb-1">
<p className="text-[9px] text-zinc-500 uppercase tracking-wider">Avg Execution Loop</p>
<span className={`text-[8px] px-1 rounded ${tradingLoopStatus === 'Active' ? 'bg-emerald-500/10 text-emerald-500' :
tradingLoopStatus === 'Degraded' ? 'bg-orange-500/10 text-orange-500' :
tradingLoopStatus === 'Failing' ? 'bg-red-500/10 text-red-500' : 'bg-zinc-500/10 text-zinc-500'
}`}>
{tradingLoopStatus}
</span>
</div>
<p className="text-lg font-mono font-bold text-white">{formatDuration((observabilityHealth as any)?.tradingLoopDurationMs)}</p>
</div>
<div>
<div className="flex items-center justify-between mb-1">
<p className="text-[9px] text-zinc-500 uppercase tracking-wider">Reconciliation Loop</p>
<span className={`text-[8px] px-1 rounded ${reconciliationLoopStatus === 'Active' ? 'bg-emerald-500/10 text-emerald-500' :
reconciliationLoopStatus === 'Degraded' ? 'bg-orange-500/10 text-orange-500' :
reconciliationLoopStatus === 'Failing' ? 'bg-red-500/10 text-red-500' : 'bg-zinc-500/10 text-zinc-500'
}`}>
{reconciliationLoopStatus}
</span>
</div>
<p className="text-lg font-mono font-bold text-white">{formatDuration((observabilityHealth as any)?.reconciliationLoopDurationMs)}</p>
</div>
<div>
<div className="flex items-center justify-between mb-1">
<p className="text-[9px] text-zinc-500 uppercase tracking-wider">Exchange Latency (p95)</p>
<span className={`text-[8px] px-1 rounded ${(observabilityHealth as any)?.exchangeLatencyP95 ? 'bg-emerald-500/10 text-emerald-500' : 'bg-zinc-500/10 text-zinc-500'
}`}>
{(observabilityHealth as any)?.exchangeLatencyP95 ? 'Active' : 'Idle'}
</span>
</div>
<p className="text-lg font-mono font-bold text-white">
{(observabilityHealth as any)?.exchangeLatencyP95 ? `${((observabilityHealth as any).exchangeLatencyP95).toFixed(1)}ms` : 'Idle'}
</p>
</div>
<div>
<div className="flex items-center justify-between mb-1">
<p className="text-[9px] text-zinc-500 uppercase tracking-wider">Uptime</p>
<span className="text-[8px] px-1 rounded bg-emerald-500/10 text-emerald-500">Active</span>
</div>
<p className="text-lg font-mono font-bold text-white">
{formatDuration(botState.uptime)}
</p>
</div>
</div>
<div className="grid grid-cols-2 lg:grid-cols-3 gap-6 mt-8 pt-8 border-t border-white/[0.02]">
<div>
<p className="text-[9px] text-zinc-500 uppercase tracking-wider mb-1">Entry Lock Contention</p>
<p className="text-sm font-mono text-white">{(observabilityHealth as any)?.entryLockContentionCount ?? 0} hits</p>
</div>
<div>
<p className="text-[9px] text-zinc-500 uppercase tracking-wider mb-1">Recon Lock Contention</p>
<p className="text-sm font-mono text-white">{(observabilityHealth as any)?.reconciliationLockContentionCount ?? 0} hits</p>
</div>
<div>
<p className="text-[9px] text-zinc-500 uppercase tracking-wider mb-1">Order Failures (24h)</p>
<p className="text-sm font-mono text-white">{botState.orderFailures?.length ?? 0} events</p>
</div>
</div>
</section>
</div>
);
case 'reconciliation':
return <ReconciliationAuditPanel />;
case 'debug':
return (
<div className="space-y-8">
{/* Dashboard Environment */}
<section>
<div className="flex items-center gap-3 mb-4">
<Activity size={14} className="text-blue-400" />
<h3 className="text-xs font-bold text-zinc-300 uppercase tracking-wider">Dashboard Environment</h3>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
{Object.entries(import.meta.env)
.filter(([key]) => key.startsWith('VITE_') || key === 'MODE')
.map(([key, value]) => (
<div key={key} className={`flex items-center justify-between gap-4 px-4 py-3 ${adminPanelClass} rounded-xl`}>
<span className="text-[10px] font-bold text-zinc-500 uppercase tracking-wider truncate">{key}</span>
<span className="text-[10px] font-mono text-blue-400/80 bg-blue-500/[0.06] px-2.5 py-1 rounded-lg truncate max-w-[260px]">
{String(value)}
</span>
</div>
))}
</div>
</section>
{/* Bot Config */}
<section>
<div className="flex items-center gap-3 mb-4">
<Cpu size={14} className="text-orange-400" />
<h3 className="text-xs font-bold text-zinc-300 uppercase tracking-wider">Bot Configuration</h3>
</div>
{botConfig ? (
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
{Object.entries(botConfig).map(([key, value]) => (
<div key={key} className={`flex items-center justify-between gap-4 px-4 py-3 ${adminPanelClass} rounded-xl`}>
<span className="text-[10px] font-bold text-zinc-500 uppercase tracking-wider truncate">{key}</span>
<span className="text-[10px] font-mono text-orange-400/80 bg-orange-500/[0.06] px-2.5 py-1 rounded-lg truncate max-w-[260px]">
{Array.isArray(value) ? value.join(', ') : String(value)}
</span>
</div>
))}
</div>
) : (
<div className={`flex items-center justify-center gap-3 py-12 ${adminPanelClass} border-dashed border-red-500/10 rounded-xl`}>
<WifiOff size={18} className="text-red-500/40" />
<p className="text-xs text-red-500/60 font-medium">Bot offline unable to fetch config</p>
</div>
)}
</section>
{/* Live Debug Logs */}
<section>
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-3">
<Bug size={14} className="text-emerald-400" />
<h3 className="text-xs font-bold text-zinc-300 uppercase tracking-wider">Live Debug Stream</h3>
</div>
<span className="text-[9px] text-zinc-600 font-mono uppercase tracking-widest">Buffer: 100 entries</span>
</div>
<div className={`${adminPanelOverlayClass} rounded-xl overflow-hidden shadow-2xl`}>
<div className="flex items-center gap-2 px-4 py-2 bg-white/[0.02] border-b border-white/[0.04]">
<div className="flex gap-1.5">
<div className="w-2.5 h-2.5 rounded-full bg-red-500/20" />
<div className="w-2.5 h-2.5 rounded-full bg-orange-500/20" />
<div className="w-2.5 h-2.5 rounded-full bg-emerald-500/20" />
</div>
<span className="text-[9px] text-zinc-600 font-mono ml-2">bytelyst-stdout</span>
</div>
<div className="p-4 h-[400px] overflow-y-auto font-mono text-[11px] custom-scrollbar selection:bg-emerald-500/30">
{debugLogs.length === 0 ? (
<div className="h-full flex flex-col items-center justify-center text-zinc-700 animate-pulse">
<Bug size={32} className="opacity-20 mb-4" />
<p>Awaiting debug events...</p>
</div>
) : (
<div className="space-y-1">
{debugLogs.map((log, idx) => (
<div key={idx} className="flex gap-3 py-0.5 group">
<span className="text-zinc-700 shrink-0 select-none">[{new Date().toLocaleTimeString([], { hour12: false })}]</span>
<span className="text-emerald-400/90 break-all">{typeof log === 'string' ? log : JSON.stringify(log)}</span>
</div>
))}
<div ref={logEndRef} />
</div>
)}
</div>
</div>
</section>
</div>
);
default: // rules
return (
<div className="space-y-4">
{rules.length === 0 ? (
<div className={`flex flex-col items-center justify-center py-20 ${adminPanelClass} border-dashed border-white/[0.06] rounded-xl`}>
<Hexagon size={28} className="text-zinc-700 mb-4" />
<p className="text-sm font-semibold text-zinc-500">No Active Rules</p>
<p className="text-[11px] text-zinc-600 mt-1 max-w-xs text-center">Connect to the bot service to load the pipeline.</p>
</div>
) : (
<>
{/* Pipeline header */}
<div className="flex items-center justify-between px-1 pb-2">
<p className="text-[10px] text-zinc-500 font-medium">
{rules.length} rules execute sequentially per trading cycle
</p>
<span className="flex items-center gap-1.5 text-[10px] text-zinc-500">
<span className="w-1.5 h-1.5 rounded-full bg-[var(--bl-success)] animate-pulse" /> All active
</span>
</div>
{/* Rule cards */}
<div className="space-y-2">
{rules.map((rule, idx) => {
const info = ruleDescriptions[rule] || { desc: 'Technical strategy rule.', category: 'System', icon: Hexagon };
const color = categoryColors[info.category] || 'var(--bl-text-quiet)';
const RuleIcon = info.icon;
const isLast = idx === rules.length - 1;
return (
<React.Fragment key={rule}>
<div className={`group relative ${adminPanelHoverClass} rounded-xl transition-all duration-200`}>
{/* Left accent */}
<div
className="absolute left-0 top-3 bottom-3 w-[3px] rounded-full opacity-50 group-hover:opacity-100 transition-opacity"
style={{ background: color }}
/>
<div className="flex items-center gap-4 pl-5 pr-5 py-4">
{/* Step number */}
<span className="text-[10px] font-bold text-zinc-600 font-mono w-5 text-center shrink-0">
{idx + 1}
</span>
{/* Icon */}
<div
className="shrink-0 w-9 h-9 rounded-lg flex items-center justify-center border group-hover:scale-105 transition-transform"
style={{
background: `${color}10`,
borderColor: `${color}20`,
color: color
}}
>
<RuleIcon size={16} />
</div>
{/* Text */}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-0.5">
<h4 className="text-[13px] font-bold text-white tracking-tight">
{rule.replace('Rule', '')}
</h4>
<span
className="text-[8px] font-bold uppercase tracking-widest px-1.5 py-0.5 rounded border"
style={{
background: `${color}08`,
borderColor: `${color}18`,
color: color
}}
>
{info.category}
</span>
</div>
<p className="text-[11px] text-zinc-500 group-hover:text-zinc-400 transition-colors leading-relaxed">
{info.desc}
</p>
</div>
{/* Status */}
<div className="shrink-0">
<span className="flex items-center gap-1.5 px-2.5 py-1 bg-[var(--bl-success)]/10 border border-[var(--bl-success)]/10 rounded-full">
<span className="w-1.5 h-1.5 rounded-full bg-[var(--bl-success)]" />
<span className="text-[9px] font-bold text-[var(--bl-success)]/80 uppercase tracking-wider">Active</span>
</span>
</div>
</div>
</div>
{/* Connector arrow between rules */}
{!isLast && (
<div className="flex justify-center py-0.5">
<ChevronRight size={12} className="text-zinc-700 rotate-90" />
</div>
)}
</React.Fragment>
);
})}
</div>
{/* Pipeline footer note */}
<p className="text-[10px] text-zinc-600 text-center pt-3">
Rules execute in order each must pass before the next is evaluated
</p>
{/* Trading Control Panel */}
<section className="mt-6 space-y-4">
<div className="flex items-center gap-3 mb-4">
<ShieldCheck size={14} className={isPaused ? "text-orange-400" : "text-emerald-400"} />
<h3 className="text-xs font-bold text-zinc-300 uppercase tracking-wider">Trading Control</h3>
</div>
{/* Status Banner */}
<div className={`flex items-center justify-between px-4 py-3 rounded-xl border ${isPaused
? 'bg-orange-500/[0.06] border-orange-500/20'
: 'bg-emerald-500/[0.06] border-emerald-500/20'
}`}>
<div className="flex items-center gap-3">
{isPaused ? (
<Pause size={16} className="text-orange-400" />
) : (
<Play size={16} className="text-emerald-400" />
)}
<div>
<p className={`text-sm font-bold ${isPaused ? 'text-orange-300' : 'text-emerald-300'}`}>
AUTO-TRADING: {isPaused ? 'PAUSED' : 'RUNNING'}
</p>
<p className="text-[10px] text-zinc-500 mt-0.5">
{isPaused
? 'No new positions will be opened. Existing positions are still managed.'
: 'Bot is actively monitoring and executing trades based on strategy rules.'}
</p>
</div>
</div>
{tradingControl && (
<div className="text-right">
<p className="text-[9px] text-zinc-500 uppercase tracking-wider">Last Changed</p>
<p className="text-[10px] text-zinc-400 font-mono">
{new Date(tradingControl.lastChangedAt).toLocaleString()}
</p>
<p className="text-[9px] text-zinc-600">by {tradingControl.lastChangedBy}</p>
</div>
)}
</div>
{/* Control Buttons */}
<div className="grid grid-cols-2 gap-3">
<button
onClick={handlePauseTrading}
disabled={isPaused || isControlLoading}
className={`flex items-center justify-center gap-2 px-4 py-3 rounded-xl font-bold text-sm transition-all ${isPaused || isControlLoading
? 'bg-zinc-800/50 border border-zinc-700/30 text-zinc-600 cursor-not-allowed'
: 'bg-orange-500/10 border border-orange-500/20 text-orange-400 hover:bg-orange-500/20 hover:border-orange-500/30'
}`}
title={isPaused ? "Trading is already paused" : "Pause all new trade entries"}
>
<Pause size={16} />
{isControlLoading && !isPaused ? 'Pausing...' : 'Pause Auto Trading'}
</button>
<button
onClick={handleResumeTrading}
disabled={!isPaused || isControlLoading}
className={`flex items-center justify-center gap-2 px-4 py-3 rounded-xl font-bold text-sm transition-all ${!isPaused || isControlLoading
? 'bg-zinc-800/50 border border-zinc-700/30 text-zinc-600 cursor-not-allowed'
: 'bg-emerald-500/10 border border-emerald-500/20 text-emerald-400 hover:bg-emerald-500/20 hover:border-emerald-500/30'
}`}
title={!isPaused ? "Trading is already running" : "Resume automated trade execution"}
>
<Play size={16} />
{isControlLoading && isPaused ? 'Resuming...' : 'Resume Auto Trading'}
</button>
</div>
{/* Error Display */}
{controlError && (
<div className="flex items-center gap-2 px-4 py-3 bg-red-500/[0.06] border border-red-500/20 rounded-xl">
<AlertTriangle size={14} className="text-red-400 shrink-0" />
<p className="text-xs text-red-400">{controlError}</p>
</div>
)}
{/* Safety Notice */}
<div className="flex items-start gap-2 px-4 py-3 bg-blue-500/[0.04] border border-blue-500/10 rounded-xl">
<AlertTriangle size={12} className="text-blue-400 shrink-0 mt-0.5" />
<p className="text-[10px] text-blue-400/80 leading-relaxed">
<strong>Safety Note:</strong> Pausing trading blocks new entries only.
Existing positions will continue to be monitored for exits, stop-losses, and take-profits.
</p>
</div>
</section>
{/* Database Synchronization Control */}
<section className="mt-6 space-y-4 pt-6 border-t border-white/[0.04]">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-3">
<Database size={14} className="text-cyan-400" />
<h3 className="text-xs font-bold text-zinc-300 uppercase tracking-wider">Database Synchronization</h3>
</div>
<div className="flex items-center gap-1.5 px-2 py-0.5 bg-cyan-500/10 border border-cyan-500/20 rounded-full">
<span className="w-1 h-1 rounded-full bg-cyan-400" />
<span className="text-[8px] font-bold text-cyan-400 uppercase tracking-widest">Neural Persistence</span>
</div>
</div>
<div className={`${adminPanelClass} rounded-2xl overflow-hidden p-5 space-y-5`}>
<div className="flex items-center justify-between">
<div className="space-y-1">
<p className="text-xs font-bold text-white">State Snapshots</p>
<p className="text-[10px] text-zinc-500">Persistent sync of bot state to Supabase</p>
</div>
<button
onClick={() => setDbSyncEnabled(!dbSyncEnabled)}
className={`relative inline-flex h-5 w-10 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none ${dbSyncEnabled ? 'bg-cyan-500' : 'bg-zinc-700'
}`}
>
<span
className={`inline-block h-4 w-4 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out ${dbSyncEnabled ? 'translate-x-5' : 'translate-x-0'
}`}
/>
</button>
</div>
<div className="space-y-3">
<div className="flex items-center justify-between">
<div className="space-y-1">
<p className="text-xs font-bold text-white">Sync Interval</p>
<p className="text-[10px] text-zinc-500">Minimum time between DB writes ({Math.round(dbSyncInterval / 60000)}m)</p>
</div>
<div className="flex items-center gap-3">
<input
type="range"
min="60000"
max="3600000"
step="60000"
value={dbSyncInterval}
onChange={(e) => setDbSyncInterval(parseInt(e.target.value))}
className="w-32 accent-cyan-500"
/>
<span className="text-[10px] font-mono text-cyan-400 w-12 text-right">
{(dbSyncInterval / 60000).toFixed(0)}m
</span>
</div>
</div>
<div className="flex items-center gap-2 p-3 bg-cyan-500/[0.03] border border-cyan-500/10 rounded-xl">
<RefreshCcw size={12} className="text-cyan-400/60" />
<p className="text-[9px] text-zinc-500 leading-relaxed italic">
Increasing the interval reduces database IOPS and prevents service throttling under high volatility.
</p>
</div>
</div>
<button
onClick={handleUpdateDbSync}
disabled={isDbSyncLoading}
className={`w-full py-2.5 rounded-xl font-bold text-[11px] uppercase tracking-widest transition-all ${isDbSyncLoading
? 'bg-zinc-800 text-zinc-600 cursor-not-allowed'
: 'bg-cyan-500/10 border border-cyan-500/20 text-cyan-400 hover:bg-cyan-500/20 shadow-lg shadow-cyan-500/5'
}`}
>
{isDbSyncLoading ? 'Synchronizing...' : 'Apply DB Sync Parameters'}
</button>
</div>
</section>
</>
)}
</div>
);
}
};
if (profile?.role !== 'admin') {
return (
<div className={`p-8 text-center ${adminPanelClass} border-red-500/20 rounded-2xl`}>
<XCircle className="mx-auto text-red-500 mb-4" size={48} />
<h2 className="text-xl font-bold text-white mb-2">Access Denied</h2>
<p className="text-zinc-500">You do not have administrative privileges to access this area.</p>
</div>
);
}
return (
<div className="space-y-6 max-w-5xl mx-auto">
{/* Header */}
<header className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className={`w-10 h-10 rounded-xl ${adminActiveAccentClass} flex items-center justify-center`}>
<ShieldCheck size={18} className="text-[var(--bl-success)]" />
</div>
<div>
<h2 className="text-lg font-bold text-white tracking-tight">Admin Panel</h2>
<p className="text-[10px] text-zinc-500">System configuration & rule pipeline</p>
</div>
</div>
<div className="flex items-center gap-3">
<div className={`flex items-center gap-1.5 px-3 py-1.5 ${adminPanelClass} rounded-lg`}>
{botConfig ? (
<>
<Wifi size={11} className="text-emerald-500" />
<span className="text-[10px] text-emerald-400 font-medium">Bot Connected</span>
</>
) : (
<>
<WifiOff size={11} className="text-zinc-600" />
<span className="text-[10px] text-zinc-500 font-medium">Bot Offline</span>
</>
)}
</div>
{/* System Status Badge */}
<div className={`flex items-center gap-1.5 px-3 py-1.5 ${adminPanelClass} rounded-lg`}>
<div className={`w-1.5 h-1.5 rounded-full ${systemBadgeDotClass}`} />
<span className={`text-[10px] font-bold uppercase tracking-widest ${systemBadgeTextClass}`}>
System: {systemBadgeLabel}
</span>
</div>
</div>
</header>
{/* Sub Navigation */}
<nav className={`flex gap-1 ${adminNavigationClass} rounded-xl p-1`}>
{subTabs.map((tab) => {
const Icon = tab.icon;
const isActive = subTab === tab.id;
return (
<button
key={tab.id}
onClick={() => setSubTab(tab.id)}
className={`flex items-center gap-2 px-4 py-2.5 rounded-lg text-[11px] font-bold transition-all duration-200 ${isActive
? 'bg-white/[0.07] text-white'
: 'text-zinc-500 hover:text-zinc-300 hover:bg-white/[0.02]'
}`}
>
<Icon size={13} className={isActive ? 'text-[var(--bl-success)]' : ''} />
<span>{tab.label}</span>
</button>
);
})}
</nav>
{/* Content */}
<main className="min-h-[300px]">
{renderSubContent()}
</main>
</div>
);
};