Fix live journal hang on empty new logbooks.

Fast-path today's entry creation, add init timeout, defer auto-position GPS, and migrate logbook keys when the server returns a different id.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-01 09:39:51 +02:00
parent d6c7952af8
commit c1ecdcad9c
3 changed files with 72 additions and 17 deletions
+50 -10
View File
@@ -87,8 +87,26 @@ type LiveModal =
const AUTO_POSITION_INTERVAL_MS = 3 * 60 * 60 * 1000
const AUTO_POSITION_CHECK_MS = 60_000
const AUTO_POSITION_START_DELAY_MS = 3000
const LIVE_LOG_INIT_TIMEOUT_MS = 25_000
const UNDO_TIMEOUT_MS = 5000
function withTimeout<T>(promise: Promise<T>, ms: number, message: string): Promise<T> {
return new Promise((resolve, reject) => {
const timer = window.setTimeout(() => reject(new Error(message)), ms)
promise.then(
(value) => {
window.clearTimeout(timer)
resolve(value)
},
(err) => {
window.clearTimeout(timer)
reject(err)
}
)
})
}
function hapticPulse() {
navigator.vibrate?.(40)
}
@@ -186,13 +204,27 @@ export default function LiveLogView({
const seq = ++initSeqRef.current
setLoading(true)
setError(null)
setEntryId(null)
setEvents([])
setYachtSails([])
if (!logbookId.trim()) {
setError(t('logs.live_load_error'))
setLoading(false)
return
}
try {
const id = await findOrCreateTodayEntry(logbookId)
const id = await withTimeout(
findOrCreateTodayEntry(logbookId),
LIVE_LOG_INIT_TIMEOUT_MS,
t('logs.live_load_error')
)
if (seq !== initSeqRef.current) return
setEntryId(id)
const masterKey = await getLogbookKey(logbookId) || getActiveMasterKey()
if (masterKey) {
const logbookKey = await getLogbookKey(logbookId)
if (logbookKey) {
const yacht = await db.yachts.get(logbookId)
if (yacht) {
try {
@@ -200,7 +232,7 @@ export default function LiveLogView({
yacht.encryptedData,
yacht.iv,
yacht.tag,
masterKey
logbookKey
)
if (decrypted?.sails && Array.isArray(decrypted.sails)) {
setYachtSails(decrypted.sails as string[])
@@ -216,18 +248,18 @@ export default function LiveLogView({
if (loaded) {
applyLoadedEntry(loaded)
} else {
throw new Error(i18n.t('logs.live_load_error'))
throw new Error(t('logs.live_load_error'))
}
} catch (err: unknown) {
if (seq !== initSeqRef.current) return
console.error('Failed to init live log:', err)
setError(err instanceof Error ? err.message : i18n.t('logs.live_load_error'))
setError(err instanceof Error ? err.message : t('logs.live_load_error'))
} finally {
if (seq === initSeqRef.current) {
setLoading(false)
}
}
}, [logbookId, applyLoadedEntry])
}, [logbookId, applyLoadedEntry, t])
useEffect(() => {
void runInit()
@@ -263,7 +295,7 @@ export default function LiveLogView({
autoPositionBusyRef.current = true
try {
const coords = await getCurrentPosition()
const coords = await getCurrentPosition(8000)
await appendQuickEvent(logbookId, entryId, {
gpsLat: coords.lat,
gpsLng: coords.lng,
@@ -277,8 +309,16 @@ export default function LiveLogView({
}
}
const interval = window.setInterval(() => void maybeAutoPosition(), AUTO_POSITION_CHECK_MS)
return () => window.clearInterval(interval)
let intervalRef: number | undefined
const startTimer = window.setTimeout(() => {
void maybeAutoPosition()
intervalRef = window.setInterval(() => void maybeAutoPosition(), AUTO_POSITION_CHECK_MS)
}, AUTO_POSITION_START_DELAY_MS)
return () => {
window.clearTimeout(startTimer)
if (intervalRef !== undefined) window.clearInterval(intervalRef)
}
}, [entryId, loading, logbookId, refreshEntry, busy])
const runQuickAction = async (
+4
View File
@@ -214,6 +214,10 @@ export async function createLogbook(title: string): Promise<DecryptedLogbook> {
if (response.ok) {
const serverLb = await response.json()
if (serverLb.id !== localId) {
await saveLogbookKey(serverLb.id, logbookKey)
await db.logbookKeys.delete(localId)
}
await db.logbooks.put({
id: serverLb.id,
encryptedTitle: serverLb.encryptedTitle,
+14 -3
View File
@@ -158,12 +158,14 @@ export async function createTodayEntry(logbookId: string): Promise<string> {
const localEntries = await db.entries.where({ logbookId }).toArray()
const decryptedEntries: Array<LogEntryTankSource & TravelDaySortable> = []
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
@@ -211,10 +213,19 @@ export async function createTodayEntry(logbookId: string): Promise<string> {
}
export async function findOrCreateTodayEntry(logbookId: string): Promise<string> {
await ensureLogbookKey(logbookId)
const existing = await findTodayEntryId(logbookId)
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(logbookId)
return createTodayEntry(id)
}
export interface AppendQuickEventResult {