|
153 | 153 | nasdaq_sp500_smart_dca: { defaultMode: "fixed", defaultBaseInvestmentUsd: "1000" }, |
154 | 154 | ibit_smart_dca: { defaultMode: "fixed", defaultBaseInvestmentUsd: "1000" }, |
155 | 155 | }; |
| 156 | + const APP_BOOT_TIMEOUT_MS = 15000; |
156 | 157 | const platformMinReservedCashVariables = { |
157 | 158 | longbridge: "LONGBRIDGE_MIN_RESERVED_CASH_USD", |
158 | 159 | ibkr: "IBKR_MIN_RESERVED_CASH_USD", |
|
286 | 287 | bootStrategy: "正在读取策略目录。", |
287 | 288 | bootSession: "正在验证登录状态。", |
288 | 289 | bootConfig: "正在读取账号配置和当前状态。", |
| 290 | + bootTimeout: "加载超时,已切换到公开预览(登录后可重试)。", |
289 | 291 | bootPublic: "公开预览已就绪。", |
290 | 292 | login: "登录", |
291 | 293 | logout: "退出", |
|
446 | 448 | bootStrategy: "Reading strategy catalog.", |
447 | 449 | bootSession: "Checking sign-in status.", |
448 | 450 | bootConfig: "Reading account config and current state.", |
| 451 | + bootTimeout: "Loading timed out; switched to public preview. Retry after signing in.", |
449 | 452 | bootPublic: "Public preview is ready.", |
450 | 453 | login: "Sign in", |
451 | 454 | logout: "Sign out", |
|
664 | 667 | } |
665 | 668 | } |
666 | 669 |
|
| 670 | + async function fetchWithTimeout(url, init = {}, timeoutMs = APP_BOOT_TIMEOUT_MS) { |
| 671 | + const controller = new AbortController(); |
| 672 | + const timeoutId = window.setTimeout(() => controller.abort(), timeoutMs); |
| 673 | + try { |
| 674 | + return await fetch(url, { ...init, signal: controller.signal }); |
| 675 | + } catch (error) { |
| 676 | + if (error?.name === "AbortError") { |
| 677 | + throw new Error("request timeout"); |
| 678 | + } |
| 679 | + throw error; |
| 680 | + } finally { |
| 681 | + window.clearTimeout(timeoutId); |
| 682 | + } |
| 683 | + } |
| 684 | + |
| 685 | + async function requestJson(url, init = {}, timeoutMs = APP_BOOT_TIMEOUT_MS) { |
| 686 | + const response = await fetchWithTimeout(url, { ...init, cache: "no-store" }, timeoutMs); |
| 687 | + if (!response.ok) throw new Error("request failed"); |
| 688 | + return response.json(); |
| 689 | + } |
| 690 | + |
| 691 | + function isRequestTimeoutError(error) { |
| 692 | + return String(error?.message || "").toLowerCase() === "request timeout"; |
| 693 | + } |
| 694 | + |
667 | 695 | function optionsFor(platform) { |
668 | 696 | return state.accountOptions[platform] && state.accountOptions[platform].length |
669 | 697 | ? state.accountOptions[platform] |
|
2513 | 2541 | state.bootMessageKey = "bootSession"; |
2514 | 2542 | render(); |
2515 | 2543 | try { |
2516 | | - const response = await fetch("/api/session", { cache: "no-store" }); |
2517 | | - if (!response.ok) throw new Error("no backend"); |
2518 | | - const session = await response.json(); |
| 2544 | + const session = await requestJson("/api/session"); |
2519 | 2545 | state.auth = { |
2520 | 2546 | available: true, |
2521 | 2547 | allowed: Boolean(session.allowed), |
|
2538 | 2564 | state.bootMessageKey = "bootStrategy"; |
2539 | 2565 | render(); |
2540 | 2566 | try { |
2541 | | - const response = await fetch("/api/strategy-profiles", { cache: "no-store" }); |
2542 | | - if (!response.ok) throw new Error("no strategy profiles"); |
2543 | | - const payload = await response.json(); |
| 2567 | + const payload = await requestJson("/api/strategy-profiles"); |
2544 | 2568 | applyStrategyProfiles(payload.strategyProfiles || []); |
2545 | 2569 | if (payload.platformMeta) platformMeta = payload.platformMeta; |
2546 | 2570 | for (const platform of Object.keys(platformMeta)) syncStrategyForAccount(platform); |
|
2557 | 2581 | state.bootMessageKey = "bootConfig"; |
2558 | 2582 | render(); |
2559 | 2583 | try { |
2560 | | - const response = await fetch("/api/config", { cache: "no-store" }); |
2561 | | - if (!response.ok) throw new Error("no config"); |
2562 | | - const payload = await response.json(); |
| 2584 | + const payload = await requestJson("/api/config"); |
2563 | 2585 | if (payload.accountOptions) { |
2564 | 2586 | applyStrategyProfiles(payload.strategyProfiles || defaultStrategyProfiles); |
2565 | 2587 | state.accountOptions = normalizeAccountOptions(payload.accountOptions); |
|
2575 | 2597 | state.configSource = "default"; |
2576 | 2598 | state.currentStrategies = {}; |
2577 | 2599 | } |
2578 | | - } catch { |
| 2600 | + } catch (error) { |
2579 | 2601 | state.configSource = "default"; |
2580 | 2602 | state.currentStrategies = {}; |
| 2603 | + if (isRequestTimeoutError(error)) { |
| 2604 | + state.bootMessageKey = "bootTimeout"; |
| 2605 | + } else { |
| 2606 | + state.bootMessageKey = "bootPublic"; |
| 2607 | + } |
2581 | 2608 | } finally { |
2582 | 2609 | state.appReady = true; |
2583 | 2610 | render(); |
|
2876 | 2903 | boot(); |
2877 | 2904 |
|
2878 | 2905 | async function boot() { |
2879 | | - await refreshStrategyProfiles(); |
2880 | | - await refreshSession(); |
| 2906 | + try { |
| 2907 | + await refreshStrategyProfiles(); |
| 2908 | + await refreshSession(); |
| 2909 | + } catch { |
| 2910 | + state.auth = { available: false, allowed: false, admin: false, login: null }; |
| 2911 | + state.configSource = "default"; |
| 2912 | + state.currentStrategies = {}; |
| 2913 | + state.bootMessageKey = "bootTimeout"; |
| 2914 | + state.appReady = true; |
| 2915 | + render(); |
| 2916 | + } |
2881 | 2917 | } |
0 commit comments