import { db } from './db.js' import { getActiveMasterKey } from './auth.js' import { ensureLogbookKey, getLogbookKey } from './logbookKeys.js' import { decryptJson, encryptJson } from './crypto.js' import { syncLogbook } from './sync.js' import { buildLogEntryPayload, normalizeLogEvent, sortLogEventsByTime, currentLocalTimeHHMM, type LogEventPayload } from '../utils/logEntryPayload.js' import { carryOverFromPreviousDay, compareTravelDaysChronological, getNextTravelDayNumber, type LogEntryTankSource, type TravelDaySortable } from '../utils/logEntryTankLevels.js' export interface LoadedEntry { payloadId: string updatedAt: string data: Record } type EncryptedRecord = { encryptedData: string iv: string tag: string } async function getMasterKey(logbookId: string): Promise { const masterKey = await getLogbookKey(logbookId) || getActiveMasterKey() if (!masterKey) throw new Error('Encryption key not found. Please log in.') return masterKey } /** Decrypt one record; skip corrupt or legacy entries instead of aborting the whole scan. */ export async function tryDecryptEntryPayload( record: EncryptedRecord, key: ArrayBuffer ): Promise | null> { try { return await decryptJson(record.encryptedData, record.iv, record.tag, key) } catch { return null } } function sortEntriesNewestFirst(entries: T[]): T[] { return [...entries].sort( (a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime() ) } function tankLevelsFromData(data: Record) { const fw = (data.freshwater as Record | undefined) ?? { morning: 0, refilled: 0, evening: 0, consumption: 0 } const fuel = (data.fuel as Record | undefined) ?? { morning: 0, refilled: 0, evening: 0, consumption: 0 } const gw = data.greywater as { level?: number } | undefined return { fw, fuel, gw } } function buildEncryptedPayload( data: Record, options: { events: LogEventPayload[] departure?: string destination?: string freshwater?: { morning: number; refilled: number; evening: number; consumption: number } fuel?: { morning: number; refilled: number; evening: number; consumption: number } clearSignatures?: boolean } ): Record { const { fw, fuel, gw } = tankLevelsFromData(data) const trackDistance = data.trackDistanceNm const trackSpeedMax = data.trackSpeedMaxKn const trackSpeedAvg = data.trackSpeedAvgKn const motorHoursRaw = data.motorHours const freshwater = options.freshwater ?? { morning: fw.morning || 0, refilled: fw.refilled || 0, evening: fw.evening || 0, consumption: fw.consumption ?? 0 } const fuelLevels = options.fuel ?? { morning: fuel.morning || 0, refilled: fuel.refilled || 0, evening: fuel.evening || 0, consumption: fuel.consumption ?? 0 } const payload = buildLogEntryPayload({ date: String(data.date || ''), dayOfTravel: String(data.dayOfTravel || ''), departure: options.departure ?? String(data.departure || ''), destination: options.destination ?? String(data.destination || ''), freshwater, fuel: fuelLevels, greywater: gw ? { level: gw.level || 0 } : undefined, trackDistanceNm: trackDistance != null && trackDistance !== '' ? parseFloat(String(trackDistance)) : undefined, trackSpeedMaxKn: trackSpeedMax != null && trackSpeedMax !== '' ? parseFloat(String(trackSpeedMax)) : undefined, trackSpeedAvgKn: trackSpeedAvg != null && trackSpeedAvg !== '' ? parseFloat(String(trackSpeedAvg)) : undefined, motorHours: motorHoursRaw != null && motorHoursRaw !== '' ? parseFloat(String(motorHoursRaw)) : undefined, events: options.events }) const clear = options.clearSignatures return { ...payload, signSkipper: clear ? '' : (data.signSkipper ?? ''), signCrew: clear ? '' : (data.signCrew ?? '') } } export async function loadEntry(logbookId: string, entryId: string): Promise { const masterKey = await getMasterKey(logbookId) const record = await db.entries.get(entryId) if (!record) return null const data = await tryDecryptEntryPayload(record, masterKey) if (!data) return null return { payloadId: record.payloadId, updatedAt: record.updatedAt, data } } export async function findTodayEntryId(logbookId: string): Promise { const todayStr = new Date().toISOString().substring(0, 10) const masterKey = await getMasterKey(logbookId) const local = sortEntriesNewestFirst(await db.entries.where({ logbookId }).toArray()) for (const entry of local) { const decrypted = await tryDecryptEntryPayload(entry, masterKey) if (decrypted && String(decrypted.date) === todayStr) { return entry.payloadId } } return null } export async function createTodayEntry(logbookId: string): Promise { const masterKey = await getMasterKey(logbookId) const localEntries = await db.entries.where({ logbookId }).toArray() const decryptedEntries: Array = [] if (localEntries.length > 0) { for (const entry of localEntries) { const decrypted = await tryDecryptEntryPayload(entry, masterKey) if (decrypted) { decryptedEntries.push(decrypted as LogEntryTankSource & TravelDaySortable) } } } decryptedEntries.sort(compareTravelDaysChronological) const previousEntry = decryptedEntries.at(-1) ?? null const { freshwater, fuel, greywaterLevel, departure } = carryOverFromPreviousDay(previousEntry) const localId = window.crypto.randomUUID() const nowStr = new Date().toISOString() const todayStr = nowStr.substring(0, 10) const initialPayload = { date: todayStr, dayOfTravel: getNextTravelDayNumber(decryptedEntries), departure, destination: '', freshwater, fuel, ...(greywaterLevel > 0 ? { greywater: { level: greywaterLevel } } : {}), signSkipper: '', signCrew: '', events: [] } const encrypted = await encryptJson(initialPayload, masterKey) await db.entries.put({ payloadId: localId, logbookId, encryptedData: encrypted.ciphertext, iv: encrypted.iv, tag: encrypted.tag, updatedAt: nowStr }) await db.syncQueue.put({ action: 'create', type: 'entry', payloadId: localId, logbookId, data: JSON.stringify(encrypted), updatedAt: nowStr }) syncLogbook(logbookId).catch((err) => console.warn('Background sync failed:', err)) return localId } export async function findOrCreateTodayEntry(logbookId: string): Promise { const id = logbookId.trim() if (!id) throw new Error('Logbook id required') await ensureLogbookKey(id) const entryCount = await db.entries.where({ logbookId: id }).count() if (entryCount === 0) { return createTodayEntry(id) } const existing = await findTodayEntryId(id) if (existing) return existing return createTodayEntry(id) } export interface AppendQuickEventResult { events: LogEventPayload[] hadSignature: boolean } export async function appendQuickEvent( logbookId: string, entryId: string, partialEvent: Partial, headerPatch?: { departure?: string; destination?: string } ): Promise { const loaded = await loadEntry(logbookId, entryId) if (!loaded) throw new Error('Entry not found') const hadSignature = !!(loaded.data.signSkipper || loaded.data.signCrew) const currentEvents = (loaded.data.events as LogEventPayload[]) || [] const newEvent = normalizeLogEvent({ time: currentLocalTimeHHMM(), ...partialEvent }) const nextEvents = sortLogEventsByTime([...currentEvents, newEvent]) await persistEntry(logbookId, entryId, loaded.data, { events: nextEvents, departure: headerPatch?.departure, destination: headerPatch?.destination, clearSignatures: hadSignature }) return { events: nextEvents, hadSignature } } /** Append multiple events in one load/encrypt/persist cycle (avoids UI freezes). */ export async function appendQuickEvents( logbookId: string, entryId: string, partialEvents: Partial[] ): Promise { const loaded = await loadEntry(logbookId, entryId) if (!loaded) throw new Error('Entry not found') const hadSignature = !!(loaded.data.signSkipper || loaded.data.signCrew) const currentEvents = (loaded.data.events as LogEventPayload[]) || [] if (partialEvents.length === 0) { return { events: currentEvents, hadSignature } } const time = currentLocalTimeHHMM() const newEvents = partialEvents.map((partial) => normalizeLogEvent({ time, ...partial }) ) const nextEvents = sortLogEventsByTime([...currentEvents, ...newEvents]) await persistEntry(logbookId, entryId, loaded.data, { events: nextEvents, clearSignatures: hadSignature }) return { events: nextEvents, hadSignature } } async function persistEntry( logbookId: string, entryId: string, data: Record, options: Parameters[1] ): Promise { const hadSignature = !!(data.signSkipper || data.signCrew) const entryData = buildEncryptedPayload(data, { ...options, clearSignatures: options.clearSignatures ?? hadSignature }) const masterKey = await getMasterKey(logbookId) const encrypted = await encryptJson(entryData, masterKey) const now = new Date().toISOString() await db.entries.put({ payloadId: entryId, logbookId, encryptedData: encrypted.ciphertext, iv: encrypted.iv, tag: encrypted.tag, updatedAt: now }) await db.syncQueue.put({ action: 'update', type: 'entry', payloadId: entryId, logbookId, data: JSON.stringify(encrypted), updatedAt: now }) syncLogbook(logbookId).catch((err) => console.warn('Background sync failed:', err)) } export async function removeLastEvent( logbookId: string, entryId: string ): Promise { const loaded = await loadEntry(logbookId, entryId) if (!loaded) throw new Error('Entry not found') const currentEvents = (loaded.data.events as LogEventPayload[]) || [] if (currentEvents.length === 0) return [] const nextEvents = sortLogEventsByTime(currentEvents.slice(0, -1)) await persistEntry(logbookId, entryId, loaded.data, { events: nextEvents }) return nextEvents } export async function appendTankRefill( logbookId: string, entryId: string, tank: 'fuel' | 'freshwater', addLiters: number, event: Partial ): Promise { const loaded = await loadEntry(logbookId, entryId) if (!loaded) throw new Error('Entry not found') const { fw, fuel } = tankLevelsFromData(loaded.data) const currentEvents = (loaded.data.events as LogEventPayload[]) || [] const newEvent = normalizeLogEvent({ time: currentLocalTimeHHMM(), ...event }) const nextEvents = sortLogEventsByTime([...currentEvents, newEvent]) const tankPatch = tank === 'fuel' ? { fuel: { morning: fuel.morning || 0, refilled: (fuel.refilled || 0) + addLiters, evening: fuel.evening || 0, consumption: fuel.consumption ?? 0 } } : { freshwater: { morning: fw.morning || 0, refilled: (fw.refilled || 0) + addLiters, evening: fw.evening || 0, consumption: fw.consumption ?? 0 } } const hadSignature = !!(loaded.data.signSkipper || loaded.data.signCrew) await persistEntry(logbookId, entryId, loaded.data, { events: nextEvents, ...tankPatch, clearSignatures: hadSignature }) return { events: nextEvents, hadSignature } }