{"record":{"id":"70bc4532bdec2d33","repo":"odysseus-dev/odysseus","slug":"server-error-res-status","errorCode":null,"errorMessage":"Server error ' + res.status","messagePattern":"Server error ' \\+ res\\.status","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"warning","filePath":"static/js/chat.js","lineNumber":6266,"sourceCode":"      }\n    }\n\n    if (!msgIds.length || !sessionId) {\n      // No persisted rows to delete (no DB IDs, or no session at all — e.g. an\n      // error output shown before a model was selected, #1428). Just remove the\n      // DOM so the \"x\" works regardless.\n      domToRemove.forEach(el => el.remove());\n      if (uiModule) uiModule.showToast('Message deleted');\n      return;\n    }\n\n    try {\n      const res = await fetch(`${API_BASE}/api/session/${sessionId}/delete-messages`, {\n        method: 'POST',\n        headers: { 'Content-Type': 'application/json' },\n        body: JSON.stringify({ msg_ids: msgIds })\n      });\n      if (!res.ok) throw new Error('Server error ' + res.status);\n      domToRemove.forEach(el => el.remove());\n      if (uiModule) uiModule.showToast('Message deleted');\n    } catch (err) {\n      console.error('Delete failed:', err);\n      if (uiModule) uiModule.showError('Delete failed: ' + err.message);\n    }\n  }\n\n  /**\n   * Edit an AI message inline. Makes the body contentEditable, saves to DB on confirm.\n   */\n  export async function editAIMessage(msgElement) {\n    const body = msgElement.querySelector('.body');\n    if (!body) return;\n\n    const isEditing = body.contentEditable === 'true' || body.contentEditable === 'plaintext-only';\n    if (isEditing) return; // already editing\n","sourceCodeStart":6248,"sourceCodeEnd":6284,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/static/js/chat.js#L6248-L6284","documentation":"HTTP 404 from PUT /{file_id}/vision when the caller is authenticated but neither the file's owner nor an admin — the owner-or-admin gate on the write path. Returned as 404 to avoid leaking other users' files. It is the second 404 on this route: the first (index miss) fires earlier, so reaching this one means the file exists in uploads.json but belongs to someone else.","triggerScenarios":"PUT /api/upload/{id}/vision as user B for a file whose index entry has owner=user A and B is not admin; ownership field changed by a migration so the editor no longer matches.","commonSituations":"Shared workstation/browser profile logged in as a different account; admin edited the file earlier, then a non-admin account tries to save over that edit.","solutions":["Save the OCR edit from the owning account (or an admin) — the UI should only expose the editor to the owner.","Inspect uploads.json to confirm the owner if access should have worked; repair misattributed ownership after user renames/migrations.","Return a clear 'read-only' state in the UI for files the current user doesn't own to prevent the failed save."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"const canEdit = (att, user) => !authConfigured || att.owner === user || user?.isAdmin;\nif (!canEdit(att, currentUser)) renderReadOnly(att);","typeGuard":null,"tryCatchPattern":"const r = await fetch(url, { method: 'PUT', credentials: 'include' });\nif (r.status === 404) showNotEditable(att); // not owner — hide editor","preventionTips":["Only render the OCR editor for files the current user owns","After account switches in shared browsers, refresh attachment ownership state"],"tags":["auth","http-404","ownership","vision"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}