Fix live journal freeze and passkey login on localhost.

Harden live log init with safe per-entry decrypt, stable loading state, and no parallel list scan in live mode. Improve multi-sail picker UX, stop WebAuthn retry after user cancel, redirect 127.0.0.1 to localhost, and tolerate missing appearance prefs table.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-01 08:49:45 +02:00
co-authored by Cursor
parent 43dc994c4f
commit 0caaf681d8
20 changed files with 580 additions and 100 deletions
+6 -1
View File
@@ -12,6 +12,7 @@ import { clearLogbookKeysCache } from './logbookKeys.js'
import { PlausibleEvents, trackPlausibleEvent } from './analytics.js'
import { db } from './db.js'
import { apiFetch, apiJson } from './api.js'
import { isWebAuthnUserAbortError } from '../utils/passkeyHost.js'
const API_BASE = '/api/auth'
@@ -361,7 +362,11 @@ export async function loginUser(username?: string): Promise<LoginResult> {
const prfRequested = !!options.extensions?.prf
try {
credentialResponse = await startAuthentication({ optionsJSON: options })
} catch (err: any) {
} catch (err: unknown) {
// User cancelled or timed out — never open a second platform prompt.
if (isWebAuthnUserAbortError(err)) {
throw err
}
if (prfRequested) {
console.warn('Passkey authentication with PRF extension failed, retrying without PRF:', err)
if (options.extensions) {
+19
View File
@@ -0,0 +1,19 @@
import { describe, expect, it, vi } from 'vitest'
import { tryDecryptEntryPayload } from './quickEventLog.js'
vi.mock('./crypto.js', () => ({
decryptJson: vi.fn(async (_c: string, _i: string, _t: string) => {
throw new Error('decrypt failed')
}),
encryptJson: vi.fn()
}))
describe('tryDecryptEntryPayload', () => {
it('returns null when decryption fails', async () => {
const result = await tryDecryptEntryPayload(
{ encryptedData: 'x', iv: 'y', tag: 'z' },
new ArrayBuffer(32)
)
expect(result).toBeNull()
})
})
+33 -6
View File
@@ -1,6 +1,6 @@
import { db } from './db.js'
import { getActiveMasterKey } from './auth.js'
import { getLogbookKey } from './logbookKeys.js'
import { ensureLogbookKey, getLogbookKey } from './logbookKeys.js'
import { decryptJson, encryptJson } from './crypto.js'
import { syncLogbook } from './sync.js'
import {
@@ -24,12 +24,36 @@ export interface LoadedEntry {
data: Record<string, unknown>
}
type EncryptedRecord = {
encryptedData: string
iv: string
tag: string
}
async function getMasterKey(logbookId: string): Promise<ArrayBuffer> {
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<Record<string, unknown> | null> {
try {
return await decryptJson(record.encryptedData, record.iv, record.tag, key)
} catch {
return null
}
}
function sortEntriesNewestFirst<T extends { updatedAt: string }>(entries: T[]): T[] {
return [...entries].sort(
(a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
)
}
function tankLevelsFromData(data: Record<string, unknown>) {
const fw = (data.freshwater as Record<string, number> | undefined) ?? {
morning: 0, refilled: 0, evening: 0, consumption: 0
@@ -110,7 +134,7 @@ export async function loadEntry(logbookId: string, entryId: string): Promise<Loa
const masterKey = await getMasterKey(logbookId)
const record = await db.entries.get(entryId)
if (!record) return null
const data = await decryptJson(record.encryptedData, record.iv, record.tag, masterKey)
const data = await tryDecryptEntryPayload(record, masterKey)
if (!data) return null
return { payloadId: record.payloadId, updatedAt: record.updatedAt, data }
}
@@ -118,10 +142,10 @@ export async function loadEntry(logbookId: string, entryId: string): Promise<Loa
export async function findTodayEntryId(logbookId: string): Promise<string | null> {
const todayStr = new Date().toISOString().substring(0, 10)
const masterKey = await getMasterKey(logbookId)
const local = await db.entries.where({ logbookId }).toArray()
const local = sortEntriesNewestFirst(await db.entries.where({ logbookId }).toArray())
for (const entry of local) {
const decrypted = await decryptJson(entry.encryptedData, entry.iv, entry.tag, masterKey)
const decrypted = await tryDecryptEntryPayload(entry, masterKey)
if (decrypted && String(decrypted.date) === todayStr) {
return entry.payloadId
}
@@ -135,8 +159,10 @@ export async function createTodayEntry(logbookId: string): Promise<string> {
const decryptedEntries: Array<LogEntryTankSource & TravelDaySortable> = []
for (const entry of localEntries) {
const decrypted = await decryptJson(entry.encryptedData, entry.iv, entry.tag, masterKey)
if (decrypted) decryptedEntries.push(decrypted as LogEntryTankSource & TravelDaySortable)
const decrypted = await tryDecryptEntryPayload(entry, masterKey)
if (decrypted) {
decryptedEntries.push(decrypted as LogEntryTankSource & TravelDaySortable)
}
}
decryptedEntries.sort(compareTravelDaysChronological)
@@ -185,6 +211,7 @@ export async function createTodayEntry(logbookId: string): Promise<string> {
}
export async function findOrCreateTodayEntry(logbookId: string): Promise<string> {
await ensureLogbookKey(logbookId)
const existing = await findTodayEntryId(logbookId)
if (existing) return existing
return createTodayEntry(logbookId)