import { Suspense, lazy, useState, useEffect, useCallback, useRef } from 'react'; import { BrowserRouter } from 'react-router-dom'; import { useWebSocket } from './hooks/useWebSocket'; import { useAuth } from './components/AuthContext'; import { Login } from './components/Login'; import { ResetPassword } from './components/ResetPassword'; import { AppContext } from './context/AppContext'; import { AppShell } from './components/layout/AppShell'; import { useBacktestFeatureGate } from './backtest/useBacktestFeatureGate'; import { useTabFeatureFlags } from './hooks/useTabFeatureFlags'; import { tradingRuntime, tradingTelemetry } from './lib/runtime'; import { createTradeProfile, fetchTradeProfiles, updateTradeProfile } from './lib/profileApi'; const ChatControl = lazy(() => import('./components/ChatControl').then((mod) => ({ default: mod.ChatControl }))); // ─── Helpers (preserved from original App.tsx) ─────────────────────────────── 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, }); // ─── App ───────────────────────────────────────────────────────────────────── function App() { const { user, profile, loading, signOut } = useAuth(); const { socket, botState, connected } = useWebSocket(tradingRuntime.tradingApiUrl); const [activeSymbol, setActiveSymbol] = useState(''); const [chatProfiles, setChatProfiles] = useState([]); const [previewAsCustomer] = useState(false); const [simpleToasts, setSimpleToasts] = useState>([]); const seenSimpleEventIdsRef = useRef>(new Set()); const { enabled: backtestEnabledForView, loading: backtestGateLoading } = useBacktestFeatureGate({ previewAsCustomer }); const { flags: tabFlags } = useTabFeatureFlags(); // Feature gates const isAdminAccount = profile?.role === 'admin'; const isAdmin = isAdminAccount && !previewAsCustomer; const showBacktestTab = isAdmin || (!backtestGateLoading && backtestEnabledForView); const showMarketplaceTab = isAdmin || tabFlags.marketplace; // Critical system events (for the alert banner) const recentCriticalEvents = (botState.operationalEvents ?? []).filter((e): e is NonNullable => Boolean(e) && (e.severity === 'ERROR' || e.severity === 'WARN') && Date.now() - e.timestamp < 600_000 ); const hasCriticalEvents = recentCriticalEvents.length > 0; useEffect(() => { const events = botState.operationalEvents ?? []; const now = Date.now(); const nextToasts: Array<{ id: string; message: string; severity: 'INFO' | 'WARN' | 'ERROR' }> = []; for (const event of events) { if (!event || event.type !== 'SIMPLE_SETUP_UPDATE') continue; if (seenSimpleEventIdsRef.current.has(event.id)) continue; seenSimpleEventIdsRef.current.add(event.id); if (now - event.timestamp > 120_000) continue; nextToasts.push({ id: event.id, message: event.message, severity: (event.severity as 'INFO' | 'WARN' | 'ERROR') || 'INFO', }); } if (nextToasts.length === 0) return; setSimpleToasts((prev) => [...prev, ...nextToasts].slice(-4)); for (const toast of nextToasts) { window.setTimeout(() => { setSimpleToasts((prev) => prev.filter((entry) => entry.id !== toast.id)); }, 4500); } }, [botState.operationalEvents]); // Chat profile management const fetchChatProfiles = useCallback(async () => { const data = await fetchTradeProfiles(); setChatProfiles(data ?? []); }, []); useEffect(() => { if (user) { fetchChatProfiles(); const id = setInterval(fetchChatProfiles, 30_000); return () => clearInterval(id); } }, [user, fetchChatProfiles]); const handleChatApply = async ( action: string, profileData: any ): Promise<{ success: boolean; error?: string }> => { const currentUserId = user?.id; if (!currentUserId) return { success: false, error: 'Not authenticated' }; const profileName = resolveProfileNameForAction(action, profileData.name, chatProfiles); const payload = buildChatApplyPayload(profileData, currentUserId, profileName); if (action === 'create_profile') { try { await createTradeProfile(payload); fetchChatProfiles(); return { success: true }; } catch (err: any) { return { success: false, error: err.message }; } } if (action === 'update_profile' && profileData.id) { try { await updateTradeProfile(profileData.id, payload); fetchChatProfiles(); return { success: true }; } catch (err: any) { return { success: false, error: err.message }; } } return { success: false, error: 'Unknown action' }; }; const handleSignOut = async () => { tradingTelemetry.client.trackEvent('info', 'auth', 'trading_web_sign_out', { userId: user?.id ?? 'anonymous', feature: 'sign_out', tags: { surface: 'web' }, }); await signOut(); }; // ── Auth gates ────────────────────────────────────────────────────────────── if (window.location.pathname === '/reset-callback') return ; if (loading) { return (
Loading…
); } if (!user) return ; // ── Render ────────────────────────────────────────────────────────────────── return ( {simpleToasts.length > 0 && (
{simpleToasts.map((toast) => { const palette = toast.severity === 'ERROR' ? { border: 'var(--bl-danger-border, var(--bl-danger))', bg: 'var(--bl-danger-muted)', fg: 'var(--foreground)' } : toast.severity === 'WARN' ? { border: 'var(--bl-warning-border, var(--bl-warning))', bg: 'var(--bl-warning-muted)', fg: 'var(--foreground)' } : { border: 'var(--bl-success-border, var(--bl-success))', bg: 'var(--bl-success-muted)', fg: 'var(--foreground)' }; return (
Simple update
{toast.message}
); })}
)} {/* Floating AI strategy assistant */}
); } export default App;