fix(offline): a missing driving-settings row no longer discards the trip bundle

syncTrip read the roadtrip preferences before writing anything and rethrew
everything but a 404. That read has an offline fallback which throws a bare
Error when there is no cached row, which is the state a first sync is in and
the state a logout leaves behind, so the whole downloaded bundle for that trip
was dropped while the run still counted it. Settings reported "N trips stored"
over an empty database, and the trips were gone the moment the connection was.
The read moved to the end and is never fatal now: it belongs to an optional
addon and the addon answers 404 when it is off.

Two things that fed it, from the same trace. A sync run started on
navigator.onLine alone, so work-offline downloaded over the real connection
while every repo read answered from cache. And work-offline itself survived a
logout, in localStorage, while step six of the same logout deleted the database
it reads from, so the next account came up believing it was offline with
nothing to be offline with.

Also in this pass: deleting a hotel booking released its day stop after
deleting the booking row, which now clears the stop's pointer by trigger. The
release goes first, or the stop stands with nothing left to reach it.
This commit is contained in:
Maurice
2026-09-13 22:37:39 +02:00
parent c42cf5477b
commit 89bae414ff
4 changed files with 59 additions and 15 deletions
+14 -4
View File
@@ -33,6 +33,7 @@ import {
import { prefetchPlacesForTrip } from './placePrefetcher'
import { prefetchTilesForTrip } from './tilePrefetcher'
import { isAuthed } from './authGate'
import { isEffectivelyOffline } from './networkMode'
import { getOfflinePrefs, isTripOfflineEnabled, isTripPinned } from './offlinePrefs'
import { useSettingsStore } from '../store/settingsStore'
import type { Trip, Day, Place, PackingItem, TodoItem, BudgetItem, Reservation, TripFile, Accommodation, TripMember } from '../types'
@@ -115,9 +116,6 @@ function isVideo(file: TripFile): boolean {
async function syncTrip(tripId: number): Promise<void> {
const bundle = await tripsApi.bundle(tripId) as TripBundle
await roadtripPreferencesRepo.read(tripId).catch((error: unknown) => {
if ((error as { response?: { status?: number } }).response?.status !== 404) throw error
})
await upsertTrip(bundle.trip)
await upsertDays(bundle.days)
await upsertPlaces(bundle.places)
@@ -144,6 +142,14 @@ async function syncTrip(tripId: number): Promise<void> {
tilesBbox: null,
filesCachedCount: 0,
})
// Last, and never fatal. Driving settings belong to an optional addon: the
// route answers 404 when it is off, and the repo's own offline fallback throws
// a bare Error when there is no cached row yet, which is exactly the state a
// first sync is in. Ahead of the writes and rethrowing anything but a 404, it
// threw the whole downloaded bundle away for that trip while the run still
// reported it stored, so Settings said "N trips ready" over an empty database.
await roadtripPreferencesRepo.read(tripId).catch(() => { /* optional addon, optional cache */ })
}
/** Cache non-photo file blobs for a trip. Fire-and-forget safe. */
@@ -222,7 +228,11 @@ export type SyncOutcome =
function skipReason(): SyncOutcome | null {
if (_syncing) return { status: 'skipped', reason: 'busy' }
if (!navigator.onLine) return { status: 'skipped', reason: 'offline' }
// isEffectivelyOffline, not navigator.onLine: work-offline is a switch the user
// holds, and it survives a logout. A run started under it downloads over the
// real connection while every repo read answers from a cache that logging out
// just deleted, which is a sync that reports trips it did not store.
if (isEffectivelyOffline()) return { status: 'skipped', reason: 'offline' }
if (!isAuthed()) return { status: 'skipped', reason: 'signed-out' }
return null
}