Files
goldenChart/frontend/src/App.tsx
T
2026-06-08 16:24:31 +09:00

649 lines
26 KiB
TypeScript

import React, { useState, useEffect, useCallback, lazy, Suspense } from 'react';
import { useIndicatorSettings } from './hooks/useIndicatorSettings';
import { getAppSettingsCache, useAppSettings } from './hooks/useAppSettings';
import { clampVirtualTargetMax } from './utils/virtualTargetLimits';
import { resolveTrendSearchAppSettings } from './utils/trendSearchAppSettings';
import TopMenuBar, { type MenuPage } from './components/TopMenuBar';
import { TradeNotificationProvider } from './contexts/TradeNotificationContext';
import { VerificationIssueNotificationProvider } from './contexts/VerificationIssueNotificationContext';
import { FormalLoginGate } from './components/FormalLoginGate';
import {
isFormalLogin,
isTradingMenuPage,
FORMAL_LOGIN_REQUIRED_MSG,
} from './utils/tradingAccess';
import { NotifyTopMenuBar } from './components/NotifyUiBindings';
import { AppNotificationLayer } from './components/AppNotificationLayer';
import type { Theme } from './types';
import type { ChartRealtimeSource } from './hooks/useChartRealtimeData';
import { useIsMobile } from './hooks/useMediaQuery';
import LoginModal from './components/LoginModal';
import AppDownloadModal from './components/AppDownloadModal';
import SplashScreen from './components/SplashScreen';
import { getAuthSession, setAuthSession, clearAuthSession, type AuthSession } from './utils/auth';
import { isAppEntered, setAppEntered, clearAppEntered } from './utils/appEntry';
import { syncDocumentTheme } from './utils/documentTheme';
import {
loadLocalTheme,
migrateDbThemeToLocalIfNeeded,
saveLocalTheme,
} from './utils/localThemeStorage';
import { useMenuPermissions, invalidateMenuPermissionsCache } from './hooks/useMenuPermissions';
import { firstAllowedTopMenu, normalizeRole, type SettingsCategoryId } from './utils/permissions';
import { clearAdminUnlock } from './utils/adminUnlock';
import type { LoginResponse } from './utils/backendApi';
import { invalidateAppSettingsCache } from './hooks/useAppSettings';
import { invalidateIndicatorSettingsCache } from './hooks/useIndicatorSettings';
import { ChartWorkspaceRoot } from './chart/ChartWorkspaceRoot';
import { useChartWorkspaceContext } from './chart/ChartWorkspaceContext';
import './App.css';
import './styles/appPopup.css';
import './styles/paperDashboard.css';
import './styles/tradeRightPanel.css';
import './styles/virtualTradingDashboard.css';
import './styles/backtestDashboard.css';
const DashboardPage = lazy(() => import('./components/DashboardPage'));
const StrategyPage = lazy(() => import('./components/StrategyPage'));
const StrategyEditorPage = lazy(() => import('./components/StrategyEditorPage'));
const TradeNotificationListPage = lazy(() => import('./components/TradeNotificationListPage'));
const BacktestHistoryPage = lazy(() => import('./components/BacktestHistoryPage').then(m => ({ default: m.BacktestHistoryPage })));
const AnalysisReportPage = lazy(() => import('./components/analysisReport/AnalysisReportPage').then(m => ({ default: m.AnalysisReportPage })));
const SettingsPage = lazy(() => import('./components/SettingsPage'));
const PaperTradingPage = lazy(() => import('./components/PaperTradingPage'));
const VirtualTradingPage = lazy(() => import('./components/VirtualTradingPage'));
const TrendSearchPage = lazy(() => import('./components/TrendSearchPage'));
const VerificationBoardPage = lazy(() => import('./components/VerificationBoardPage'));
function PageLoadFallback() {
return <div className="page-load-fallback" role="status">화면 로딩 중…</div>;
}
function AppMainContent({
theme,
onThemeToggle,
sessionKey,
formalLogin,
requireFormalLogin,
authUser,
guestMode,
loginOpen,
setLoginOpen,
appDownloadOpen,
setAppDownloadOpen,
menuPage,
setMenuPage,
guardedSetMenuPage,
settingsInitialCategory,
setSettingsInitialCategory,
verificationFocusIssueId,
setVerificationFocusIssueId,
menuPermissions,
canMenu,
appDefaults,
appSettingsLoaded,
saveAppDef,
getIndicatorParams,
saveIndicatorParams,
getIndicatorVisual,
saveIndicatorVisual,
saveAllIndicatorDefaults,
indicatorSettingsRevision,
handleLoginSuccess,
handleLogout,
}: {
theme: Theme;
onThemeToggle: () => void;
sessionKey: number;
formalLogin: boolean;
requireFormalLogin: () => void;
authUser: AuthSession | null;
guestMode: boolean;
loginOpen: boolean;
setLoginOpen: (v: boolean) => void;
appDownloadOpen: boolean;
setAppDownloadOpen: (v: boolean) => void;
menuPage: MenuPage;
setMenuPage: (p: MenuPage) => void;
guardedSetMenuPage: (p: MenuPage) => void;
settingsInitialCategory: SettingsCategoryId | undefined;
setSettingsInitialCategory: (c: SettingsCategoryId | undefined) => void;
verificationFocusIssueId: number | null;
setVerificationFocusIssueId: (id: number | null) => void;
menuPermissions: ReturnType<typeof useMenuPermissions>['permissions'];
canMenu: ReturnType<typeof useMenuPermissions>['can'];
appDefaults: ReturnType<typeof useAppSettings>['defaults'];
appSettingsLoaded: boolean;
saveAppDef: ReturnType<typeof useAppSettings>['save'];
getIndicatorParams: ReturnType<typeof useIndicatorSettings>['getParams'];
saveIndicatorParams: ReturnType<typeof useIndicatorSettings>['saveParams'];
getIndicatorVisual: ReturnType<typeof useIndicatorSettings>['getVisualConfig'];
saveIndicatorVisual: ReturnType<typeof useIndicatorSettings>['saveVisual'];
saveAllIndicatorDefaults: ReturnType<typeof useIndicatorSettings>['saveAllIndicatorDefaults'];
indicatorSettingsRevision: number;
handleLoginSuccess: (res: LoginResponse) => void;
handleLogout: () => void;
}) {
const isMobile = useIsMobile();
const {
symbol,
indicators,
goToMarketChart,
refreshPaperAccount,
handlePaperOrderFilled,
paperRefreshKey,
mode,
marketTickers,
settingsPageProps,
chartScreen,
} = useChartWorkspaceContext();
const paperTradingEnabled = appDefaults.paperTradingEnabled ?? true;
const paperAutoTradeEnabled = appDefaults.paperAutoTradeEnabled ?? false;
const handleFullscreen = () => {
if (!document.fullscreenElement) document.documentElement.requestFullscreen().catch(() => {});
else document.exitFullscreen().catch(() => {});
};
return (
<VerificationIssueNotificationProvider
enabled={canMenu('verification-board')}
notifyEnabled={appDefaults.verificationIssueNotify}
>
<div className={`app ${theme} mode-${mode}${isMobile ? ' app--mobile' : ''}`}>
<NotifyTopMenuBar
activePage={menuPage}
theme={theme}
onPage={guardedSetMenuPage}
onTheme={onThemeToggle}
onOpenAppDownload={() => setAppDownloadOpen(true)}
authUser={authUser}
guestMode={guestMode}
menuPermissions={menuPermissions}
onLoginClick={() => setLoginOpen(true)}
onLogout={handleLogout}
tradeAlertPopupPosition={appDefaults.tradeAlertPopupPosition}
tradeAlertPopupLayout={appDefaults.tradeAlertPopupLayout}
tradeAlertPopupGridCols={appDefaults.tradeAlertPopupGridCols}
onTradeAlertPopupPosition={v => saveAppDef({ tradeAlertPopupPosition: v })}
onTradeAlertPopupLayout={v => saveAppDef({ tradeAlertPopupLayout: v })}
onTradeAlertPopupGridCols={v => saveAppDef({ tradeAlertPopupGridCols: v })}
onFullscreen={handleFullscreen}
/>
<LoginModal
open={loginOpen}
onClose={() => setLoginOpen(false)}
onSuccess={handleLoginSuccess}
/>
{appDownloadOpen && (
<AppDownloadModal onClose={() => setAppDownloadOpen(false)} />
)}
{menuPage === 'dashboard' && (
<Suspense fallback={<PageLoadFallback />}>
<DashboardPage theme={theme} onGoChart={m => { goToMarketChart(m); setMenuPage('chart'); }} />
</Suspense>
)}
{menuPage === 'strategy' && (
formalLogin
? (
<Suspense fallback={<PageLoadFallback />}>
<StrategyPage theme={theme} activeIndicators={indicators} />
</Suspense>
)
: <FormalLoginGate onLogin={requireFormalLogin} />
)}
{menuPage === 'strategy-editor' && (
formalLogin
? (
<Suspense fallback={<PageLoadFallback />}>
<StrategyEditorPage theme={theme} />
</Suspense>
)
: <FormalLoginGate onLogin={requireFormalLogin} />
)}
{menuPage === 'backtest' && (
formalLogin
? (
<Suspense fallback={<PageLoadFallback />}>
<BacktestHistoryPage theme={theme} />
</Suspense>
)
: <FormalLoginGate onLogin={requireFormalLogin} />
)}
{menuPage === 'analysis-report' && (
formalLogin
? (
<Suspense fallback={<PageLoadFallback />}>
<AnalysisReportPage theme={theme} />
</Suspense>
)
: <FormalLoginGate onLogin={requireFormalLogin} />
)}
{menuPage === 'paper' && (
formalLogin ? (
<Suspense fallback={<PageLoadFallback />}>
<PaperTradingPage
theme={theme}
tickers={marketTickers}
refreshKey={paperRefreshKey}
defaultMarket={symbol}
paperTradingEnabled={paperTradingEnabled}
paperAutoTradeEnabled={paperAutoTradeEnabled}
onPaperOrderFilled={handlePaperOrderFilled}
onOpenSettings={() => {
if (!formalLogin) { requireFormalLogin(); return; }
setSettingsInitialCategory('trading');
setMenuPage('settings');
}}
onGoChart={m => {
goToMarketChart(m);
setMenuPage('chart');
}}
/>
</Suspense>
) : (
<FormalLoginGate onLogin={requireFormalLogin} title="투자관리" />
)
)}
{menuPage === 'virtual' && (
formalLogin ? (
<Suspense fallback={<PageLoadFallback />}>
<VirtualTradingPage
theme={theme}
tickers={marketTickers}
defaultMarket={symbol}
paperTradingEnabled={paperTradingEnabled}
paperAutoTradeEnabled={paperAutoTradeEnabled}
paperAutoTradeBudgetPct={appDefaults.paperAutoTradeBudgetPct}
onPaperAutoTradeEnabled={v => saveAppDef({ paperAutoTradeEnabled: v })}
onPaperOrderFilled={handlePaperOrderFilled}
/>
</Suspense>
) : (
<FormalLoginGate onLogin={requireFormalLogin} title="가상매매" />
)
)}
{menuPage === 'trend-search' && (
<Suspense fallback={<PageLoadFallback />}>
<TrendSearchPage
theme={theme}
chartRealtimeSource={(appDefaults.chartRealtimeSource ?? 'BACKEND_STOMP') as ChartRealtimeSource}
chartCandleAreaPriceLabels={appDefaults.chartCandleAreaPriceLabels}
chartSeriesPriceLabels={appDefaults.chartSeriesPriceLabels}
tickers={marketTickers}
/>
</Suspense>
)}
{menuPage === 'verification-board' && (
<Suspense fallback={<PageLoadFallback />}>
<VerificationBoardPage
theme={theme}
focusIssueId={verificationFocusIssueId}
onFocusIssueConsumed={() => setVerificationFocusIssueId(null)}
/>
</Suspense>
)}
{menuPage === 'notifications' && canMenu('notifications') && (
formalLogin ? (
<Suspense fallback={<PageLoadFallback />}>
<TradeNotificationListPage
theme={theme}
tickers={marketTickers}
defaultMarket={symbol}
paperTradingEnabled={paperTradingEnabled}
paperAutoTradeEnabled={paperAutoTradeEnabled}
onPaperOrderFilled={handlePaperOrderFilled}
onGoToChart={market => {
goToMarketChart(market);
setMenuPage('chart');
}}
/>
</Suspense>
) : (
<FormalLoginGate onLogin={requireFormalLogin} title="알림목록" />
)
)}
{menuPage === 'settings' && canMenu('settings') && (
<Suspense fallback={<PageLoadFallback />}>
<SettingsPage
isFormalLogin={formalLogin}
onRequireFormalLogin={requireFormalLogin}
theme={theme}
menuPermissions={menuPermissions}
btAutoPopup={appDefaults.btAutoPopup}
onBtAutoPopup={v => saveAppDef({ btAutoPopup: v })}
btShowPrice={appDefaults.btShowPrice}
onBtShowPrice={v => saveAppDef({ btShowPrice: v })}
liveMarket={symbol}
tradeAlertPopup={appDefaults.tradeAlertPopup}
onTradeAlertPopup={v => saveAppDef({ tradeAlertPopup: v })}
tradeAlertSoundEnabled={appDefaults.tradeAlertSoundEnabled}
onTradeAlertSoundEnabled={v => saveAppDef({ tradeAlertSoundEnabled: v })}
tradeAlertSound={appDefaults.tradeAlertSound}
onTradeAlertSound={v => saveAppDef({ tradeAlertSound: v })}
tradeAlertPopupPosition={appDefaults.tradeAlertPopupPosition}
onTradeAlertPopupPosition={v => saveAppDef({ tradeAlertPopupPosition: v })}
tradeAlertPopupLayout={appDefaults.tradeAlertPopupLayout}
onTradeAlertPopupLayout={v => saveAppDef({ tradeAlertPopupLayout: v })}
tradeAlertPopupGridCols={appDefaults.tradeAlertPopupGridCols}
onTradeAlertPopupGridCols={v => saveAppDef({ tradeAlertPopupGridCols: v })}
getIndicatorParams={getIndicatorParams}
saveIndicatorParams={saveIndicatorParams}
getIndicatorVisual={getIndicatorVisual}
saveIndicatorVisual={saveIndicatorVisual}
saveAllIndicatorDefaults={saveAllIndicatorDefaults}
indicatorSettingsRevision={indicatorSettingsRevision}
paperTradingEnabled={paperTradingEnabled}
onPaperTradingEnabled={v => saveAppDef({ paperTradingEnabled: v })}
paperInitialCapital={appDefaults.paperInitialCapital}
onPaperInitialCapital={v => saveAppDef({ paperInitialCapital: v })}
paperFeeRatePct={appDefaults.paperFeeRatePct}
onPaperFeeRatePct={v => saveAppDef({ paperFeeRatePct: v })}
paperSlippagePct={appDefaults.paperSlippagePct}
onPaperSlippagePct={v => saveAppDef({ paperSlippagePct: v })}
paperMinOrderKrw={appDefaults.paperMinOrderKrw}
onPaperMinOrderKrw={v => saveAppDef({ paperMinOrderKrw: v })}
paperAutoTradeEnabled={appDefaults.paperAutoTradeEnabled}
onPaperAutoTradeEnabled={v => saveAppDef({ paperAutoTradeEnabled: v })}
paperAutoTradeBudgetPct={appDefaults.paperAutoTradeBudgetPct}
onPaperAutoTradeBudgetPct={v => saveAppDef({ paperAutoTradeBudgetPct: v })}
paperOrderFillMode={appDefaults.paperOrderFillMode}
onPaperOrderFillMode={v => saveAppDef({ paperOrderFillMode: v })}
paperOrderAutoCancelPct={appDefaults.paperOrderAutoCancelPct}
onPaperOrderAutoCancelPct={v => saveAppDef({ paperOrderAutoCancelPct: v })}
virtualTargetMaxCount={appDefaults.virtualTargetMaxCount}
onVirtualTargetMaxCount={v => saveAppDef({ virtualTargetMaxCount: clampVirtualTargetMax(v) })}
trendSearchSettings={appDefaults.trendSearchSettings}
onTrendSearchSettingsChange={s => saveAppDef({
trendSearchSettings: resolveTrendSearchAppSettings(s),
})}
tradingMode={appDefaults.tradingMode}
onTradingMode={v => saveAppDef({ tradingMode: v })}
liveAutoTradeEnabled={appDefaults.liveAutoTradeEnabled}
onLiveAutoTradeEnabled={v => saveAppDef({ liveAutoTradeEnabled: v })}
hasUpbitKeys={appDefaults.hasUpbitKeys}
upbitAccessKeyMasked={appDefaults.upbitAccessKeyMasked}
onUpbitKeys={(access, secret) => {
const patch: Parameters<typeof saveAppDef>[0] = {};
const a = access.trim();
const s = secret.trim();
const isMasked = (v: string) => v.startsWith('····') || v.startsWith('****');
if (a && !isMasked(a)) patch.upbitAccessKey = a;
if (s && !isMasked(s)) patch.upbitSecretKey = s;
if (Object.keys(patch).length) saveAppDef(patch);
}}
chartRealtimeSource={appDefaults.chartRealtimeSource}
onChartRealtimeSource={v => saveAppDef({ chartRealtimeSource: v })}
liveAutoTradeBudgetPct={appDefaults.liveAutoTradeBudgetPct}
onLiveAutoTradeBudgetPct={v => saveAppDef({ liveAutoTradeBudgetPct: v })}
fcmPushEnabled={appDefaults.fcmPushEnabled}
onFcmPushEnabled={v => saveAppDef({ fcmPushEnabled: v })}
verificationIssueNotify={appDefaults.verificationIssueNotify}
onVerificationIssueNotify={v => saveAppDef({ verificationIssueNotify: v })}
onFcmTest={async () => {
const { sendFcmTest } = await import('./utils/backendApi');
const r = await sendFcmTest();
window.alert(r?.available ? 'FCM 테스트 요청을 보냈습니다.' : 'FCM 서버가 비활성 상태입니다.');
}}
initialCategory={settingsInitialCategory}
{...settingsPageProps}
/>
</Suspense>
)}
{chartScreen}
<AppNotificationLayer
menuPage={menuPage}
onPage={guardedSetMenuPage}
onGoToChart={goToMarketChart}
onGoToVerificationIssue={issueId => {
setVerificationFocusIssueId(issueId);
guardedSetMenuPage('verification-board');
}}
tradingMode={appDefaults.tradingMode}
hasUpbitKeys={appDefaults.hasUpbitKeys}
paperTradingEnabled={paperTradingEnabled}
liveAutoTradeBudgetPct={appDefaults.liveAutoTradeBudgetPct}
paperAutoTradeBudgetPct={appDefaults.paperAutoTradeBudgetPct}
tradeAlertPopupPosition={appDefaults.tradeAlertPopupPosition}
tradeAlertPopupLayout={appDefaults.tradeAlertPopupLayout}
tradeAlertPopupGridCols={appDefaults.tradeAlertPopupGridCols}
onOrderDone={() => {
refreshPaperAccount(symbol);
handlePaperOrderFilled();
}}
/>
</div>
</VerificationIssueNotificationProvider>
);
}
function App() {
const [sessionKey, setSessionKey] = useState(0);
const [appEntered, setAppEnteredState] = useState(() => isAppEntered());
const [authUser, setAuthUser] = useState<AuthSession | null>(() =>
isAppEntered() ? getAuthSession() : null,
);
const [guestMode, setGuestMode] = useState(() =>
isAppEntered() && !getAuthSession(),
);
const formalLogin = isFormalLogin(authUser, guestMode);
const [loginOpen, setLoginOpen] = useState(false);
const requireFormalLogin = useCallback(() => {
window.alert(FORMAL_LOGIN_REQUIRED_MSG);
setLoginOpen(true);
}, []);
const [appDownloadOpen, setAppDownloadOpen] = useState(false);
const {
getParams: getIndicatorParams,
saveParams: saveIndicatorParams,
getVisualConfig: getIndicatorVisual,
saveVisual: saveIndicatorVisual,
saveAllIndicatorDefaults,
settingsRevision: indicatorSettingsRevision,
} = useIndicatorSettings(sessionKey);
const getParams = getIndicatorParams;
const saveParams = saveIndicatorParams;
const getVisualConfig = getIndicatorVisual;
const saveVisual = saveIndicatorVisual;
const { defaults: appDefaults, isLoaded: appSettingsLoaded, save: saveAppDef } = useAppSettings(sessionKey);
const { permissions: menuPermissions, isLoaded: menuPermsLoaded, can: canMenu } = useMenuPermissions(sessionKey);
const [theme, setTheme] = useState<Theme>(loadLocalTheme);
const [menuPage, setMenuPage] = useState<MenuPage>('chart');
const [settingsInitialCategory, setSettingsInitialCategory] = useState<SettingsCategoryId | undefined>();
const [verificationFocusIssueId, setVerificationFocusIssueId] = useState<number | null>(null);
useEffect(() => {
if (menuPage !== 'settings') setSettingsInitialCategory(undefined);
}, [menuPage]);
const enterMainApp = useCallback(() => {
setAppEntered();
setAppEnteredState(true);
}, []);
const handleLoginSuccess = useCallback((res: LoginResponse) => {
const session: AuthSession = {
userId: res.userId,
username: res.username,
displayName: res.displayName,
role: normalizeRole(res.role),
};
setAuthSession(session);
setAuthUser(session);
setGuestMode(false);
clearAdminUnlock();
invalidateAppSettingsCache();
invalidateIndicatorSettingsCache();
invalidateMenuPermissionsCache();
setSessionKey(k => k + 1);
enterMainApp();
}, [enterMainApp]);
const handleGuestEnter = useCallback(() => {
clearAuthSession();
setAuthUser(null);
setGuestMode(true);
clearAdminUnlock();
invalidateAppSettingsCache();
invalidateIndicatorSettingsCache();
invalidateMenuPermissionsCache();
setSessionKey(k => k + 1);
enterMainApp();
}, [enterMainApp]);
const handleLogout = useCallback(() => {
clearAuthSession();
setAuthUser(null);
setGuestMode(false);
clearAdminUnlock();
invalidateAppSettingsCache();
invalidateIndicatorSettingsCache();
invalidateMenuPermissionsCache();
setSessionKey(k => k + 1);
clearAppEntered();
setAppEnteredState(false);
setLoginOpen(false);
}, []);
const guardedSetMenuPage = useCallback((page: MenuPage) => {
if (!formalLogin && isTradingMenuPage(page)) {
requireFormalLogin();
return;
}
if (page === 'notifications' && !canMenu('notifications')) return;
if (page !== 'notifications' && !canMenu(page)) {
setMenuPage(firstAllowedTopMenu(menuPermissions));
return;
}
setMenuPage(page);
}, [canMenu, menuPermissions, formalLogin, requireFormalLogin]);
useEffect(() => {
if (!menuPermsLoaded) return;
if (menuPage === 'notifications') {
if (!canMenu('notifications')) setMenuPage(firstAllowedTopMenu(menuPermissions));
return;
}
if (!canMenu(menuPage)) {
setMenuPage(firstAllowedTopMenu(menuPermissions));
}
}, [menuPage, menuPermsLoaded, menuPermissions, canMenu]);
const handleThemeToggle = useCallback(() => {
setTheme(t => {
const next: Theme = t === 'dark' ? 'blue' : t === 'blue' ? 'light' : 'dark';
saveLocalTheme(next);
return next;
});
}, []);
useEffect(() => {
syncDocumentTheme(theme);
}, [theme]);
useEffect(() => {
if (!appSettingsLoaded) return;
setTheme(migrateDbThemeToLocalIfNeeded(getAppSettingsCache().defaultTheme));
}, [appSettingsLoaded]);
const openNotificationsPage = useCallback(
() => guardedSetMenuPage('notifications'),
[guardedSetMenuPage],
);
if (!appEntered) {
return (
<SplashScreen
onLoginSuccess={handleLoginSuccess}
onGuest={handleGuestEnter}
/>
);
}
return (
<TradeNotificationProvider
popupEnabled={appDefaults.tradeAlertPopup ?? true}
soundEnabled={appDefaults.tradeAlertSoundEnabled ?? true}
soundId={appDefaults.tradeAlertSound ?? 'bell'}
settingsSessionKey={authUser?.userId ?? 0}
>
<ChartWorkspaceRoot
visible={menuPage === 'chart'}
menuPage={menuPage}
sessionKey={sessionKey}
formalLogin={formalLogin}
requireFormalLogin={requireFormalLogin}
theme={theme}
onThemeToggle={handleThemeToggle}
appDefaults={appDefaults as unknown as import('./utils/backendApi').AppSettingsDto}
appSettingsLoaded={appSettingsLoaded}
saveAppDef={saveAppDef}
getParams={getParams}
saveParams={saveParams}
getVisualConfig={getVisualConfig}
saveVisual={saveVisual}
saveAllIndicatorDefaults={saveAllIndicatorDefaults}
indicatorSettingsRevision={indicatorSettingsRevision}
onOpenNotificationsPage={openNotificationsPage}
onOpenAppDownload={() => setAppDownloadOpen(true)}
>
<AppMainContent
theme={theme}
onThemeToggle={handleThemeToggle}
sessionKey={sessionKey}
formalLogin={formalLogin}
requireFormalLogin={requireFormalLogin}
authUser={authUser}
guestMode={guestMode}
loginOpen={loginOpen}
setLoginOpen={setLoginOpen}
appDownloadOpen={appDownloadOpen}
setAppDownloadOpen={setAppDownloadOpen}
menuPage={menuPage}
setMenuPage={setMenuPage}
guardedSetMenuPage={guardedSetMenuPage}
settingsInitialCategory={settingsInitialCategory}
setSettingsInitialCategory={setSettingsInitialCategory}
verificationFocusIssueId={verificationFocusIssueId}
setVerificationFocusIssueId={setVerificationFocusIssueId}
menuPermissions={menuPermissions}
canMenu={canMenu}
appDefaults={appDefaults}
appSettingsLoaded={appSettingsLoaded}
saveAppDef={saveAppDef}
getIndicatorParams={getIndicatorParams}
saveIndicatorParams={saveIndicatorParams}
getIndicatorVisual={getIndicatorVisual}
saveIndicatorVisual={saveIndicatorVisual}
saveAllIndicatorDefaults={saveAllIndicatorDefaults}
indicatorSettingsRevision={indicatorSettingsRevision}
handleLoginSuccess={handleLoginSuccess}
handleLogout={handleLogout}
/>
</ChartWorkspaceRoot>
</TradeNotificationProvider>
);
}
export default App;