Add voice memos to live journal and event log.

Record short E2E-encrypted audio attachments from the live log, link them to events via __live:voice markers, and play them back in the stream and chronological event table.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-03 14:52:12 +02:00
parent f83d67b527
commit 975c7a2e40
30 changed files with 1155 additions and 14 deletions
+2
View File
@@ -83,6 +83,7 @@ router.get('/share-pull', async (req: any, res) => {
const logbookVesselSelection = await findLogbookVesselSelectionSafe(logbookId)
const entries = await prisma.entryPayload.findMany({ where: { logbookId } })
const photos = await prisma.photoPayload.findMany({ where: { logbookId } })
const voiceMemos = await prisma.voiceMemoPayload.findMany({ where: { logbookId } })
const gpsTracks = await prisma.gpsTrackPayload.findMany({ where: { logbookId } })
return res.json({
@@ -94,6 +95,7 @@ router.get('/share-pull', async (req: any, res) => {
logbookVesselSelection,
entries,
photos,
voiceMemos,
gpsTracks
})
} catch (error: unknown) {
+20
View File
@@ -143,6 +143,8 @@ router.post('/push', async (req: any, res) => {
await prisma.entryPayload.deleteMany({ where: { logbookId, payloadId } })
} else if (type === 'photo') {
await prisma.photoPayload.deleteMany({ where: { logbookId, payloadId } })
} else if (type === 'voiceMemo') {
await prisma.voiceMemoPayload.deleteMany({ where: { logbookId, payloadId } })
} else if (type === 'gpsTrack') {
await prisma.gpsTrackPayload.deleteMany({ where: { logbookId, entryId: payloadId } })
} else if (type === 'logbookCrew') {
@@ -234,6 +236,22 @@ router.post('/push', async (req: any, res) => {
update: { encryptedData, iv, tag, updatedAt: itemUpdatedAt }
})
}
} else if (type === 'voiceMemo') {
{
const existing = await prisma.voiceMemoPayload.findUnique({
where: { logbookId_payloadId: { logbookId, payloadId } }
})
if (existing && new Date(existing.updatedAt) > itemUpdatedAt) {
results.push({ payloadId, status: 'conflict', reason: 'Server version is newer' })
continue
}
const entryId = parsed.entryId || ''
await prisma.voiceMemoPayload.upsert({
where: { logbookId_payloadId: { logbookId, payloadId } },
create: { logbookId, payloadId, entryId, encryptedData, iv, tag, updatedAt: itemUpdatedAt },
update: { encryptedData, iv, tag, updatedAt: itemUpdatedAt }
})
}
} else if (type === 'gpsTrack') {
{
const existing = await prisma.gpsTrackPayload.findUnique({
@@ -365,6 +383,7 @@ router.get('/pull', async (req: any, res) => {
const crews = await prisma.crewPayload.findMany({ where: { logbookId } })
const entries = await prisma.entryPayload.findMany({ where: { logbookId } })
const photos = await prisma.photoPayload.findMany({ where: { logbookId } })
const voiceMemos = await prisma.voiceMemoPayload.findMany({ where: { logbookId } })
const gpsTracks = await prisma.gpsTrackPayload.findMany({ where: { logbookId } })
const { findLogbookCrewSelectionSafe, findLogbookVesselSelectionSafe } =
await import('../utils/crewPoolSchema.js')
@@ -379,6 +398,7 @@ router.get('/pull', async (req: any, res) => {
logbookVesselSelection,
entries,
photos,
voiceMemos,
gpsTracks
})
} catch (error: any) {