{"record":{"id":"83b1e626b7c38181","repo":"Zackriya-Solutions/meetily","slug":"no-meeting-id-received-from-save-operation","errorCode":null,"errorMessage":"No meeting ID received from save operation","messagePattern":"No meeting ID received from save operation","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"frontend/src/hooks/useRecordingStop.ts","lineNumber":265,"sourceCode":"        console.log('💾 Saving COMPLETE transcripts to database...', {\n          transcript_count: freshTranscripts.length,\n          meeting_name: savedMeetingName || meetingTitle,\n          folder_path: folderPath,\n          sample_text: freshTranscripts.length > 0 ? freshTranscripts[0].text.substring(0, 50) + '...' : 'none',\n          last_transcript: freshTranscripts.length > 0 ? freshTranscripts[freshTranscripts.length - 1].text.substring(0, 30) + '...' : 'none',\n        });\n\n        try {\n          const responseData = await storageService.saveMeeting(\n            savedMeetingName || meetingTitle || 'New Meeting',  // PREFER savedMeetingName (backend source)\n            freshTranscripts,\n            folderPath\n          );\n\n          const meetingId = responseData.meeting_id;\n          if (!meetingId) {\n            console.error('No meeting_id in response:', responseData);\n            throw new Error('No meeting ID received from save operation');\n          }\n\n          let shouldDetectSummaryLanguage = false;\n          try {\n            shouldDetectSummaryLanguage = !(await applyPinnedSummaryLanguageToMeeting(meetingId));\n          } catch (error) {\n            console.warn('Failed to apply pinned summary language preference for new meeting:', error);\n            toast.warning('Could not apply default summary language', {\n              description: 'The meeting was saved, but the default summary language was not applied.',\n            });\n          }\n\n          if (shouldDetectSummaryLanguage) {\n            try {\n              await detectAndCacheSummaryLanguage(\n                meetingId,\n                freshTranscripts.map(t => t.text)\n              );","sourceCodeStart":247,"sourceCodeEnd":283,"githubUrl":"https://github.com/Zackriya-Solutions/meetily/blob/0281737d87d26352fb0adc78c8c0975f691b23d1/frontend/src/hooks/useRecordingStop.ts#L247-L283","documentation":"After storageService.saveMeeting — which invokes the Tauri command api_save_transcript — the code reads responseData.meeting_id and throws when it is falsy. The TypeScript SaveMeetingResponse contract expects a snake_case meeting_id string, so this fires whenever the Rust command returns a body lacking that exact key: a serde rename_all = \"camelCase\" on the Rust response struct (yielding meetingId), a command returning a different shape, or a stale build registering an older api_save_transcript. The meeting may actually be saved in SQLite; only the returned ID is missing, which then breaks every follow-up step (language pinning, post-processing).","triggerScenarios":"api_save_transcript resolves successfully but the response object has no meeting_id: Rust struct uses #[serde(rename_all = \"camelCase\"]), the command returns Ok(()) or a different struct, the field is Option and serialized as null, or frontend and Rust binaries are from different builds during development.","commonSituations":"Frontend/Rust contract drift during refactors; running pnpm dev against a stale compiled Tauri binary; a DB layer change that returns a success envelope without the generated row ID.","solutions":["Inspect the console.error output of responseData and compare its keys with the TS SaveMeetingResponse type — a meetingId vs meeting_id casing mismatch is the most common cause.","Check the Rust api_save_transcript return type: it must serialize snake_case meeting_id — remove #[serde(rename_all = \"camelCase\")] from the response struct or pin the field with #[serde(rename = \"meeting_id\")].","Rebuild the Tauri app (pnpm run tauri:dev) so the registered command matches the frontend contract; stale dev builds are a frequent cause.","Add a regression test that calls api_save_transcript and asserts response.meeting_id is a non-empty string."],"exampleFix":"// before (Rust response struct)\n#[derive(Serialize)]\n#[serde(rename_all = \"camelCase\")] // meeting_id serializes as meetingId -> TS reads undefined\npub struct SaveTranscriptResponse { pub meeting_id: String }\n\n// after — field serializes as meeting_id, matching SaveMeetingResponse\n#[derive(Serialize)]\npub struct SaveTranscriptResponse { pub meeting_id: String }","handlingStrategy":"type-guard","validationCode":null,"typeGuard":"interface SaveMeetingResponse { meeting_id: string }\nfunction hasMeetingId(r: unknown): r is SaveMeetingResponse {\n  return typeof r === 'object' && r !== null\n    && typeof (r as Record<string, unknown>).meeting_id === 'string'\n    && (r as Record<string, unknown>).meeting_id !== '';\n}","tryCatchPattern":"try {\n  const responseData = await storageService.saveMeeting(title, transcripts, folderPath);\n  if (!hasMeetingId(responseData)) {\n    console.error('Malformed saveMeeting response:', responseData);\n    // audio was already written to disk — run cleanup/recovery, don't abort silently\n  }\n} catch (e) {\n  throw e;\n}","preventionTips":["Keep the TS SaveMeetingResponse interface in lockstep with the Rust response struct's serde attributes.","Add an integration test asserting api_save_transcript returns a non-empty meeting_id.","Never run a new frontend against a stale compiled Rust binary — rebuild both sides together."],"tags":["tauri","ipc","serde","response-shape","meeting-save"],"backgroundTag":"api-response-shape-mismatch","analyzedSha":"0281737d87d26352fb0adc78c8c0975f691b23d1","analyzedAt":"2026-08-16T20:57:52.567Z","schemaVersion":2},"datasetVersion":"2026-08-16T23:17:17.608Z"}