diff --git a/backend/src/routes/tasks.ts b/backend/src/routes/tasks.ts index ea420f73..fc2a6762 100644 --- a/backend/src/routes/tasks.ts +++ b/backend/src/routes/tasks.ts @@ -841,6 +841,56 @@ router.post('/recover-stale', async (req: Request, res: Response) => { } }); +/** + * POST /api/tasks/retry-failed + * Reset failed tasks back to pending for retry + * + * Body: + * - role: string (optional, filter by role) + * - max_age_hours: number (optional, default 24 - only retry tasks from last N hours) + * - limit: number (optional, default 100) + */ +router.post('/retry-failed', async (req: Request, res: Response) => { + try { + const { role, max_age_hours = 24, limit = 100 } = req.body; + + let query = ` + UPDATE worker_tasks + SET status = 'pending', + worker_id = NULL, + claimed_at = NULL, + started_at = NULL, + completed_at = NULL, + error_message = NULL, + retry_count = retry_count + 1, + updated_at = NOW() + WHERE status = 'failed' + AND created_at > NOW() - INTERVAL '${parseInt(max_age_hours)} hours' + `; + const params: any[] = []; + + if (role) { + query += ` AND role = $1`; + params.push(role); + } + + query += ` RETURNING id, role, dispensary_id`; + + const { rows } = await pool.query(query, params); + + console.log(`[Tasks] Retried ${rows.length} failed tasks`); + + res.json({ + success: true, + tasks_retried: rows.length, + tasks: rows.slice(0, 20), // Return first 20 for visibility + }); + } catch (error: unknown) { + console.error('Error retrying failed tasks:', error); + res.status(500).json({ error: 'Failed to retry tasks' }); + } +}); + /** * GET /api/tasks/role/:role/last-completion * Get the last completion time for a role