release: model switching fix + active strategy deletion guard (#1465)

* feat(store): prevent deletion of active strategies and update translations (#1461)

Co-authored-by: Dean <afei.wuhao@gmail.com>

* 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) <noreply@anthropic.com>

---------

Co-authored-by: deanokk <wuhao@vergex.trade>
Co-authored-by: Dean <afei.wuhao@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Lance
2026-04-12 11:43:34 +08:00
committed by GitHub
parent 6fe849c18d
commit c93ee337a7
5 changed files with 75 additions and 29 deletions
+3
View File
@@ -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
@@ -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({
</button>
<button
type="submit"
disabled={!isKeyValid}
disabled={!isKeyValid && !hasExistingWallet}
className="flex-1 flex items-center justify-center gap-2 px-4 py-3 rounded-xl text-sm font-bold transition-all hover:scale-[1.02] disabled:opacity-50 disabled:cursor-not-allowed"
style={{ background: isKeyValid ? 'linear-gradient(135deg, #2563EB, #7C3AED)' : '#2B3139', color: '#fff' }}
style={{ background: (isKeyValid || hasExistingWallet) ? 'linear-gradient(135deg, #2563EB, #7C3AED)' : '#2B3139', color: '#fff' }}
>
{'🚀 ' + t('modelConfig.startTrading', language)}
</button>
+3
View File
@@ -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',
+60 -23
View File
@@ -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<Record<string, GridStrategyConfig>>({})
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,
setEditingConfig((prev) => {
if (!prev) return prev
return {
...prev,
[section]: value,
}
})
setHasChanges(true)
}
const handleStrategyTypeChange = (strategyType: NonNullable<StrategyConfig['strategy_type']>) => {
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() {
<Sparkles className="w-5 h-5 text-black" />
</div>
<div>
<h1 className="text-lg font-bold text-nofx-text">{tr('strategyStudio')}</h1>
<h1 className="text-lg font-bold text-nofx-text">{tr('title')}</h1>
<p className="text-xs text-nofx-text-muted">{tr('subtitle')}</p>
</div>
</div>
@@ -743,8 +793,9 @@ export function StrategyStudioPage() {
</button>
<button
onClick={(e) => { e.stopPropagation(); handleDeleteStrategy(strategy.id) }}
className="p-1 rounded hover:bg-nofx-danger/20 text-nofx-danger"
title={tr('deleteTooltip')}
disabled={strategy.is_active}
className="p-1 rounded hover:bg-nofx-danger/20 text-nofx-danger disabled:opacity-40 disabled:cursor-not-allowed disabled:hover:bg-transparent"
title={strategy.is_active ? tr('cannotDeleteActiveStrategy') : tr('deleteTooltip')}
>
<Trash2 className="w-3 h-3" />
</button>
@@ -848,13 +899,7 @@ export function StrategyStudioPage() {
</div>
<div className="grid grid-cols-2 gap-3">
<button
onClick={() => {
if (!selectedStrategy?.is_default) {
updateConfig('strategy_type', 'ai_trading')
// Clear grid config when switching to AI trading
updateConfig('grid_config', undefined)
}
}}
onClick={() => handleStrategyTypeChange('ai_trading')}
disabled={selectedStrategy?.is_default}
className={`p-3 rounded-lg border transition-all ${
(!editingConfig.strategy_type || editingConfig.strategy_type === 'ai_trading')
@@ -869,15 +914,7 @@ export function StrategyStudioPage() {
<p className="text-xs text-nofx-text-muted text-left">{tr('aiTradingDesc')}</p>
</button>
<button
onClick={() => {
if (!selectedStrategy?.is_default) {
updateConfig('strategy_type', 'grid_trading')
// Initialize grid config if not exists
if (!editingConfig.grid_config) {
updateConfig('grid_config', defaultGridConfig)
}
}
}}
onClick={() => handleStrategyTypeChange('grid_trading')}
disabled={selectedStrategy?.is_default}
className={`p-3 rounded-lg border transition-all ${
editingConfig.strategy_type === 'grid_trading'
+1 -1
View File
@@ -50,7 +50,7 @@ export interface StrategyConfig {
risk_control: RiskControlConfig;
prompt_sections?: PromptSectionsConfig;
// Grid trading configuration (only used when strategy_type is 'grid_trading')
grid_config?: GridStrategyConfig;
grid_config?: GridStrategyConfig | null;
}
// Grid trading specific configuration