{"record":{"id":"a97e5deb710892a2","repo":"odysseus-dev/odysseus","slug":"failed-to-delete-task","errorCode":null,"errorMessage":"Failed to delete task","messagePattern":"Failed to delete task","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"static/js/tasks.js","lineNumber":98,"sourceCode":"  return await res.json();\n}\n\nasync function _updateTask(id, data) {\n  const res = await fetch(`${API_BASE}/api/tasks/${id}`, {\n    method: 'PUT',\n    credentials: 'same-origin',\n    headers: { 'Content-Type': 'application/json' },\n    body: JSON.stringify(data),\n  });\n  if (!res.ok) throw new Error('Failed to update task');\n  return await res.json();\n}\n\nasync function _deleteTask(id) {\n  const res = await fetch(`${API_BASE}/api/tasks/${id}`, {\n    method: 'DELETE', credentials: 'same-origin',\n  });\n  if (!res.ok) throw new Error('Failed to delete task');\n}\n\nfunction _taskCardById(id) {\n  const safe = (window.CSS && CSS.escape) ? CSS.escape(String(id)) : String(id).replace(/\"/g, '\\\\\"');\n  return document.querySelector(`.task-card[data-id=\"${safe}\"]`);\n}\n\nfunction _animateTaskRemoval(ids) {\n  const cards = ids.map(_taskCardById).filter(Boolean);\n  if (!cards.length) return Promise.resolve();\n  for (const card of cards) {\n    card.style.maxHeight = `${Math.max(card.getBoundingClientRect().height, card.scrollHeight)}px`;\n    card.classList.add('memory-tidy-removing');\n  }\n  return new Promise(resolve => setTimeout(resolve, 520));\n}\n\nfunction _setTaskCardsDeleting(ids, active) {","sourceCodeStart":80,"sourceCodeEnd":116,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/static/js/tasks.js#L80-L116","documentation":"Thrown by _deleteTask (static/js/tasks.js:98) when DELETE /api/tasks/{id} returns non-2xx; generic 'Failed to delete task'. The UI shows a 'Deleting…' busy badge on the card while this runs, so a failure leaves the badge stuck unless the caller clears it. Like update, this route is ownership-gated server-side.","triggerScenarios":"Confirming task deletion: DELETE /api/tasks/{id}. 404 when already deleted (double-confirm, concurrent delete), 403 non-owner, 401 expired session, 500 on persistence error.","commonSituations":"Two tabs both deleting; agent loop removed the task; permission change mid-session; slow server letting the user click delete twice.","solutions":["Treat 404 as deleted — remove the card locally and refresh instead of erroring.","Ensure the busy badge is removed in a finally block so failed deletes do not leave a stuck 'Deleting' state.","Re-login on 401; on 403 refresh the list to reconcile ownership.","Prevent double-fire by disabling the delete control while the request is pending."],"exampleFix":"// before\nif (!res.ok) throw new Error('Failed to delete task');\n// after\nif (res.status === 404) return; // already gone — treat as success\nconst d = await res.json().catch(() => ({}));\nif (!res.ok) throw new Error(d.detail || `Failed to delete task (HTTP ${res.status})`);","handlingStrategy":"try-catch","validationCode":"if (!_taskCardById(id)) { return; // card already gone — nothing to delete visually }","typeGuard":null,"tryCatchPattern":"try { await _deleteTask(id); } catch (err) { /* remove busy badge in finally */ } finally { card.querySelector('.task-card-delete-busy')?.remove(); }","preventionTips":["Treat 404 as success for deletes and remove the card locally.","Always clear the 'Deleting…' badge in a finally block.","Disable the delete control while the request is in flight.","Re-login on 401; reconcile with a list refresh on 403."],"tags":["fetch","api","http","tasks","idempotency"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}