{"record":{"id":"798ff9c24dfeb69e","repo":"odysseus-dev/odysseus","slug":"note-has-no-checklist-items","errorCode":null,"errorMessage":"Note has no checklist items","messagePattern":"Note has no checklist items","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"warning","filePath":"routes/note/note_routes.py","lineNumber":830,"sourceCode":"            return {\"ok\": True, \"archived\": note.archived}\n        finally:\n            db.close()\n\n    # --- TOGGLE CHECKLIST ITEM ---\n    @router.post(\"/{note_id}/items/{index}/toggle\")\n    def toggle_item(request: Request, note_id: str, index: int):\n        user = _owner(request)\n        db = SessionLocal()\n        try:\n            note = db.query(Note).filter(Note.id == note_id).first()\n            if not note:\n                raise HTTPException(404, \"Note not found\")\n            # SECURITY: strict ownership — previously `note.owner and note.owner != user`\n            # let any user touch a row whose owner field was null/empty.\n            if user is not None and note.owner != user:\n                raise HTTPException(404, \"Note not found\")\n            if not note.items:\n                raise HTTPException(400, \"Note has no checklist items\")\n            items = json.loads(note.items)\n            if index < 0 or index >= len(items):\n                raise HTTPException(400, f\"Item index {index} out of range\")\n            items[index][\"done\"] = not items[index].get(\"done\", False)\n            note.items = json.dumps(items)\n            flag_modified(note, \"items\")\n            db.commit()\n            return {\"ok\": True, \"items\": items}\n        finally:\n            db.close()\n\n    # --- FIRE REMINDER ---\n    @router.post(\"/fire-reminder\")\n    async def fire_reminder(request: Request):\n        \"\"\"Dispatch a reminder according to user settings.\n\n        Called by the frontend when a reminder fires. Optionally generates an\n        LLM synthesis line and/or sends an email through configured SMTP.","sourceCodeStart":812,"sourceCodeEnd":848,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/routes/note/note_routes.py#L812-L848","documentation":"400 raised at routes/note/note_routes.py:830 in POST /notes/{note_id}/items/{index}/toggle when the note exists (and is owned by the caller) but `note.items` is falsy — null, empty string, or an empty JSON payload. The route toggles checklist items, so there is nothing to toggle. Note the next guard: index out of range is a separate 400, and a non-JSON items column would raise json.JSONDecodeError instead.","triggerScenarios":"Calling the toggle on a plain text/image note that was never given a checklist. Toggling an item after the note's items were cleared client-side (items set to null/[] via PUT). Race where a PUT removes items while a checklist toggle is in flight.","commonSituations":"UI rendering a toggle control for notes without checklists (missing conditional on note type). Converting a checklist note to a text note then clicking a stale toggle. Scripts assuming every note has items.","solutions":["Only show/offer the toggle when the note actually has checklist items (non-empty items array).","If the checklist was cleared by mistake, re-add items via PUT ({\"items\": [{\"text\": \"...\", \"done\": false}]}) then toggle.","Client-side, guard: if (!note.items?.length) skip/disable the toggle.","Handle races by re-fetching the note on 400 and reconciling UI state."],"exampleFix":"// before\n<button onClick={() => api.toggleItem(note.id, i)} />\n\n// after\n{(note.items?.length ?? 0) > 0\n  ? <button onClick={() => api.toggleItem(note.id, i)} />\n  : null}","handlingStrategy":"type-guard","validationCode":"const items = (() => { try { return JSON.parse(note.items ?? 'null'); } catch { return null; } })();\nif (!Array.isArray(items) || items.length === 0) { disableToggle(); return; }\nif (index < 0 || index >= items.length) { disableToggle(); return; }","typeGuard":"interface ChecklistItem { text?: string; done?: boolean }\nfunction isChecklistNote(n: Note): n is Note & { items: ChecklistItem[] } {\n  try { const v = JSON.parse(n.items ?? 'null'); return Array.isArray(v) && v.length > 0; }\n  catch { return false; }\n}","tryCatchPattern":"try { await api.toggleItem(noteId, index); }\ncatch (e) {\n  if (e.status === 400 && /checklist/i.test(e.message)) { hideChecklistUI(noteId); return; }\n  throw e;\n}","preventionTips":["Render toggle controls only when the note has a non-empty items array.","When clearing items via PUT, also tear down any live checklist listeners.","On 400, re-fetch the note and reconcile UI before any retry."],"tags":["fastapi","http-400","notes","checklist","empty-state"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}