import { db } from './db.js' import { getActiveMasterKey } from './auth.js' import { getLogbookKey } from './logbookKeys.js' import { encryptJson, decryptJson } from './crypto.js' import { syncLogbook } from './sync.js' import { PlausibleEvents, trackPlausibleEvent } from './analytics.js' async function getEncryptionKey(logbookId: string): Promise { const key = await getLogbookKey(logbookId) || getActiveMasterKey() if (!key) throw new Error('Encryption key not found. Please log in.') return key } export async function saveEntryVoiceMemo(options: { logbookId: string entryId: string audioDataUrl: string mimeType: string durationSec: number caption?: string transcribed?: boolean analyticsContext?: string }): Promise { const { logbookId, entryId, audioDataUrl, mimeType, durationSec, caption = '', transcribed = true, analyticsContext = 'logbook' } = options const masterKey = await getEncryptionKey(logbookId) const voiceId = window.crypto.randomUUID() const voicePayload = { audio: audioDataUrl, mimeType, durationSec, caption: caption.trim(), transcribed: !!transcribed } const encrypted = await encryptJson(voicePayload, masterKey) const now = new Date().toISOString() await db.voiceMemos.put({ payloadId: voiceId, entryId, logbookId, encryptedData: encrypted.ciphertext, iv: encrypted.iv, tag: encrypted.tag, updatedAt: now }) await db.syncQueue.put({ action: 'create', type: 'voiceMemo', payloadId: voiceId, logbookId, data: JSON.stringify({ encryptedData: encrypted.ciphertext, iv: encrypted.iv, tag: encrypted.tag, entryId }), updatedAt: now }) trackPlausibleEvent(PlausibleEvents.VOICE_MEMO_UPLOADED, { context: analyticsContext }) syncLogbook(logbookId).catch((err) => console.warn('Background sync failed:', err)) return voiceId } export async function deleteEntryVoiceMemo(logbookId: string, voiceId: string): Promise { const now = new Date().toISOString() await db.voiceMemos.delete(voiceId) await db.syncQueue.put({ action: 'delete', type: 'voiceMemo', payloadId: voiceId, logbookId, data: '', updatedAt: now }) syncLogbook(logbookId).catch((err) => console.warn('Background sync failed:', err)) } /** Deletes the newest voice memo for an entry; returns its id or null. */ export async function removeLastVoiceMemoForEntry( logbookId: string, entryId: string ): Promise { const memos = await db.voiceMemos.where({ entryId }).toArray() if (memos.length === 0) return null memos.sort( (a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime() ) const lastId = memos[0].payloadId await deleteEntryVoiceMemo(logbookId, lastId) return lastId } /** Updates an existing voice memo payload with a new transcript and sets transcribed: true. */ export async function updateVoiceMemoTranscript( logbookId: string, voiceId: string, transcript: string ): Promise { const masterKey = await getEncryptionKey(logbookId) const record = await db.voiceMemos.get(voiceId) if (!record) throw new Error('Voice memo not found') const decrypted = await decryptJson(record.encryptedData, record.iv, record.tag, masterKey) if (!decrypted) throw new Error('Failed to decrypt voice memo') const manualCaption = decrypted.caption ? String(decrypted.caption).trim() : '' const finalCaption = manualCaption ? `${manualCaption}\n(Transkript: ${transcript.trim()})` : transcript.trim() const updatedPayload = { ...decrypted, caption: finalCaption, transcribed: true } const encrypted = await encryptJson(updatedPayload, masterKey) const now = new Date().toISOString() await db.voiceMemos.put({ ...record, encryptedData: encrypted.ciphertext, iv: encrypted.iv, tag: encrypted.tag, updatedAt: now }) await db.syncQueue.put({ action: 'update', type: 'voiceMemo', payloadId: voiceId, logbookId, data: JSON.stringify({ encryptedData: encrypted.ciphertext, iv: encrypted.iv, tag: encrypted.tag, entryId: record.entryId }), updatedAt: now }) syncLogbook(logbookId).catch((err) => console.warn('Background sync failed:', err)) }