diff --git a/api/strategy.go b/api/strategy.go
index 1985c384..64105fa5 100644
--- a/api/strategy.go
+++ b/api/strategy.go
@@ -516,8 +516,17 @@ func (s *Server) handleStrategyTestRun(c *gin.Context) {
req.PromptVariant = "balanced"
}
+ claw402WalletKey, err := s.resolveStrategyDataWalletKey(userID, req.AIModelID)
+ if err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "error": err.Error(),
+ "ai_response": "",
+ })
+ return
+ }
+
// Create strategy engine to build prompt
- engine := kernel.NewStrategyEngine(&req.Config)
+ engine := kernel.NewStrategyEngine(&req.Config, claw402WalletKey)
// Get candidate coins
candidates, err := engine.GetCandidateCoins()
@@ -697,3 +706,7 @@ func (s *Server) runRealAITest(userID, modelID, systemPrompt, userPrompt string)
return response, nil
}
+
+func (s *Server) resolveStrategyDataWalletKey(userID, selectedModelID string) (string, error) {
+ return s.store.AIModel().ResolveClaw402WalletKey(userID, selectedModelID)
+}
diff --git a/manager/trader_manager.go b/manager/trader_manager.go
index ab4b6583..36b745b8 100644
--- a/manager/trader_manager.go
+++ b/manager/trader_manager.go
@@ -703,6 +703,8 @@ func (tm *TraderManager) addTraderFromStore(traderCfg *store.Trader, aiModelCfg
traderConfig.CustomAPIKey = string(aiModelCfg.APIKey)
}
+ traderConfig.Claw402WalletKey = resolveTraderDataWalletKey(st, traderCfg.UserID, aiModelCfg)
+
// Create trader instance
at, err := trader.NewAutoTrader(traderConfig, st, traderCfg.UserID)
if err != nil {
@@ -741,3 +743,26 @@ func (tm *TraderManager) addTraderFromStore(traderCfg *store.Trader, aiModelCfg
return nil
}
+func resolveTraderDataWalletKey(st *store.Store, userID string, selectedModel *store.AIModel) string {
+ // Fast path: selected model is itself a claw402 model.
+ if selectedModel != nil && selectedModel.Provider == "claw402" {
+ if walletKey := string(selectedModel.APIKey); walletKey != "" {
+ return walletKey
+ }
+ }
+
+ if st == nil {
+ return ""
+ }
+
+ // Fallback: find any configured claw402 model for this user so that paid
+ // NofxAI data sources work even when a non-claw402 model (e.g. deepseek) is
+ // selected as the AI brain.
+ preferredID := ""
+ walletKey, err := st.AIModel().ResolveClaw402WalletKey(userID, preferredID)
+ if err != nil {
+ logger.Warnf("⚠️ Failed to load claw402 wallet for trader data routing: %v", err)
+ return ""
+ }
+ return walletKey
+}
diff --git a/store/ai_model.go b/store/ai_model.go
index e1af9d0f..ef577cc1 100644
--- a/store/ai_model.go
+++ b/store/ai_model.go
@@ -253,6 +253,43 @@ func (s *AIModelStore) Update(userID, id string, enabled bool, apiKey, customAPI
}
// Create creates an AI model
+// ResolveClaw402WalletKey returns the claw402 wallet private key for a user.
+// If preferredModelID is non-empty and points to a claw402 model, its key is returned first.
+// Otherwise the first enabled claw402 model in the user's model list is used.
+// Returns ("", nil) when no claw402 model is configured — callers should treat this as
+// "no paid data routing" rather than an error.
+func (s *AIModelStore) ResolveClaw402WalletKey(userID, preferredModelID string) (string, error) {
+ if preferredModelID != "" {
+ model, err := s.Get(userID, preferredModelID)
+ if err != nil {
+ return "", fmt.Errorf("failed to load selected AI model")
+ }
+ if model.Provider == "claw402" {
+ walletKey := string(model.APIKey)
+ if walletKey == "" {
+ return "", fmt.Errorf("selected claw402 model is missing wallet private key")
+ }
+ return walletKey, nil
+ }
+ }
+
+ models, err := s.List(userID)
+ if err != nil {
+ return "", fmt.Errorf("failed to load AI models")
+ }
+
+ for _, model := range models {
+ if model == nil || model.Provider != "claw402" {
+ continue
+ }
+ if walletKey := string(model.APIKey); walletKey != "" {
+ return walletKey, nil
+ }
+ }
+
+ return "", nil
+}
+
func (s *AIModelStore) Create(userID, id, name, provider string, enabled bool, apiKey, customAPIURL string) error {
model := &AIModel{
ID: id,
diff --git a/trader/auto_trader.go b/trader/auto_trader.go
index 42d6a13f..0a0a6786 100644
--- a/trader/auto_trader.go
+++ b/trader/auto_trader.go
@@ -2,14 +2,13 @@ package trader
import (
"fmt"
+ "github.com/ethereum/go-ethereum/crypto"
"nofx/kernel"
"nofx/logger"
"nofx/mcp"
_ "nofx/mcp/payment"
_ "nofx/mcp/provider"
"nofx/store"
- "nofx/wallet"
- "github.com/ethereum/go-ethereum/crypto"
"nofx/trader/aster"
"nofx/trader/binance"
"nofx/trader/bitget"
@@ -20,6 +19,7 @@ import (
"nofx/trader/kucoin"
"nofx/trader/lighter"
"nofx/trader/okx"
+ "nofx/wallet"
"sync"
"time"
)
@@ -90,9 +90,10 @@ type AutoTraderConfig struct {
QwenKey string
// Custom AI API configuration
- CustomAPIURL string
- CustomAPIKey string
- CustomModelName string
+ CustomAPIURL string
+ CustomAPIKey string
+ CustomModelName string
+ Claw402WalletKey string
// Scan configuration
ScanInterval time.Duration // Scan interval (recommended 3 minutes)
@@ -148,9 +149,9 @@ type AutoTrader struct {
userID string // User ID
gridState *GridState // Grid trading state (only used when StrategyType == "grid_trading")
claw402WalletAddr string // Claw402 wallet address (derived from private key at start)
- consecutiveAIFailures int // Consecutive AI call failures
- safeMode bool // Safe mode: no new positions, protect existing ones
- safeModeReason string // Why safe mode was activated
+ consecutiveAIFailures int // Consecutive AI call failures
+ safeMode bool // Safe mode: no new positions, protect existing ones
+ safeModeReason string // Why safe mode was activated
}
// NewAutoTrader creates an automatic trader
@@ -335,8 +336,8 @@ func NewAutoTrader(config AutoTraderConfig, st *store.Store, userID string) (*Au
}
// Pass claw402 wallet key to strategy engine so nofxos data requests
// are routed through claw402 (reuses the same wallet as AI calls)
- var claw402Key string
- if config.AIModel == "claw402" && config.CustomAPIKey != "" {
+ claw402Key := config.Claw402WalletKey
+ if claw402Key == "" && config.AIModel == "claw402" && config.CustomAPIKey != "" {
claw402Key = config.CustomAPIKey
}
strategyEngine := kernel.NewStrategyEngine(config.StrategyConfig, claw402Key)
diff --git a/trader/auto_trader_grid.go b/trader/auto_trader_grid.go
index 6151dbbb..c16a5d8d 100644
--- a/trader/auto_trader_grid.go
+++ b/trader/auto_trader_grid.go
@@ -324,6 +324,17 @@ func (at *AutoTrader) InitializeGrid() error {
at.gridState.IsInitialized = true
+ // Keep grid orders aligned with the trader's configured cross/isolated mode.
+ if err := at.trader.SetMarginMode(gridConfig.Symbol, at.config.IsCrossMargin); err != nil {
+ logger.Warnf("[Grid] Failed to set margin mode for %s: %v", gridConfig.Symbol, err)
+ } else {
+ marginMode := "cross"
+ if !at.config.IsCrossMargin {
+ marginMode = "isolated"
+ }
+ logger.Infof("[Grid] Margin mode set to %s for %s", marginMode, gridConfig.Symbol)
+ }
+
// CRITICAL: Set leverage on exchange before trading
if err := at.trader.SetLeverage(gridConfig.Symbol, gridConfig.Leverage); err != nil {
logger.Warnf("[Grid] Failed to set leverage %dx on exchange: %v", gridConfig.Leverage, err)
diff --git a/trader/okx/trader.go b/trader/okx/trader.go
index b46f23c4..41e4a45f 100644
--- a/trader/okx/trader.go
+++ b/trader/okx/trader.go
@@ -41,7 +41,7 @@ type OKXTrader struct {
secretKey string
passphrase string
- // Margin mode setting
+ // Margin mode setting used for new orders and leverage changes.
isCrossMargin bool
// Position mode: "long_short_mode" (hedge) or "net_mode" (one-way)
@@ -121,6 +121,7 @@ func NewOKXTrader(apiKey, secretKey, passphrase string) *OKXTrader {
apiKey: apiKey,
secretKey: secretKey,
passphrase: passphrase,
+ isCrossMargin: true,
httpClient: httpClient,
cacheDuration: 15 * time.Second,
instrumentsCache: make(map[string]*OKXInstrument),
@@ -139,10 +140,18 @@ func NewOKXTrader(apiKey, secretKey, passphrase string) *OKXTrader {
}
}
- logger.Infof("✓ OKX trader initialized with position mode: %s", trader.positionMode)
+ logger.Infof("✓ OKX trader initialized with position mode: %s, default margin mode: %s",
+ trader.positionMode, trader.marginMode())
return trader
}
+func (t *OKXTrader) marginMode() string {
+ if t.isCrossMargin {
+ return "cross"
+ }
+ return "isolated"
+}
+
// detectPositionMode gets current position mode from account config
func (t *OKXTrader) detectPositionMode() error {
data, err := t.doRequest("GET", okxAccountConfigPath, nil)
diff --git a/trader/okx/trader_account.go b/trader/okx/trader_account.go
index e10fadf0..9220b3d8 100644
--- a/trader/okx/trader_account.go
+++ b/trader/okx/trader_account.go
@@ -80,49 +80,42 @@ func (t *OKXTrader) GetBalance() (map[string]interface{}, error) {
return result, nil
}
-// SetMarginMode sets margin mode
+// SetMarginMode configures the margin mode (cross/isolated) that will be applied
+// to all subsequent leverage and order requests for this trader instance.
+//
+// OKX V5 unified accounts do not expose a per-symbol mode-switch endpoint that
+// works reliably — the legacy /api/v5/account/set-isolated-mode endpoint returns
+// error 51000 ("Parameter isoMode error") when called on a unified account.
+// Instead, OKX applies the mode per-request via the mgnMode field on
+// /api/v5/account/set-leverage and via the tdMode field on order placement.
+//
+// This implementation therefore stores the configured mode locally and injects it
+// into each subsequent API request, rather than making an API call here.
+// NOTE: unlike Binance/Bybit implementations of this interface, no network call
+// is made — the method only updates local state.
func (t *OKXTrader) SetMarginMode(symbol string, isCrossMargin bool) error {
- instId := t.convertSymbol(symbol)
+ t.isCrossMargin = isCrossMargin
+ mgnMode := t.marginMode()
- mgnMode := "isolated"
- if isCrossMargin {
- mgnMode = "cross"
- }
-
- body := map[string]interface{}{
- "instId": instId,
- "mgnMode": mgnMode,
- }
-
- _, err := t.doRequest("POST", "/api/v5/account/set-isolated-mode", body)
- if err != nil {
- // Ignore error if already in target mode
- if strings.Contains(err.Error(), "already") {
- logger.Infof(" ✓ %s margin mode is already %s", symbol, mgnMode)
- return nil
- }
- // Cannot change when there are positions
- if strings.Contains(err.Error(), "position") {
- logger.Infof(" ⚠️ %s has positions, cannot change margin mode", symbol)
- return nil
- }
- return err
- }
-
- logger.Infof(" ✓ %s margin mode set to %s", symbol, mgnMode)
+ // OKX V5 unified account applies cross/isolated per order via tdMode,
+ // while leverage uses mgnMode on /account/set-leverage.
+ // Persist the configured mode locally so subsequent leverage/order calls use it,
+ // instead of calling the legacy isolated-mode endpoint that returns 51000 errors.
+ logger.Infof(" ✓ %s margin mode configured as %s (applied via tdMode/mgnMode on subsequent requests)", symbol, mgnMode)
return nil
}
// SetLeverage sets leverage
func (t *OKXTrader) SetLeverage(symbol string, leverage int) error {
instId := t.convertSymbol(symbol)
+ marginMode := t.marginMode()
// Set leverage for both long and short
for _, posSide := range []string{"long", "short"} {
body := map[string]interface{}{
"instId": instId,
"lever": strconv.Itoa(leverage),
- "mgnMode": "cross",
+ "mgnMode": marginMode,
"posSide": posSide,
}
@@ -136,7 +129,7 @@ func (t *OKXTrader) SetLeverage(symbol string, leverage int) error {
}
}
- logger.Infof(" ✓ %s leverage set to %dx", symbol, leverage)
+ logger.Infof(" ✓ %s leverage set to %dx (%s)", symbol, leverage, marginMode)
return nil
}
diff --git a/trader/okx/trader_margin_mode_test.go b/trader/okx/trader_margin_mode_test.go
new file mode 100644
index 00000000..e4bbb694
--- /dev/null
+++ b/trader/okx/trader_margin_mode_test.go
@@ -0,0 +1,247 @@
+package okx
+
+import (
+ "bytes"
+ "encoding/json"
+ "io"
+ "net/http"
+ "strings"
+ "testing"
+ "time"
+
+ "nofx/trader/types"
+)
+
+type capturedRequest struct {
+ Method string
+ Path string
+ Body map[string]interface{}
+}
+
+type recordingTransport struct {
+ requests []capturedRequest
+}
+
+func (rt *recordingTransport) RoundTrip(req *http.Request) (*http.Response, error) {
+ var body map[string]interface{}
+ if req.Body != nil {
+ data, _ := io.ReadAll(req.Body)
+ if len(data) > 0 && strings.HasPrefix(strings.TrimSpace(string(data)), "{") {
+ _ = json.Unmarshal(data, &body)
+ }
+ }
+
+ rt.requests = append(rt.requests, capturedRequest{
+ Method: req.Method,
+ Path: req.URL.Path,
+ Body: body,
+ })
+
+ response := `{"code":"0","msg":"","data":[]}`
+ switch req.URL.Path {
+ case okxInstrumentsPath:
+ response = `{"code":"0","msg":"","data":[{"instId":"BTC-USDT-SWAP","ctVal":"0.01","ctMult":"1","lotSz":"1","minSz":"1","maxMktSz":"100000","tickSz":"0.1","ctType":"linear"}]}`
+ case okxOrderPath:
+ response = `{"code":"0","msg":"","data":[{"ordId":"123","clOrdId":"abc","sCode":"0","sMsg":""}]}`
+ }
+
+ return &http.Response{
+ StatusCode: http.StatusOK,
+ Header: make(http.Header),
+ Body: io.NopCloser(bytes.NewBufferString(response)),
+ }, nil
+}
+
+func (rt *recordingTransport) requestsForPath(path string) []capturedRequest {
+ var matches []capturedRequest
+ for _, req := range rt.requests {
+ if req.Path == path {
+ matches = append(matches, req)
+ }
+ }
+ return matches
+}
+
+func newTestOKXTrader(rt *recordingTransport, isCrossMargin bool) *OKXTrader {
+ return &OKXTrader{
+ apiKey: "key",
+ secretKey: "secret",
+ passphrase: "pass",
+ isCrossMargin: isCrossMargin,
+ positionMode: "long_short_mode",
+ httpClient: &http.Client{
+ Transport: rt,
+ },
+ cacheDuration: 15 * time.Second,
+ instrumentsCache: make(map[string]*OKXInstrument),
+ instrumentsCacheTime: time.Now(),
+ }
+}
+
+func TestOKXSetLeverageUsesConfiguredMarginMode(t *testing.T) {
+ rt := &recordingTransport{}
+ trader := newTestOKXTrader(rt, false)
+
+ if err := trader.SetLeverage("BTCUSDT", 5); err != nil {
+ t.Fatalf("SetLeverage failed: %v", err)
+ }
+
+ leverageRequests := rt.requestsForPath(okxLeveragePath)
+ if len(leverageRequests) != 2 {
+ t.Fatalf("expected 2 leverage requests, got %d", len(leverageRequests))
+ }
+
+ for _, req := range leverageRequests {
+ if req.Body["mgnMode"] != "isolated" {
+ t.Fatalf("expected isolated leverage mode, got %#v", req.Body["mgnMode"])
+ }
+ }
+}
+
+func TestOKXSetMarginModeUpdatesFutureRequestsWithoutAPIError(t *testing.T) {
+ rt := &recordingTransport{}
+ trader := newTestOKXTrader(rt, true)
+
+ if err := trader.SetMarginMode("BTCUSDT", false); err != nil {
+ t.Fatalf("SetMarginMode failed: %v", err)
+ }
+
+ if len(rt.requestsForPath("/api/v5/account/set-isolated-mode")) != 0 {
+ t.Fatal("expected SetMarginMode not to call legacy isolated-mode endpoint")
+ }
+
+ if err := trader.SetLeverage("BTCUSDT", 5); err != nil {
+ t.Fatalf("SetLeverage failed: %v", err)
+ }
+
+ leverageRequests := rt.requestsForPath(okxLeveragePath)
+ if len(leverageRequests) != 2 {
+ t.Fatalf("expected 2 leverage requests, got %d", len(leverageRequests))
+ }
+
+ for _, req := range leverageRequests {
+ if req.Body["mgnMode"] != "isolated" {
+ t.Fatalf("expected isolated leverage mode after SetMarginMode(false), got %#v", req.Body["mgnMode"])
+ }
+ }
+}
+
+func TestOKXOpenLongUsesConfiguredMarginMode(t *testing.T) {
+ rt := &recordingTransport{}
+ trader := newTestOKXTrader(rt, false)
+
+ if _, err := trader.OpenLong("BTCUSDT", 0.1, 5); err != nil {
+ t.Fatalf("OpenLong failed: %v", err)
+ }
+
+ orderRequests := rt.requestsForPath(okxOrderPath)
+ if len(orderRequests) == 0 {
+ t.Fatal("expected at least one order request")
+ }
+
+ lastOrder := orderRequests[len(orderRequests)-1]
+ if lastOrder.Body["tdMode"] != "isolated" {
+ t.Fatalf("expected isolated tdMode, got %#v", lastOrder.Body["tdMode"])
+ }
+}
+
+func TestOKXSetStopLossUsesConfiguredMarginMode(t *testing.T) {
+ rt := &recordingTransport{}
+ trader := newTestOKXTrader(rt, false)
+
+ if err := trader.SetStopLoss("BTCUSDT", "LONG", 0.1, 90000); err != nil {
+ t.Fatalf("SetStopLoss failed: %v", err)
+ }
+
+ algoRequests := rt.requestsForPath(okxAlgoOrderPath)
+ if len(algoRequests) != 1 {
+ t.Fatalf("expected 1 algo order request, got %d", len(algoRequests))
+ }
+
+ if algoRequests[0].Body["tdMode"] != "isolated" {
+ t.Fatalf("expected isolated tdMode, got %#v", algoRequests[0].Body["tdMode"])
+ }
+}
+
+func TestOKXPlaceLimitOrderUsesConfiguredMarginMode(t *testing.T) {
+ rt := &recordingTransport{}
+ trader := newTestOKXTrader(rt, false)
+
+ _, err := trader.PlaceLimitOrder(&types.LimitOrderRequest{
+ Symbol: "BTCUSDT",
+ Side: "BUY",
+ PositionSide: "LONG",
+ Price: 95000,
+ Quantity: 0.1,
+ Leverage: 3,
+ })
+ if err != nil {
+ t.Fatalf("PlaceLimitOrder failed: %v", err)
+ }
+
+ orderRequests := rt.requestsForPath(okxOrderPath)
+ if len(orderRequests) != 1 {
+ t.Fatalf("expected 1 limit order request, got %d", len(orderRequests))
+ }
+
+ if orderRequests[0].Body["tdMode"] != "isolated" {
+ t.Fatalf("expected isolated tdMode, got %#v", orderRequests[0].Body["tdMode"])
+ }
+}
+
+func TestOKXCrossMarginModeUsedInLeverage(t *testing.T) {
+ rt := &recordingTransport{}
+ trader := newTestOKXTrader(rt, true) // cross margin
+
+ if err := trader.SetLeverage("BTCUSDT", 10); err != nil {
+ t.Fatalf("SetLeverage failed: %v", err)
+ }
+
+ leverageRequests := rt.requestsForPath(okxLeveragePath)
+ if len(leverageRequests) != 2 {
+ t.Fatalf("expected 2 leverage requests, got %d", len(leverageRequests))
+ }
+
+ for _, req := range leverageRequests {
+ if req.Body["mgnMode"] != "cross" {
+ t.Fatalf("expected cross leverage mode, got %#v", req.Body["mgnMode"])
+ }
+ }
+}
+
+func TestOKXOpenShortUsesConfiguredMarginMode(t *testing.T) {
+ rt := &recordingTransport{}
+ trader := newTestOKXTrader(rt, false) // isolated
+
+ if _, err := trader.OpenShort("BTCUSDT", 0.1, 5); err != nil {
+ t.Fatalf("OpenShort failed: %v", err)
+ }
+
+ orderRequests := rt.requestsForPath(okxOrderPath)
+ if len(orderRequests) == 0 {
+ t.Fatal("expected at least one order request")
+ }
+
+ lastOrder := orderRequests[len(orderRequests)-1]
+ if lastOrder.Body["tdMode"] != "isolated" {
+ t.Fatalf("expected isolated tdMode for OpenShort, got %#v", lastOrder.Body["tdMode"])
+ }
+}
+
+func TestOKXSetTakeProfitUsesConfiguredMarginMode(t *testing.T) {
+ rt := &recordingTransport{}
+ trader := newTestOKXTrader(rt, false) // isolated
+
+ if err := trader.SetTakeProfit("BTCUSDT", "LONG", 0.1, 100000); err != nil {
+ t.Fatalf("SetTakeProfit failed: %v", err)
+ }
+
+ algoRequests := rt.requestsForPath(okxAlgoOrderPath)
+ if len(algoRequests) != 1 {
+ t.Fatalf("expected 1 algo order request, got %d", len(algoRequests))
+ }
+
+ if algoRequests[0].Body["tdMode"] != "isolated" {
+ t.Fatalf("expected isolated tdMode for SetTakeProfit, got %#v", algoRequests[0].Body["tdMode"])
+ }
+}
diff --git a/trader/okx/trader_orders.go b/trader/okx/trader_orders.go
index 33697acc..2676db3f 100644
--- a/trader/okx/trader_orders.go
+++ b/trader/okx/trader_orders.go
@@ -41,9 +41,11 @@ func (t *OKXTrader) OpenLong(symbol string, quantity float64, leverage int) (map
szStr = t.formatSize(sz, inst)
}
+ marginMode := t.marginMode()
+
body := map[string]interface{}{
"instId": instId,
- "tdMode": "cross",
+ "tdMode": marginMode,
"side": "buy",
"posSide": "long",
"ordType": "market",
@@ -118,9 +120,11 @@ func (t *OKXTrader) OpenShort(symbol string, quantity float64, leverage int) (ma
szStr = t.formatSize(sz, inst)
}
+ marginMode := t.marginMode()
+
body := map[string]interface{}{
"instId": instId,
- "tdMode": "cross",
+ "tdMode": marginMode,
"side": "sell",
"posSide": "short",
"ordType": "market",
@@ -410,9 +414,11 @@ func (t *OKXTrader) SetStopLoss(symbol string, positionSide string, quantity, st
posSide = "short"
}
+ marginMode := t.marginMode()
+
body := map[string]interface{}{
"instId": instId,
- "tdMode": "cross",
+ "tdMode": marginMode,
"side": side,
"posSide": posSide,
"ordType": "conditional",
@@ -453,9 +459,11 @@ func (t *OKXTrader) SetTakeProfit(symbol string, positionSide string, quantity,
posSide = "short"
}
+ marginMode := t.marginMode()
+
body := map[string]interface{}{
"instId": instId,
- "tdMode": "cross",
+ "tdMode": marginMode,
"side": side,
"posSide": posSide,
"ordType": "conditional",
@@ -815,9 +823,11 @@ func (t *OKXTrader) PlaceLimitOrder(req *types.LimitOrderRequest) (*types.LimitO
posSide = "short"
}
+ marginMode := t.marginMode()
+
body := map[string]interface{}{
"instId": instId,
- "tdMode": "cross",
+ "tdMode": marginMode,
"side": side,
"posSide": posSide,
"ordType": "limit",
diff --git a/web/src/router/AppRoutes.tsx b/web/src/router/AppRoutes.tsx
index 6469643e..7ee5d2e8 100644
--- a/web/src/router/AppRoutes.tsx
+++ b/web/src/router/AppRoutes.tsx
@@ -439,7 +439,9 @@ export function AppRoutes() {
+ ) : systemConfig?.initialized ? (
) : (