196 lines
7.1 KiB
TypeScript
196 lines
7.1 KiB
TypeScript
import { useState, useEffect, useCallback } 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 { ChatControl } from './components/ChatControl';
|
||
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';
|
||
|
||
// ─── 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<any[]>([]);
|
||
const [previewAsCustomer] = useState(false);
|
||
|
||
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.severity === 'ERROR' || e.severity === 'WARN') &&
|
||
Date.now() - e.timestamp < 600_000
|
||
);
|
||
const hasCriticalEvents = recentCriticalEvents.length > 0;
|
||
|
||
// 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 <ResetPassword />;
|
||
|
||
if (loading) {
|
||
return (
|
||
<div style={{
|
||
display: 'flex', justifyContent: 'center', alignItems: 'center',
|
||
height: '100vh', background: '#F3F4F6', color: '#374151',
|
||
fontSize: 15, fontFamily: 'Inter, system-ui, sans-serif',
|
||
}}>
|
||
Loading…
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (!user) return <Login />;
|
||
|
||
// ── Render ──────────────────────────────────────────────────────────────────
|
||
return (
|
||
<BrowserRouter>
|
||
<AppContext.Provider value={{
|
||
botState,
|
||
socket,
|
||
connected,
|
||
activeSymbol,
|
||
setActiveSymbol,
|
||
isAdmin,
|
||
user,
|
||
profile,
|
||
showBacktestTab,
|
||
showMarketplaceTab,
|
||
handleSignOut,
|
||
}}>
|
||
{/* Critical system alert banner */}
|
||
{hasCriticalEvents && (
|
||
<div style={{
|
||
position: 'fixed', top: 0, left: 0, right: 0, zIndex: 9999,
|
||
background: 'linear-gradient(90deg, #991b1b 0%, #dc2626 50%, #991b1b 100%)',
|
||
color: '#fff',
|
||
padding: '6px 20px',
|
||
textAlign: 'center',
|
||
fontSize: 11,
|
||
fontWeight: 900,
|
||
textTransform: 'uppercase',
|
||
letterSpacing: '0.1em',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
gap: 10,
|
||
}}>
|
||
<span>⚠️</span>
|
||
<span>
|
||
SYSTEM ALERT: {recentCriticalEvents.length} CRITICAL ISSUES DETECTED — GO TO SETTINGS › ADMIN PANEL
|
||
</span>
|
||
<span>⚠️</span>
|
||
</div>
|
||
)}
|
||
|
||
<div style={{ paddingTop: hasCriticalEvents ? 32 : 0 }}>
|
||
<AppShell />
|
||
</div>
|
||
|
||
{/* Floating AI strategy assistant */}
|
||
<ChatControl profiles={chatProfiles} onApplyProfile={handleChatApply} />
|
||
</AppContext.Provider>
|
||
</BrowserRouter>
|
||
);
|
||
}
|
||
|
||
export default App;
|