Extend live journal with weather, tanks, undo, and event series stats.
Adds weather and course quick actions, diesel/water refills, five-second undo, foreground auto-position every three hours, and chronological pressure/wind/motor series in the stats tab. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
import { db } from './db.js'
|
||||
import { getActiveMasterKey } from './auth.js'
|
||||
import { getLogbookKey } from './logbookKeys.js'
|
||||
import { decryptJson } from './crypto.js'
|
||||
import { compareTravelDaysChronological } from '../utils/logEntryTankLevels.js'
|
||||
import type { LogEventPayload } from '../utils/logEntryPayload.js'
|
||||
import { LIVE_EVENT_CODES } from '../utils/liveEventCodes.js'
|
||||
|
||||
export interface EventSeriesPoint {
|
||||
entryId: string
|
||||
date: string
|
||||
dayOfTravel: string
|
||||
time: string
|
||||
summary: string
|
||||
}
|
||||
|
||||
export interface EventSeriesSummary {
|
||||
pressure: EventSeriesPoint[]
|
||||
wind: EventSeriesPoint[]
|
||||
motor: EventSeriesPoint[]
|
||||
}
|
||||
|
||||
function sortPoints(points: EventSeriesPoint[]): EventSeriesPoint[] {
|
||||
return [...points].sort((a, b) => {
|
||||
const dateCompare = a.date.localeCompare(b.date)
|
||||
if (dateCompare !== 0) return dateCompare
|
||||
return a.time.localeCompare(b.time)
|
||||
})
|
||||
}
|
||||
|
||||
export async function loadLogbookEventSeries(logbookId: string): Promise<EventSeriesSummary> {
|
||||
const masterKey = await getLogbookKey(logbookId) || getActiveMasterKey()
|
||||
if (!masterKey) throw new Error('Encryption key not found. Please log in.')
|
||||
|
||||
const local = await db.entries.where({ logbookId }).toArray()
|
||||
const decryptedEntries: Array<{
|
||||
entryId: string
|
||||
date: string
|
||||
dayOfTravel: string
|
||||
events: LogEventPayload[]
|
||||
}> = []
|
||||
|
||||
for (const entry of local) {
|
||||
const decrypted = await decryptJson(entry.encryptedData, entry.iv, entry.tag, masterKey)
|
||||
if (!decrypted) continue
|
||||
decryptedEntries.push({
|
||||
entryId: entry.payloadId,
|
||||
date: String(decrypted.date || ''),
|
||||
dayOfTravel: String(decrypted.dayOfTravel || ''),
|
||||
events: (decrypted.events as LogEventPayload[]) || []
|
||||
})
|
||||
}
|
||||
|
||||
decryptedEntries.sort((a, b) =>
|
||||
compareTravelDaysChronological(
|
||||
{ date: a.date, dayOfTravel: a.dayOfTravel },
|
||||
{ date: b.date, dayOfTravel: b.dayOfTravel }
|
||||
)
|
||||
)
|
||||
|
||||
const pressure: EventSeriesPoint[] = []
|
||||
const wind: EventSeriesPoint[] = []
|
||||
const motor: EventSeriesPoint[] = []
|
||||
|
||||
for (const entry of decryptedEntries) {
|
||||
for (const event of entry.events) {
|
||||
const base = {
|
||||
entryId: entry.entryId,
|
||||
date: entry.date,
|
||||
dayOfTravel: entry.dayOfTravel,
|
||||
time: event.time
|
||||
}
|
||||
|
||||
if (event.windPressure.trim()) {
|
||||
pressure.push({
|
||||
...base,
|
||||
summary: `${event.windPressure} hPa`
|
||||
})
|
||||
}
|
||||
|
||||
if (event.windDirection.trim() || event.windStrength.trim()) {
|
||||
wind.push({
|
||||
...base,
|
||||
summary: [event.windDirection, event.windStrength].filter(Boolean).join(' ')
|
||||
})
|
||||
}
|
||||
|
||||
const code = event.remarks.trim()
|
||||
if (
|
||||
code === LIVE_EVENT_CODES.MOTOR_START ||
|
||||
code === LIVE_EVENT_CODES.MOTOR_STOP
|
||||
) {
|
||||
motor.push({
|
||||
...base,
|
||||
summary: code === LIVE_EVENT_CODES.MOTOR_START ? 'start' : 'stop'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
pressure: sortPoints(pressure),
|
||||
wind: sortPoints(wind),
|
||||
motor: sortPoints(motor)
|
||||
}
|
||||
}
|
||||
@@ -47,6 +47,8 @@ function buildEncryptedPayload(
|
||||
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<string, unknown> {
|
||||
@@ -56,23 +58,26 @@ function buildEncryptedPayload(
|
||||
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: {
|
||||
morning: fw.morning || 0,
|
||||
refilled: fw.refilled || 0,
|
||||
evening: fw.evening || 0,
|
||||
consumption: fw.consumption ?? 0
|
||||
},
|
||||
fuel: {
|
||||
morning: fuel.morning || 0,
|
||||
refilled: fuel.refilled || 0,
|
||||
evening: fuel.evening || 0,
|
||||
consumption: fuel.consumption ?? 0
|
||||
},
|
||||
freshwater,
|
||||
fuel: fuelLevels,
|
||||
greywater: gw ? { level: gw.level || 0 } : undefined,
|
||||
trackDistanceNm:
|
||||
trackDistance != null && trackDistance !== ''
|
||||
@@ -207,13 +212,28 @@ export async function appendQuickEvent(
|
||||
})
|
||||
const nextEvents = sortLogEventsByTime([...currentEvents, newEvent])
|
||||
|
||||
const entryData = buildEncryptedPayload(loaded.data, {
|
||||
await persistEntry(logbookId, entryId, loaded.data, {
|
||||
events: nextEvents,
|
||||
departure: headerPatch?.departure,
|
||||
destination: headerPatch?.destination,
|
||||
clearSignatures: hadSignature
|
||||
})
|
||||
|
||||
return { events: nextEvents, hadSignature }
|
||||
}
|
||||
|
||||
async function persistEntry(
|
||||
logbookId: string,
|
||||
entryId: string,
|
||||
data: Record<string, unknown>,
|
||||
options: Parameters<typeof buildEncryptedPayload>[1]
|
||||
): Promise<void> {
|
||||
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()
|
||||
@@ -237,6 +257,65 @@ export async function appendQuickEvent(
|
||||
})
|
||||
|
||||
syncLogbook(logbookId).catch((err) => console.warn('Background sync failed:', err))
|
||||
}
|
||||
|
||||
export async function removeLastEvent(
|
||||
logbookId: string,
|
||||
entryId: string
|
||||
): Promise<LogEventPayload[]> {
|
||||
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<LogEventPayload>
|
||||
): Promise<AppendQuickEventResult> {
|
||||
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 }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user