diff --git a/frontend-modern/src/api/ai.ts b/frontend-modern/src/api/ai.ts index 6aa3649..afc4d19 100644 --- a/frontend-modern/src/api/ai.ts +++ b/frontend-modern/src/api/ai.ts @@ -55,7 +55,7 @@ export class AIAPI { return apiFetchJSON(`${this.baseUrl}/ai/cost/reset`, { method: 'POST', body: JSON.stringify({}), - }) as Promise<{ ok: boolean }>; + }) as Promise<{ ok: boolean; backup_file?: string }>; } // Start OAuth flow for Claude Pro/Max subscription diff --git a/frontend-modern/src/components/AI/AICostDashboard.tsx b/frontend-modern/src/components/AI/AICostDashboard.tsx index 19fca74..0b6ba96 100644 --- a/frontend-modern/src/components/AI/AICostDashboard.tsx +++ b/frontend-modern/src/components/AI/AICostDashboard.tsx @@ -187,10 +187,14 @@ export const AICostDashboard: Component = () => { }; const resetHistory = async () => { - if (!confirm('Reset AI usage history? This clears the stored token/cost history.')) return; + if (!confirm('Reset AI usage history? A backup will be created in the Pulse config directory.')) return; try { - await AIAPI.resetCostHistory(); - notificationStore.success('AI usage history reset'); + const result = await AIAPI.resetCostHistory(); + if (result.backup_file) { + notificationStore.success(`AI usage history reset (backup: ${result.backup_file})`); + } else { + notificationStore.success('AI usage history reset'); + } await loadSummary(days()); } catch (err) { logger.error('[AICostDashboard] Failed to reset AI cost history:', err); diff --git a/internal/api/ai_handlers.go b/internal/api/ai_handlers.go index 256b470..4659cd3 100644 --- a/internal/api/ai_handlers.go +++ b/internal/api/ai_handlers.go @@ -6,6 +6,8 @@ import ( "fmt" "io" "net/http" + "os" + "path/filepath" "strings" "sync" "time" @@ -2460,13 +2462,36 @@ func (h *AISettingsHandler) HandleResetAICostHistory(w http.ResponseWriter, r *h return } + backupFile := "" + if h.persistence != nil { + configDir := h.persistence.DataDir() + if strings.TrimSpace(configDir) != "" { + usagePath := filepath.Join(configDir, "ai_usage_history.json") + if _, err := os.Stat(usagePath); err == nil { + ts := time.Now().UTC().Format("20060102-150405") + backupFile = fmt.Sprintf("ai_usage_history.json.bak-%s", ts) + backupPath := filepath.Join(configDir, backupFile) + if err := os.Rename(usagePath, backupPath); err != nil { + log.Error().Err(err).Str("path", usagePath).Msg("Failed to backup AI usage history before reset") + http.Error(w, "Failed to backup AI usage history", http.StatusInternalServerError) + return + } + } + } + } + if err := h.aiService.ClearCostHistory(); err != nil { log.Error().Err(err).Msg("Failed to clear AI cost history") http.Error(w, "Failed to clear AI cost history", http.StatusInternalServerError) return } - if err := utils.WriteJSONResponse(w, map[string]any{"ok": true}); err != nil { + resp := map[string]any{"ok": true} + if backupFile != "" { + resp["backup_file"] = backupFile + } + + if err := utils.WriteJSONResponse(w, resp); err != nil { log.Error().Err(err).Msg("Failed to write clear cost history response") } }