Skip to content

Commit ec1a0de

Browse files
committed
feat: Add retry functionality for history tasks.
1 parent f5fa393 commit ec1a0de

7 files changed

Lines changed: 144 additions & 9 deletions

File tree

app.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,7 @@ func (a *App) startHTTPServer() error {
309309
apiGroup.PUT("/tasks/:id/pause", tasksHandler.PauseTask)
310310
apiGroup.PUT("/tasks/:id/resume", tasksHandler.ResumeTask)
311311
apiGroup.PUT("/tasks/:id/cancel", tasksHandler.CancelTask)
312+
apiGroup.POST("/tasks/:id/retry", tasksHandler.RetryTask)
312313
apiGroup.DELETE("/tasks/:id", tasksHandler.DeleteTask)
313314

314315
// Presets

cmd/server/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ func main() {
9090
apiGroup.PUT("/tasks/:id/pause", tasksHandler.PauseTask)
9191
apiGroup.PUT("/tasks/:id/resume", tasksHandler.ResumeTask)
9292
apiGroup.PUT("/tasks/:id/cancel", tasksHandler.CancelTask)
93+
apiGroup.POST("/tasks/:id/retry", tasksHandler.RetryTask)
9394
apiGroup.DELETE("/tasks/:id", tasksHandler.DeleteTask)
9495

9596
// Presets

frontend/src/i18n/translations.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,12 @@ export const translations = {
222222
sourceFileSize: 'Source Size',
223223
outputFileSize: 'Output Size',
224224
compressionRatio: 'Compression Ratio',
225+
retry: 'Retry',
226+
retryTask: 'Retry Task',
227+
retrySuccess: 'Task retry submitted successfully',
228+
retryFailed: 'Failed to retry task',
229+
retrySelected: 'Retry Selected',
230+
retryMultipleSuccess: 'Selected tasks retry submitted successfully',
225231
},
226232
// Settings page
227233
settings: {
@@ -541,6 +547,12 @@ export const translations = {
541547
sourceFileSize: '源文件大小',
542548
outputFileSize: '输出文件大小',
543549
compressionRatio: '压缩比',
550+
retry: '重试',
551+
retryTask: '重试任务',
552+
retrySuccess: '任务重试已提交',
553+
retryFailed: '重试任务失败',
554+
retrySelected: '重试选中',
555+
retryMultipleSuccess: '选中任务重试已提交',
544556
},
545557
// 设置页面
546558
settings: {

frontend/src/lib/api.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,14 @@ class APIClient {
6666
if (!response.ok) throw new Error('Failed to cancel task')
6767
}
6868

69+
async retryTask(id: string): Promise<Task> {
70+
const response = await fetch(`${getAPIBaseURL()}/tasks/${id}/retry`, {
71+
method: 'POST',
72+
})
73+
if (!response.ok) throw new Error('Failed to retry task')
74+
return response.json()
75+
}
76+
6977
// Presets
7078
async getPresets(): Promise<Preset[]> {
7179
const response = await fetch(`${getAPIBaseURL()}/presets`)

frontend/src/lib/mockApi.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,28 @@ class MockAPIClient {
139139
)
140140
}
141141

142+
async retryTask(id: string): Promise<Task> {
143+
await delay(200)
144+
const originalTask = tasks.find(t => t.id === id)
145+
if (!originalTask) throw new Error('Task not found')
146+
147+
const newTask: Task = {
148+
id: `task-mock-${++taskIdCounter}`,
149+
sourceFile: originalTask.sourceFile,
150+
outputFile: '',
151+
status: 'pending' as const,
152+
progress: 0,
153+
speed: 0,
154+
eta: 0,
155+
createdAt: new Date().toISOString(),
156+
sourceFileSize: originalTask.sourceFileSize,
157+
config: originalTask.config,
158+
}
159+
160+
tasks = [...tasks, newTask]
161+
return newTask
162+
}
163+
142164
// Presets
143165
async getPresets(): Promise<Preset[]> {
144166
await delay()

frontend/src/pages/HistoryPage.tsx

Lines changed: 55 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useState } from 'react'
22
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
3-
import { Trash2, CheckCircle2, XCircle, CheckSquare, Square, Terminal } from 'lucide-react'
3+
import { Trash2, CheckCircle2, XCircle, CheckSquare, Square, Terminal, RotateCcw } from 'lucide-react'
44
import { api } from '@/lib/api'
55
import { useApp } from '@/contexts/AppContext'
66
import { Button } from '@/components/ui/button'
@@ -76,6 +76,31 @@ export default function HistoryPage() {
7676
},
7777
})
7878

79+
const retryMutation = useMutation({
80+
mutationFn: (id: string) => api.retryTask(id),
81+
onSuccess: () => {
82+
queryClient.invalidateQueries({ queryKey: ['tasks'] })
83+
showToast(t.history.retrySuccess, 'success')
84+
},
85+
onError: () => {
86+
showToast(t.history.retryFailed, 'error')
87+
},
88+
})
89+
90+
const retryMultipleMutation = useMutation({
91+
mutationFn: async (ids: string[]) => {
92+
await Promise.all(ids.map(id => api.retryTask(id)))
93+
},
94+
onSuccess: () => {
95+
queryClient.invalidateQueries({ queryKey: ['tasks'] })
96+
showToast(t.history.retryMultipleSuccess, 'success')
97+
setSelectedTasks([])
98+
},
99+
onError: () => {
100+
showToast(t.history.retryFailed, 'error')
101+
},
102+
})
103+
79104
const handleDelete = (id: string) => {
80105
setTaskToDelete(id)
81106
setDeleteConfirmOpen(true)
@@ -152,14 +177,25 @@ export default function HistoryPage() {
152177
</div>
153178
<div className="flex gap-2">
154179
{selectedTasks.length > 0 && (
155-
<Button
156-
variant="destructive"
157-
size="sm"
158-
onClick={handleDeleteSelected}
159-
>
160-
<Trash2 className="h-4 w-4 mr-1" />
161-
{t.history.deleteSelected} ({selectedTasks.length})
162-
</Button>
180+
<>
181+
<Button
182+
variant="default"
183+
size="sm"
184+
onClick={() => retryMultipleMutation.mutate(selectedTasks)}
185+
disabled={retryMultipleMutation.isPending}
186+
>
187+
<RotateCcw className={cn("h-4 w-4 mr-1", retryMultipleMutation.isPending && "animate-spin")} />
188+
{t.history.retrySelected} ({selectedTasks.length})
189+
</Button>
190+
<Button
191+
variant="destructive"
192+
size="sm"
193+
onClick={handleDeleteSelected}
194+
>
195+
<Trash2 className="h-4 w-4 mr-1" />
196+
{t.history.deleteSelected} ({selectedTasks.length})
197+
</Button>
198+
</>
163199
)}
164200
{completedTasks.length > 0 && (
165201
<Button
@@ -474,6 +510,16 @@ export default function HistoryPage() {
474510

475511
{/* Actions */}
476512
<div className="pt-3 border-t space-y-2">
513+
<Button
514+
variant="default"
515+
size="sm"
516+
onClick={() => retryMutation.mutate(selectedTask.id)}
517+
disabled={retryMutation.isPending}
518+
className="w-full"
519+
>
520+
<RotateCcw className={cn("h-4 w-4 mr-1", retryMutation.isPending && "animate-spin")} />
521+
{t.history.retryTask}
522+
</Button>
477523
<Button
478524
variant="outline"
479525
size="sm"

internal/api/tasks.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,3 +219,48 @@ func (h *TasksHandler) CancelTask(c *gin.Context) {
219219

220220
c.JSON(http.StatusOK, gin.H{"message": "task cancelled"})
221221
}
222+
223+
// RetryTask handles POST /api/tasks/:id/retry
224+
// Creates a new task based on an existing failed, cancelled, or completed task
225+
func (h *TasksHandler) RetryTask(c *gin.Context) {
226+
id := c.Param("id")
227+
228+
// Get original task
229+
originalTask, err := h.db.GetTask(id)
230+
if err != nil {
231+
c.JSON(http.StatusNotFound, gin.H{"error": "task not found"})
232+
return
233+
}
234+
235+
// Can only retry completed, failed, or cancelled tasks
236+
if originalTask.Status != model.TaskStatusCompleted &&
237+
originalTask.Status != model.TaskStatusFailed &&
238+
originalTask.Status != model.TaskStatusCancelled {
239+
c.JSON(http.StatusBadRequest, gin.H{"error": "can only retry completed, failed, or cancelled tasks"})
240+
return
241+
}
242+
243+
// Create a new task with the same source file and config
244+
newTask := &model.Task{
245+
ID: uuid.New().String(),
246+
SourceFile: originalTask.SourceFile,
247+
OutputFile: "", // Will be set by worker
248+
Status: model.TaskStatusPending,
249+
Progress: 0,
250+
Speed: 0,
251+
ETA: 0,
252+
CreatedAt: time.Now(),
253+
Preset: originalTask.Preset,
254+
Config: originalTask.Config,
255+
}
256+
257+
if err := h.db.CreateTask(newTask); err != nil {
258+
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create task"})
259+
return
260+
}
261+
262+
// Submit task to worker pool
263+
h.pool.SubmitTask(newTask.ID)
264+
265+
c.JSON(http.StatusOK, newTask)
266+
}

0 commit comments

Comments
 (0)