429 lines
20 KiB
TypeScript
429 lines
20 KiB
TypeScript
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;
|