learning_ai_invt_trdg/web/src/App.tsx

429 lines
20 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.

import { useState, useEffect, useCallback } from 'react';
import { useWebSocket } from './hooks/useWebSocket';
import { LivePulseTicker } from './components/LivePulseTicker';
import { OverviewTab } from './tabs/OverviewTab';
import { SignalsTab } from './tabs/SignalsTab';
import { PositionsTab } from './tabs/PositionsTab';
import { HistoryTab } from './tabs/HistoryTab';
import { SettingsTab } from './tabs/SettingsTab';
import { EntriesTab } from './tabs/EntriesTab';
import { AdminTab } from './tabs/AdminTab';
import { TradeProfileManager } from './components/TradeProfileManager';
import { StrategyWizard } from './components/StrategyWizard';
import { MyStrategiesTab } from './tabs/MyStrategiesTab';
import { MarketplaceTab } from './tabs/MarketplaceTab';
import { MembershipTab } from './tabs/MembershipTab';
import { BacktestTab } from './tabs/BacktestTab';
import { ChatControl } from './components/ChatControl';
import type { StrategyPreset } from './lib/PresetRegistry';
import { RISK_STYLE_TEMPLATES } from './lib/RiskStyleTemplates';
import './App.css';
import { useAuth } from './components/AuthContext';
import { Login } from './components/Login';
import { ResetPassword } from './components/ResetPassword';
import { supabase } from './lib/supabaseClient';
import { tableNameProfiles } from './lib/const';
import { useBacktestFeatureGate } from './backtest/useBacktestFeatureGate';
import { tradingRuntime, tradingTelemetry } from './lib/runtime';
export const resolveProfileNameForAction = (
action: string,
requestedName: string | undefined,
chatProfiles: Array<{ name?: string }>,
suffixProvider: () => string = () => new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }).replace(/:/g, '')
) => {
let profileName = requestedName || 'AI Profile';
if (action === 'create_profile') {
const existing = chatProfiles.find(p => p.name === profileName);
if (existing) {
profileName = `${profileName} (${suffixProvider()})`;
}
}
return profileName;
};
export const buildChatApplyPayload = (
profileData: any,
currentUserId: string,
profileName: string
) => ({
name: profileName,
user_id: currentUserId,
allocated_capital: Number(profileData.allocated_capital || 1000),
risk_per_trade_percent: Number(profileData.risk_per_trade_percent || 1),
symbols: profileData.symbols || 'BTC/USDT',
is_active: profileData.is_active ?? true,
strategy_config: profileData.strategy_config,
});
function App() {
const { user, profile, loading, signOut } = useAuth();
const { botState, connected } = useWebSocket(tradingRuntime.tradingApiUrl);
const [activeTab, setActiveTab] = useState('overview');
const [wizardSeed, setWizardSeed] = useState<any>(null);
const [chatProfiles, setChatProfiles] = useState<any[]>([]);
const [previewAsCustomer, setPreviewAsCustomer] = useState(false);
const { enabled: backtestEnabledForView, loading: backtestGateLoading } = useBacktestFeatureGate({ previewAsCustomer });
const systemHealthData = botState.health || {};
const hasCapitalViolation = ((systemHealthData as any)?.capitalInvariantViolations || 0) > 0;
const anyLoopUnhealthy = (systemHealthData as any)?.tradingLoopHealthy === false || (systemHealthData as any)?.reconciliationLoopHealthy === false;
const systemHealthState = hasCapitalViolation ? 'Unhealthy' : anyLoopUnhealthy ? 'Degraded' : 'Healthy';
const systemHealthColor = hasCapitalViolation ? '#ff6657' : anyLoopUnhealthy ? '#facc15' : '#34c759';
const systemHealthTooltip = hasCapitalViolation
? 'Capital invariant violation detected—resolve ledger divergence before trading resumes.'
: anyLoopUnhealthy
? 'Degraded indicates a trading or reconciliation loop is lagging or experiencing lock contention.'
: 'All monitored loops are healthy.';
const recentCriticalEvents = (botState.operationalEvents || []).filter(e =>
(e.severity === 'ERROR' || e.severity === 'WARN') &&
(Date.now() - e.timestamp < 600000)
);
const hasCriticalEvents = recentCriticalEvents.length > 0;
const fetchChatProfiles = useCallback(async () => {
const { data } = await supabase.from(tableNameProfiles).select('*').order('created_at', { ascending: false });
setChatProfiles(data || []);
}, []);
useEffect(() => {
if (user) {
fetchChatProfiles();
const interval = setInterval(fetchChatProfiles, 30000);
return () => clearInterval(interval);
}
}, [user, fetchChatProfiles]);
const handleChatApply = async (action: string, profileData: any): Promise<{ success: boolean; error?: string }> => {
const profileName = resolveProfileNameForAction(action, profileData.name, chatProfiles);
// IMPORTANT: Use the authenticated user's ID (auth.uid()) for RLS compliance
const currentUserId = user?.id;
if (!currentUserId) {
console.error('[ChatApply] No authenticated user found');
return { success: false, error: 'Not authenticated' };
}
const payload = buildChatApplyPayload(profileData, currentUserId, profileName);
console.log('[ChatApply] Action:', action, 'user_id:', currentUserId, 'Payload:', payload);
if (action === 'create_profile') {
const { error } = await supabase.from(tableNameProfiles).insert([payload]);
if (error) {
console.error('[ChatApply] Insert error:', error);
return { success: false, error: error.message };
}
console.log('[ChatApply] Profile created successfully:', profileName);
fetchChatProfiles();
window.dispatchEvent(new Event('profiles-updated'));
return { success: true };
} else if (action === 'update_profile' && profileData.id) {
const { error } = await supabase.from(tableNameProfiles).update(payload).eq('id', profileData.id);
if (error) {
console.error('[ChatApply] Update error:', error);
return { success: false, error: error.message };
}
console.log('[ChatApply] Profile updated successfully:', profileName);
fetchChatProfiles();
window.dispatchEvent(new Event('profiles-updated'));
return { success: true };
}
return { success: false, error: 'Unknown action' };
};
const handleClonePreset = (preset: StrategyPreset) => {
const style = RISK_STYLE_TEMPLATES.find(t => t.id === preset.riskStyleId);
const dummyProfile = {
name: `${preset.name} (Clone)`,
symbols: preset.recommendedAssets.join(','),
allocated_capital: 1000,
is_active: false,
strategy_config: {
execution: { minRulePassRatio: style?.minRulePassRatio || 1.0 },
riskLimits: { dailyProfitTargetUsd: 100 },
rules: [
{ ruleId: 'RiskManagementRule', enabled: true, ruleType: 'mandatory' },
{ ruleId: 'SessionRule', enabled: true, ruleType: 'mandatory', params: { sessions: 'LDN,NY' } },
...(style?.mandatoryRules || []).map(r => ({ ruleId: r, enabled: true, ruleType: 'mandatory' })),
...(style?.votingRules || []).map(r => ({ ruleId: r, enabled: true, ruleType: 'voting' }))
]
}
};
setWizardSeed(dummyProfile);
setActiveTab('wizard');
};
// Handle Password Reset Route
if (window.location.pathname === '/reset-callback') {
return <ResetPassword />;
}
if (loading) {
return <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh', background: '#0a0b0d', color: '#fff' }}>Loading...</div>;
}
if (!user) {
return <Login />;
}
const isAdminAccount = profile?.role === 'admin';
const isAdmin = isAdminAccount && !previewAsCustomer;
const showBacktestTab = isAdmin || (!backtestGateLoading && backtestEnabledForView);
const renderTab = () => {
switch (activeTab) {
case 'overview': return <OverviewTab botState={botState} previewAsCustomer={previewAsCustomer} connected={connected} />;
case 'signals':
if (!isAdmin) return <OverviewTab botState={botState} previewAsCustomer={previewAsCustomer} connected={connected} />;
return <SignalsTab botState={botState} />;
case 'entries':
if (!isAdmin) return <OverviewTab botState={botState} previewAsCustomer={previewAsCustomer} connected={connected} />;
return <EntriesTab botState={botState} />;
case 'positions': return <PositionsTab botState={botState} />;
case 'history': return <HistoryTab botState={botState} />;
case 'strategy_clusters':
return <TradeProfileManager botState={botState} />;
case 'profiles':
return <MyStrategiesTab botState={botState} alerts={botState.alerts} previewAsCustomer={previewAsCustomer} />;
case 'marketplace':
return <MarketplaceTab onClone={handleClonePreset} botState={botState} />;
case 'membership':
return <MembershipTab />;
case 'wizard':
return <StrategyWizard
editingProfile={wizardSeed}
profile={profile}
previewAsCustomer={previewAsCustomer}
onComplete={() => {
setWizardSeed(null);
setActiveTab('profiles');
}}
/>;
case 'backtest':
if (!showBacktestTab) return <OverviewTab botState={botState} previewAsCustomer={previewAsCustomer} connected={connected} />;
return <BacktestTab previewAsCustomer={previewAsCustomer} />;
case 'settings': return <SettingsTab botState={botState} />;
case 'admin':
if (!isAdmin) return <OverviewTab botState={botState} previewAsCustomer={previewAsCustomer} connected={connected} />;
return <AdminTab botState={botState} />;
default: return <OverviewTab botState={botState} connected={connected} />;
}
};
const handleSignOut = async () => {
tradingTelemetry.client.trackEvent('info', 'auth', 'trading_web_sign_out', {
userId: user?.id ?? 'anonymous',
feature: 'sign_out',
tags: { surface: 'web' },
});
await signOut();
};
return (
<div className="app-container">
{hasCriticalEvents && (
<div
className="system-critical-notice"
style={{
background: 'linear-gradient(90deg, #991b1b 0%, #dc2626 50%, #991b1b 100%)',
color: 'white',
padding: '8px 20px',
textAlign: 'center',
fontSize: '11px',
fontWeight: '900',
textTransform: 'uppercase',
letterSpacing: '0.1em',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '10px',
zIndex: 1000
}}
onClick={() => setActiveTab('admin')}
>
<span style={{ fontSize: '14px' }}></span>
<span>SYSTEM ALERT: {recentCriticalEvents.length} CRITICAL ISSUES DETECTED. CLICK TO REVIEW IN ADMIN PANEL.</span>
<span style={{ fontSize: '14px' }}></span>
</div>
)}
<header className="app-header">
<div className="header-main">
<h1>Trading Bot Dashboard <small style={{ fontSize: '0.6rem', opacity: 0.5 }}>v2.3</small></h1>
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
<div className={`connection-status ${connected ? 'online' : 'offline'}`}>
{connected ? 'Connected' : 'Reconnecting...'}
</div>
<div style={{
display: 'flex',
alignItems: 'center',
gap: '0.5rem',
padding: '0.4rem 0.8rem',
background: botState.settings.isAlgoEnabled ? 'rgba(52,199,89,0.15)' : 'rgba(255,149,0,0.15)',
border: `1px solid ${botState.settings.isAlgoEnabled ? 'rgba(52,199,89,0.4)' : 'rgba(255,149,0,0.4)'}`,
borderRadius: '8px',
boxShadow: botState.settings.isAlgoEnabled ? '0 0 20px rgba(52,199,89,0.1)' : 'none'
}}>
<span className={botState.settings.isAlgoEnabled ? 'pulse-green' : ''} style={{ fontSize: '0.85rem' }}>
{botState.settings.isAlgoEnabled ? '🟢' : '⏸️'}
</span>
<div style={{ display: 'flex', flexDirection: 'column' }}>
<span style={{
fontSize: '0.75rem',
fontWeight: '900',
color: botState.settings.isAlgoEnabled ? '#34c759' : '#ff9500',
letterSpacing: '0.05em',
textTransform: 'uppercase'
}}>
{botState.settings.isAlgoEnabled ? 'Bot Active' : 'Bot Monitoring'}
</span>
<span style={{
fontSize: '10px',
color: '#666',
fontWeight: 'bold'
}}>
Mode: <span style={{ color: '#aaa' }}>{botState.settings.executionMode}</span>
</span>
</div>
</div>
{/* Trading Control Status Badge */}
{botState.health?.tradingControl && (
<div style={{
display: 'flex',
alignItems: 'center',
gap: '0.5rem',
padding: '0.4rem 0.8rem',
background: botState.health.tradingControl.mode === 'PAUSED' ? 'rgba(255,149,0,0.15)' : 'rgba(52,199,89,0.08)',
border: `1px solid ${botState.health.tradingControl.mode === 'PAUSED' ? 'rgba(255,149,0,0.4)' : 'rgba(52,199,89,0.3)'}`,
borderRadius: '8px'
}}
title={botState.health.tradingControl.mode === 'PAUSED'
? `Trading paused by ${botState.health.tradingControl.lastChangedBy}. No new entries will be placed.`
: 'Auto-trading is active'}
>
<span style={{ fontSize: '0.85rem' }}>
{botState.health.tradingControl.mode === 'PAUSED' ? '⏸️' : '▶️'}
</span>
<div style={{ display: 'flex', flexDirection: 'column' }}>
<span style={{
fontSize: '0.75rem',
fontWeight: '900',
color: botState.health.tradingControl.mode === 'PAUSED' ? '#ff9500' : '#34c759',
letterSpacing: '0.05em',
textTransform: 'uppercase'
}}>
{botState.health.tradingControl.mode === 'PAUSED' ? 'Trading Paused' : 'Trading Active'}
</span>
<span style={{
fontSize: '10px',
color: '#666',
fontWeight: 'bold'
}}>
{botState.health.tradingControl.mode === 'PAUSED' ? 'No new entries' : 'Entries allowed'}
</span>
</div>
</div>
)}
{/* Preview as Customer Toggle — Admin Only */}
{isAdminAccount && (
<button
onClick={() => setPreviewAsCustomer(p => !p)}
title={previewAsCustomer ? 'Exit customer preview mode' : 'Preview the app as a regular customer'}
style={{
display: 'flex',
alignItems: 'center',
gap: '6px',
padding: '6px 14px',
background: previewAsCustomer ? 'rgba(251,191,36,0.15)' : 'rgba(255,255,255,0.04)',
border: previewAsCustomer ? '1px solid rgba(251,191,36,0.5)' : '1px solid rgba(255,255,255,0.1)',
borderRadius: '8px',
color: previewAsCustomer ? '#fbbf24' : '#666',
cursor: 'pointer',
fontSize: '0.78rem',
fontWeight: 700,
transition: 'all 0.2s',
letterSpacing: '0.02em'
}}
>
<span style={{ fontSize: '0.9rem' }}>{previewAsCustomer ? '🎭' : '👁️'}</span>
{previewAsCustomer ? 'Exit Preview' : 'Preview as Customer'}
</button>
)}
<div className="system-health-badge" style={{ borderColor: systemHealthColor, color: systemHealthColor }} title={systemHealthTooltip}>
<span className="system-health-dot" style={{ background: systemHealthColor }} />
<div className="system-health-texts">
<span className="system-health-label">System Health</span>
<span className="system-health-value">{systemHealthState}</span>
</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', padding: '0.5rem 1rem', background: 'rgba(255,255,255,0.05)', borderRadius: '8px' }}>
<span style={{ fontSize: '0.9rem', color: '#aaa' }}>👤</span>
<span style={{ fontSize: '0.85rem', color: '#fff' }}>{user?.email}</span>
<button
onClick={handleSignOut}
style={{
marginLeft: '0.5rem',
padding: '0.25rem 0.75rem',
background: 'rgba(255,59,48,0.2)',
border: '1px solid rgba(255,59,48,0.4)',
borderRadius: '4px',
color: '#ff3b30',
cursor: 'pointer',
fontSize: '0.8rem',
transition: 'all 0.2s'
}}
onMouseEnter={(e) => e.currentTarget.style.background = 'rgba(255,59,48,0.3)'}
onMouseLeave={(e) => e.currentTarget.style.background = 'rgba(255,59,48,0.2)'}
>
Logout
</button>
</div>
</div>
</div>
<nav className="tab-navigation">
<button className={activeTab === 'overview' ? 'active' : ''} onClick={() => setActiveTab('overview')}>Overview</button>
{isAdmin && (
<button className={activeTab === 'signals' ? 'active' : ''} onClick={() => setActiveTab('signals')}>📡 Signals</button>
)}
{isAdmin && (
<button className={activeTab === 'entries' ? 'active' : ''} onClick={() => setActiveTab('entries')}>📋 Entries</button>
)}
<button className={activeTab === 'positions' ? 'active' : ''} onClick={() => setActiveTab('positions')}>Positions & Orders</button>
<button className={activeTab === 'history' ? 'active' : ''} onClick={() => setActiveTab('history')}>Trade History</button>
<button className={activeTab === 'profiles' ? 'active' : ''} onClick={() => setActiveTab('profiles')}>My Strategies</button>
<button className={activeTab === 'marketplace' ? 'active' : ''} onClick={() => setActiveTab('marketplace')}> Marketplace</button>
<button className={activeTab === 'membership' ? 'active' : ''} onClick={() => setActiveTab('membership')}>💎 Plans</button>
<button className={activeTab === 'wizard' ? 'active' : ''} onClick={() => setActiveTab('wizard')}>🛠 Build Strategy</button>
{showBacktestTab && (
<button className={activeTab === 'backtest' ? 'active' : ''} onClick={() => setActiveTab('backtest')}>📈 Backtesting</button>
)}
{isAdmin && (
<button className={activeTab === 'strategy_clusters' ? 'active' : ''} onClick={() => setActiveTab('strategy_clusters')}>🛡 Strategy Clusters</button>
)}
<button className={activeTab === 'settings' ? 'active' : ''} onClick={() => setActiveTab('settings')}>Settings</button>
{isAdmin && (
<button className={activeTab === 'admin' ? 'active' : ''} onClick={() => setActiveTab('admin')}> Admin Panel</button>
)}
</nav>
</header>
<main className="app-content">
<LivePulseTicker botState={botState} />
<div className="tab-content-full">
{renderTab()}
</div>
</main>
{/* Global AI Strategy Assistant - floating robot icon on all pages */}
<ChatControl profiles={chatProfiles} onApplyProfile={handleChatApply} />
</div>
);
}
export default App;