From c93ee337a710af7e14704ea0e4b6111ed51e3b15 Mon Sep 17 00:00:00 2001 From: Lance Date: Sun, 12 Apr 2026 11:43:34 +0800 Subject: [PATCH] release: model switching fix + active strategy deletion guard (#1465) * feat(store): prevent deletion of active strategies and update translations (#1461) Co-authored-by: Dean * fix: allow model switching without re-entering wallet key Users with existing wallets could not switch AI models because the "Start Trading" button required a valid private key even when one was already configured. Now the button is enabled when hasExistingWallet is true, and handleSubmit passes an empty key so the backend preserves the existing key. Co-Authored-By: Claude Opus 4.6 (1M context) --------- Co-authored-by: deanokk Co-authored-by: Dean Co-authored-by: Claude Opus 4.6 (1M context) --- store/strategy.go | 3 + .../components/trader/ModelConfigModal.tsx | 11 ++- web/src/i18n/translations.ts | 3 + web/src/pages/StrategyStudioPage.tsx | 85 +++++++++++++------ web/src/types/strategy.ts | 2 +- 5 files changed, 75 insertions(+), 29 deletions(-) diff --git a/store/strategy.go b/store/strategy.go index 19bc9526..8860e8fe 100644 --- a/store/strategy.go +++ b/store/strategy.go @@ -444,6 +444,9 @@ func (s *StrategyStore) Delete(userID, id string) error { if st.IsDefault { return fmt.Errorf("cannot delete system default strategy") } + if st.IsActive { + return fmt.Errorf("cannot delete active strategy") + } } // Check if any trader references this strategy diff --git a/web/src/components/trader/ModelConfigModal.tsx b/web/src/components/trader/ModelConfigModal.tsx index 4b8d0250..e24c5054 100644 --- a/web/src/components/trader/ModelConfigModal.tsx +++ b/web/src/components/trader/ModelConfigModal.tsx @@ -77,8 +77,11 @@ export function ModelConfigModal({ const handleSubmit = (e: React.FormEvent) => { e.preventDefault() - if (!selectedModelId || !apiKey.trim()) return - onSave(selectedModelId, apiKey.trim(), baseUrl.trim() || undefined, modelName.trim() || undefined) + if (!selectedModelId) return + const key = apiKey.trim() + // Allow empty key when editing an existing model (backend preserves existing key) + if (!key && !editingModelId) return + onSave(selectedModelId, key, baseUrl.trim() || undefined, modelName.trim() || undefined) } const availableModels = allModels || [] @@ -833,9 +836,9 @@ function Claw402ConfigForm({ diff --git a/web/src/i18n/translations.ts b/web/src/i18n/translations.ts index 48a6ef8c..57bc63ee 100644 --- a/web/src/i18n/translations.ts +++ b/web/src/i18n/translations.ts @@ -1066,6 +1066,7 @@ export const translations = { newStrategyName: 'New Strategy', strategyCopy: 'Strategy Copy', strategyDeleted: 'Strategy deleted', + cannotDeleteActiveStrategy: 'Active strategy cannot be deleted', confirmDeleteStrategy: 'Delete this strategy?', confirmDelete: 'Confirm Delete', delete: 'Delete', @@ -2373,6 +2374,7 @@ export const translations = { newStrategyName: '新策略', strategyCopy: '策略副本', strategyDeleted: '策略已删除', + cannotDeleteActiveStrategy: '激活中的策略不能删除', confirmDeleteStrategy: '确定删除此策略?', confirmDelete: '确认删除', delete: '删除', @@ -3482,6 +3484,7 @@ export const translations = { newStrategyName: 'Strategi Baru', strategyCopy: 'Salinan Strategi', strategyDeleted: 'Strategi dihapus', + cannotDeleteActiveStrategy: 'Strategi aktif tidak bisa dihapus', confirmDeleteStrategy: 'Hapus strategi ini?', confirmDelete: 'Konfirmasi Hapus', delete: 'Hapus', diff --git a/web/src/pages/StrategyStudioPage.tsx b/web/src/pages/StrategyStudioPage.tsx index 5e4ee255..62ddea00 100644 --- a/web/src/pages/StrategyStudioPage.tsx +++ b/web/src/pages/StrategyStudioPage.tsx @@ -30,7 +30,7 @@ import { Upload, Globe, } from 'lucide-react' -import type { Strategy, StrategyConfig, AIModel } from '../types' +import type { Strategy, StrategyConfig, AIModel, GridStrategyConfig } from '../types' import { confirmToast, notify } from '../lib/notify' import { CoinSourceEditor } from '../components/strategy/CoinSourceEditor' import { IndicatorEditor } from '../components/strategy/IndicatorEditor' @@ -94,6 +94,7 @@ export function StrategyStudioPage() { duration_ms?: number } | null>(null) const [isRunningAiTest, setIsRunningAiTest] = useState(false) + const gridConfigCacheRef = useRef>({}) const toggleSection = (section: keyof typeof expandedSections) => { setExpandedSections((prev) => ({ @@ -156,6 +157,12 @@ export function StrategyStudioPage() { fetchAiModels() }, [fetchStrategies, fetchAiModels]) + useEffect(() => { + if (!selectedStrategy?.id || !editingConfig?.grid_config) return + + gridConfigCacheRef.current[selectedStrategy.id] = { ...editingConfig.grid_config } + }, [selectedStrategy?.id, editingConfig?.grid_config]) + // Track previous language to detect actual changes const prevLanguageRef = useRef(language) @@ -248,6 +255,12 @@ export function StrategyStudioPage() { // Delete strategy const handleDeleteStrategy = async (id: string) => { if (!token) return + const strategy = strategies.find((item) => item.id === id) + + if (strategy?.is_active) { + notify.error(tr('cannotDeleteActiveStrategy')) + return + } // Check if strategy is in use by any trader before showing dialog try { @@ -443,14 +456,51 @@ export function StrategyStudioPage() { section: K, value: StrategyConfig[K] ) => { - if (!editingConfig) return - setEditingConfig({ - ...editingConfig, - [section]: value, + setEditingConfig((prev) => { + if (!prev) return prev + return { + ...prev, + [section]: value, + } }) setHasChanges(true) } + const handleStrategyTypeChange = (strategyType: NonNullable) => { + if (selectedStrategy?.is_default) return + + const cachedGridConfig = selectedStrategy?.id + ? gridConfigCacheRef.current[selectedStrategy.id] + : null + + setEditingConfig((prev) => { + if (!prev) return prev + + if (strategyType === 'ai_trading') { + if (selectedStrategy?.id && prev.grid_config) { + gridConfigCacheRef.current[selectedStrategy.id] = { ...prev.grid_config } + } + + return { + ...prev, + strategy_type: 'ai_trading', + // Use null so the field is preserved in JSON and backend merge can actually clear it. + grid_config: null, + } + } + + return { + ...prev, + strategy_type: 'grid_trading', + grid_config: cachedGridConfig ?? prev.grid_config ?? { ...defaultGridConfig }, + } + }) + + setPromptPreview(null) + setAiTestResult(null) + setHasChanges(true) + } + // Fetch prompt preview const fetchPromptPreview = async () => { if (!token || !editingConfig) return @@ -666,7 +716,7 @@ export function StrategyStudioPage() {
-

{tr('strategyStudio')}

+

{tr('title')}

{tr('subtitle')}

@@ -743,8 +793,9 @@ export function StrategyStudioPage() { @@ -848,13 +899,7 @@ export function StrategyStudioPage() {