mirror of
https://github.com/laoxong/nofx.git
synced 2026-06-04 09:58:22 +08:00
feat: enhance strategy deletion process with user feedback and validation checks
This commit is contained in:
+1
-1
@@ -344,7 +344,7 @@ func (s *Server) handleDeleteStrategy(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := s.store.Strategy().Delete(userID, strategyID); err != nil {
|
||||
SafeInternalError(c, "Failed to delete strategy", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": SanitizeError(err, "Failed to delete strategy")})
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
+12
-2
@@ -440,8 +440,18 @@ func (s *StrategyStore) Update(strategy *Strategy) error {
|
||||
func (s *StrategyStore) Delete(userID, id string) error {
|
||||
// do not allow deleting system default strategy
|
||||
var st Strategy
|
||||
if err := s.db.Where("id = ?", id).First(&st).Error; err == nil && st.IsDefault {
|
||||
return fmt.Errorf("cannot delete system default strategy")
|
||||
if err := s.db.Where("id = ?", id).First(&st).Error; err == nil {
|
||||
if st.IsDefault {
|
||||
return fmt.Errorf("cannot delete system default strategy")
|
||||
}
|
||||
}
|
||||
|
||||
// Check if any trader references this strategy
|
||||
var count int64
|
||||
if err := s.db.Model(&Trader{}).
|
||||
Where("user_id = ? AND strategy_id = ?", userID, id).
|
||||
Count(&count).Error; err == nil && count > 0 {
|
||||
return fmt.Errorf("cannot delete strategy in use by %d trader(s) - reassign those traders first", count)
|
||||
}
|
||||
|
||||
return s.db.Where("id = ? AND user_id = ?", id, userID).Delete(&Strategy{}).Error
|
||||
|
||||
+47
-13
@@ -137,6 +137,18 @@ function App() {
|
||||
const hasPersistedAuth =
|
||||
!!localStorage.getItem('auth_token') && !!localStorage.getItem('auth_user')
|
||||
|
||||
// Poll-off states: stop polling after 3 consecutive failures
|
||||
const [accountPollOff, setAccountPollOff] = useState(false)
|
||||
const [positionsPollOff, setPositionsPollOff] = useState(false)
|
||||
const [decisionsPollOff, setDecisionsPollOff] = useState(false)
|
||||
|
||||
// Reset poll-off states when trader changes
|
||||
useEffect(() => {
|
||||
setAccountPollOff(false)
|
||||
setPositionsPollOff(false)
|
||||
setDecisionsPollOff(false)
|
||||
}, [selectedTraderId])
|
||||
|
||||
// 监听URL变化,同步页面状态
|
||||
useEffect(() => {
|
||||
const handleRouteChange = () => {
|
||||
@@ -242,11 +254,16 @@ function App() {
|
||||
currentPage === 'trader' && selectedTraderId
|
||||
? `account-${selectedTraderId}`
|
||||
: null,
|
||||
() => api.getAccount(selectedTraderId),
|
||||
() => api.getAccount(selectedTraderId, true),
|
||||
{
|
||||
refreshInterval: 15000, // 15秒刷新(配合后端15秒缓存)
|
||||
revalidateOnFocus: false, // 禁用聚焦时重新验证,减少请求
|
||||
dedupingInterval: 10000, // 10秒去重,防止短时间内重复请求
|
||||
refreshInterval: accountPollOff ? 0 : 15000,
|
||||
revalidateOnFocus: false,
|
||||
dedupingInterval: 10000,
|
||||
onErrorRetry: (_err, _key, _config, revalidate, { retryCount }) => {
|
||||
if (retryCount >= 2) { setAccountPollOff(true); return }
|
||||
setTimeout(() => revalidate({ retryCount }), 500)
|
||||
},
|
||||
onSuccess: () => { if (accountPollOff) setAccountPollOff(false) },
|
||||
}
|
||||
)
|
||||
|
||||
@@ -254,11 +271,16 @@ function App() {
|
||||
currentPage === 'trader' && selectedTraderId
|
||||
? `positions-${selectedTraderId}`
|
||||
: null,
|
||||
() => api.getPositions(selectedTraderId),
|
||||
() => api.getPositions(selectedTraderId, true),
|
||||
{
|
||||
refreshInterval: 15000, // 15秒刷新(配合后端15秒缓存)
|
||||
revalidateOnFocus: false, // 禁用聚焦时重新验证,减少请求
|
||||
dedupingInterval: 10000, // 10秒去重,防止短时间内重复请求
|
||||
refreshInterval: positionsPollOff ? 0 : 15000,
|
||||
revalidateOnFocus: false,
|
||||
dedupingInterval: 10000,
|
||||
onErrorRetry: (_err, _key, _config, revalidate, { retryCount }) => {
|
||||
if (retryCount >= 2) { setPositionsPollOff(true); return }
|
||||
setTimeout(() => revalidate({ retryCount }), 500)
|
||||
},
|
||||
onSuccess: () => { if (positionsPollOff) setPositionsPollOff(false) },
|
||||
}
|
||||
)
|
||||
|
||||
@@ -266,11 +288,16 @@ function App() {
|
||||
currentPage === 'trader' && selectedTraderId
|
||||
? `decisions/latest-${selectedTraderId}-${decisionsLimit}`
|
||||
: null,
|
||||
() => api.getLatestDecisions(selectedTraderId, decisionsLimit),
|
||||
() => api.getLatestDecisions(selectedTraderId, decisionsLimit, true),
|
||||
{
|
||||
refreshInterval: 30000, // 30秒刷新(决策更新频率较低)
|
||||
refreshInterval: decisionsPollOff ? 0 : 30000,
|
||||
revalidateOnFocus: false,
|
||||
dedupingInterval: 20000,
|
||||
onErrorRetry: (_err, _key, _config, revalidate, { retryCount }) => {
|
||||
if (retryCount >= 2) { setDecisionsPollOff(true); return }
|
||||
setTimeout(() => revalidate({ retryCount }), 500)
|
||||
},
|
||||
onSuccess: () => { if (decisionsPollOff) setDecisionsPollOff(false) },
|
||||
}
|
||||
)
|
||||
|
||||
@@ -295,6 +322,13 @@ function App() {
|
||||
|
||||
const selectedTrader = traders?.find((t) => t.trader_id === selectedTraderId)
|
||||
|
||||
// When polling has permanently failed, provide zero-value data instead of keeping skeleton
|
||||
const effectiveAccount = (accountPollOff && !account)
|
||||
? { total_equity: 0, available_balance: 0, total_pnl: 0, total_pnl_pct: 0, position_count: 0, margin_used: 0, margin_used_pct: 0 } as AccountInfo
|
||||
: account
|
||||
const effectivePositions = (positionsPollOff && !positions) ? [] as Position[] : positions
|
||||
const effectiveDecisions = (decisionsPollOff && !decisions) ? [] as DecisionRecord[] : decisions
|
||||
|
||||
// Handle routing
|
||||
useEffect(() => {
|
||||
const handlePopState = () => {
|
||||
@@ -510,9 +544,9 @@ function App() {
|
||||
<TraderDashboardPage
|
||||
selectedTrader={selectedTrader}
|
||||
status={status}
|
||||
account={account}
|
||||
positions={positions}
|
||||
decisions={decisions}
|
||||
account={effectiveAccount}
|
||||
positions={effectivePositions}
|
||||
decisions={effectiveDecisions}
|
||||
decisionsLimit={decisionsLimit}
|
||||
onDecisionsLimitChange={setDecisionsLimit}
|
||||
stats={stats}
|
||||
|
||||
@@ -19,20 +19,20 @@ export const dataApi = {
|
||||
return result.data!
|
||||
},
|
||||
|
||||
async getAccount(traderId?: string): Promise<AccountInfo> {
|
||||
async getAccount(traderId?: string, silent?: boolean): Promise<AccountInfo> {
|
||||
const url = traderId
|
||||
? `${API_BASE}/account?trader_id=${traderId}`
|
||||
: `${API_BASE}/account`
|
||||
const result = await httpClient.get<AccountInfo>(url)
|
||||
const result = await httpClient.request<AccountInfo>(url, { silent })
|
||||
if (!result.success) throw new Error('Failed to fetch account info')
|
||||
return result.data!
|
||||
},
|
||||
|
||||
async getPositions(traderId?: string): Promise<Position[]> {
|
||||
async getPositions(traderId?: string, silent?: boolean): Promise<Position[]> {
|
||||
const url = traderId
|
||||
? `${API_BASE}/positions?trader_id=${traderId}`
|
||||
: `${API_BASE}/positions`
|
||||
const result = await httpClient.get<Position[]>(url)
|
||||
const result = await httpClient.request<Position[]>(url, { silent })
|
||||
if (!result.success) throw new Error('Failed to fetch positions')
|
||||
return result.data!
|
||||
},
|
||||
@@ -48,7 +48,8 @@ export const dataApi = {
|
||||
|
||||
async getLatestDecisions(
|
||||
traderId?: string,
|
||||
limit: number = 5
|
||||
limit: number = 5,
|
||||
silent?: boolean
|
||||
): Promise<DecisionRecord[]> {
|
||||
const params = new URLSearchParams()
|
||||
if (traderId) {
|
||||
@@ -56,8 +57,9 @@ export const dataApi = {
|
||||
}
|
||||
params.append('limit', limit.toString())
|
||||
|
||||
const result = await httpClient.get<DecisionRecord[]>(
|
||||
`${API_BASE}/decisions/latest?${params}`
|
||||
const result = await httpClient.request<DecisionRecord[]>(
|
||||
`${API_BASE}/decisions/latest?${params}`,
|
||||
{ silent }
|
||||
)
|
||||
if (!result.success) throw new Error('Failed to fetch latest decisions')
|
||||
return result.data!
|
||||
|
||||
+28
-12
@@ -85,11 +85,16 @@ export class HttpClient {
|
||||
* Only business errors are returned to caller
|
||||
*/
|
||||
private async handleError(error: AxiosError): Promise<any> {
|
||||
const isSilent = (error.config as any)?.silentError === true
|
||||
|
||||
// Network error (no response from server)
|
||||
if (!error.response) {
|
||||
toast.error('Network error - Please check your connection', {
|
||||
description: 'Unable to reach the server',
|
||||
})
|
||||
if (!isSilent) {
|
||||
toast.error('Network error - Please check your connection', {
|
||||
id: 'network-error',
|
||||
description: 'Unable to reach the server',
|
||||
})
|
||||
}
|
||||
throw new Error('Network error')
|
||||
}
|
||||
|
||||
@@ -132,25 +137,34 @@ export class HttpClient {
|
||||
|
||||
// Handle 403 Forbidden - system error
|
||||
if (status === 403) {
|
||||
toast.error('Permission Denied', {
|
||||
description: 'You do not have permission to access this resource',
|
||||
})
|
||||
if (!isSilent) {
|
||||
toast.error('Permission Denied', {
|
||||
id: 'permission-denied',
|
||||
description: 'You do not have permission to access this resource',
|
||||
})
|
||||
}
|
||||
throw new Error('Permission denied')
|
||||
}
|
||||
|
||||
// Handle 404 Not Found - system error
|
||||
if (status === 404) {
|
||||
toast.error('API Not Found', {
|
||||
description: 'The requested endpoint does not exist (404)',
|
||||
})
|
||||
if (!isSilent) {
|
||||
toast.error('API Not Found', {
|
||||
id: `404-${(error.config as any)?.url || 'unknown'}`,
|
||||
description: 'The requested endpoint does not exist (404)',
|
||||
})
|
||||
}
|
||||
throw new Error('API not found')
|
||||
}
|
||||
|
||||
// Handle 500+ Server Error - system error
|
||||
if (status >= 500) {
|
||||
toast.error('Server Error', {
|
||||
description: 'Please try again later or contact support',
|
||||
})
|
||||
if (!isSilent) {
|
||||
toast.error('Server Error', {
|
||||
id: 'server-error',
|
||||
description: 'Please try again later or contact support',
|
||||
})
|
||||
}
|
||||
throw new Error('Server error')
|
||||
}
|
||||
|
||||
@@ -171,6 +185,7 @@ export class HttpClient {
|
||||
data?: any
|
||||
params?: any
|
||||
headers?: Record<string, string>
|
||||
silent?: boolean
|
||||
} = {}
|
||||
): Promise<ApiResponse<T>> {
|
||||
try {
|
||||
@@ -180,6 +195,7 @@ export class HttpClient {
|
||||
data: options.data,
|
||||
params: options.params,
|
||||
headers: options.headers,
|
||||
...(options.silent && { silentError: true }),
|
||||
})
|
||||
|
||||
// Success
|
||||
|
||||
@@ -247,6 +247,24 @@ export function StrategyStudioPage() {
|
||||
const handleDeleteStrategy = async (id: string) => {
|
||||
if (!token) return
|
||||
|
||||
// Check if strategy is in use by any trader before showing dialog
|
||||
try {
|
||||
const tradersResp = await fetch(`${API_BASE}/api/my-traders`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
})
|
||||
if (tradersResp.ok) {
|
||||
const traderList = await tradersResp.json()
|
||||
const using = traderList.filter((t: any) => t.strategy_id === id)
|
||||
if (using.length > 0) {
|
||||
const names = using.map((t: any) => t.trader_name).join(', ')
|
||||
notify.error(`Strategy is in use by: ${names}`)
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// fetch failed — proceed, backend will guard
|
||||
}
|
||||
|
||||
const confirmed = await confirmToast(
|
||||
tr('confirmDeleteStrategy'),
|
||||
{
|
||||
@@ -262,9 +280,12 @@ export function StrategyStudioPage() {
|
||||
method: 'DELETE',
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
})
|
||||
if (!response.ok) throw new Error('Failed to delete strategy')
|
||||
if (!response.ok) {
|
||||
const data = await response.json().catch(() => ({}))
|
||||
notify.error(data.error || 'Failed to delete strategy')
|
||||
return
|
||||
}
|
||||
notify.success(tr('strategyDeleted'))
|
||||
// Clear selection if deleted strategy was selected
|
||||
if (selectedStrategy?.id === id) {
|
||||
setSelectedStrategy(null)
|
||||
setEditingConfig(null)
|
||||
@@ -272,9 +293,7 @@ export function StrategyStudioPage() {
|
||||
}
|
||||
await fetchStrategies()
|
||||
} catch (err) {
|
||||
const errorMsg = err instanceof Error ? err.message : 'Unknown error'
|
||||
setError(errorMsg)
|
||||
notify.error(errorMsg)
|
||||
notify.error(err instanceof Error ? err.message : 'Unknown error')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user