Fix live journal freeze during OpenWeatherMap fetch.

Batch weather events in one persist cycle, avoid global busy state while loading, and add a 20s API timeout.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-01 09:19:03 +02:00
co-authored by Cursor
parent 3d02f841a0
commit d6c7952af8
3 changed files with 102 additions and 43 deletions
+55 -42
View File
@@ -26,6 +26,7 @@ import { decryptJson } from '../services/crypto.js'
import { PlausibleEvents, trackPlausibleEvent } from '../services/analytics.js' import { PlausibleEvents, trackPlausibleEvent } from '../services/analytics.js'
import { import {
appendQuickEvent, appendQuickEvent,
appendQuickEvents,
appendTankRefill, appendTankRefill,
findOrCreateTodayEntry, findOrCreateTodayEntry,
loadEntry, loadEntry,
@@ -397,7 +398,7 @@ export default function LiveLogView({
} }
const handleFetchOwmWeather = () => { const handleFetchOwmWeather = () => {
if (!entryId || busy || weatherOwmLoading) return if (!entryId || weatherOwmLoading) return
const position = getLastPositionFixWithin( const position = getLastPositionFixWithin(
events, events,
@@ -416,51 +417,62 @@ export default function LiveLogView({
} }
const { lat, lng } = position const { lat, lng } = position
const id = entryId
setWeatherOwmLoading(true) setWeatherOwmLoading(true)
void runQuickAction(async () => { setError(null)
let data: Record<string, unknown> void (async () => {
try { try {
data = await fetchOpenWeatherCurrent({ lat, lon: lng }) let data: Record<string, unknown>
} catch (err) { try {
if (err instanceof WeatherApiError && err.code === 'NO_KEY') { data = await fetchOpenWeatherCurrent({ lat, lon: lng })
await showAlert(t('settings.no_key'), t('logs.live_weather_owm_btn')) } catch (err) {
return false if (err instanceof WeatherApiError && err.code === 'NO_KEY') {
void showAlert(t('settings.no_key'), t('logs.live_weather_owm_btn'))
return
}
console.error('Live log OWM weather failed:', err)
void showAlert(t('settings.weather_error'), t('logs.live_weather_owm_btn'))
return
} }
console.error('Live log OWM weather failed:', err)
await showAlert(t('settings.weather_error'), t('logs.live_weather_owm_btn'))
return false
}
const parsed = parseOwmCurrentWeather(data) const parsed = parseOwmCurrentWeather(data)
if (parsed.windDirection || parsed.windStrength) { const partials: Partial<LogEventPayload>[] = []
await appendQuickEvent(logbookId, entryId, { if (parsed.windDirection || parsed.windStrength) {
windDirection: parsed.windDirection, partials.push({
windStrength: parsed.windStrength, windDirection: parsed.windDirection,
weatherIcon: parsed.weatherIcon || undefined, windStrength: parsed.windStrength,
remarks: LIVE_EVENT_CODES.WIND weatherIcon: parsed.weatherIcon || undefined,
}) remarks: LIVE_EVENT_CODES.WIND
} })
if (parsed.windPressure) { }
await appendQuickEvent(logbookId, entryId, { if (parsed.windPressure) {
windPressure: parsed.windPressure, partials.push({
remarks: LIVE_EVENT_CODES.PRESSURE windPressure: parsed.windPressure,
}) remarks: LIVE_EVENT_CODES.PRESSURE
} })
if (parsed.tempC) { }
await appendQuickEvent(logbookId, entryId, { if (parsed.tempC) {
remarks: liveTempRemark(parsed.tempC) partials.push({ remarks: liveTempRemark(parsed.tempC) })
}) }
} if (parsed.precipText) {
if (parsed.precipText) { partials.push({ remarks: livePrecipRemark(parsed.precipText) })
await appendQuickEvent(logbookId, entryId, { }
remarks: livePrecipRemark(parsed.precipText) if (partials.length === 0) {
}) void showAlert(t('settings.weather_error'), t('logs.live_weather_owm_btn'))
} return
}
await showAlert(t('settings.weather_success'), t('logs.live_weather_owm_btn')) await appendQuickEvents(logbookId, id, partials)
}, 'weather_owm').finally(() => { await refreshEntry(id)
setWeatherOwmLoading(false) showUndo()
}) trackPlausibleEvent(PlausibleEvents.LIVE_LOG_EVENT_LOGGED, { action: 'weather_owm' })
} catch (err: unknown) {
console.error('Live log OWM weather save failed:', err)
setError(err instanceof Error ? err.message : t('logs.live_action_error'))
} finally {
setWeatherOwmLoading(false)
}
})()
} }
const handleUndo = () => { const handleUndo = () => {
@@ -729,7 +741,8 @@ export default function LiveLogView({
type="button" type="button"
className="live-log-subaction-btn live-log-subaction-btn-owm" className="live-log-subaction-btn live-log-subaction-btn-owm"
onClick={handleFetchOwmWeather} onClick={handleFetchOwmWeather}
disabled={busy || weatherOwmLoading} disabled={weatherOwmLoading}
aria-busy={weatherOwmLoading}
> >
{weatherOwmLoading ? t('logs.live_weather_owm_loading') : t('logs.live_weather_owm_btn')} {weatherOwmLoading ? t('logs.live_weather_owm_loading') : t('logs.live_weather_owm_btn')}
</button> </button>
+29
View File
@@ -249,6 +249,35 @@ export async function appendQuickEvent(
return { events: nextEvents, 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<LogEventPayload>[]
): Promise<AppendQuickEventResult> {
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( async function persistEntry(
logbookId: string, logbookId: string,
entryId: string, entryId: string,
+18 -1
View File
@@ -11,6 +11,8 @@ export class WeatherApiError extends Error {
} }
} }
const OWM_FETCH_TIMEOUT_MS = 20_000
export async function fetchOpenWeatherCurrent(params: { export async function fetchOpenWeatherCurrent(params: {
lat?: string lat?: string
lon?: string lon?: string
@@ -31,7 +33,22 @@ export async function fetchOpenWeatherCurrent(params: {
const headers: Record<string, string> = {} const headers: Record<string, string> = {}
if (userKey) headers['X-OWM-Api-Key'] = userKey if (userKey) headers['X-OWM-Api-Key'] = userKey
const res = await apiFetch(`/api/weather/current?${searchParams.toString()}`, { headers }) const controller = new AbortController()
const timeoutId = window.setTimeout(() => controller.abort(), OWM_FETCH_TIMEOUT_MS)
let res: Response
try {
res = await apiFetch(`/api/weather/current?${searchParams.toString()}`, {
headers,
signal: controller.signal
})
} catch (err) {
if (err instanceof DOMException && err.name === 'AbortError') {
throw new WeatherApiError('Weather request timed out')
}
throw err
} finally {
window.clearTimeout(timeoutId)
}
if (res.status === 503) { if (res.status === 503) {
throw new WeatherApiError('No OpenWeatherMap API key configured', 'NO_KEY') throw new WeatherApiError('No OpenWeatherMap API key configured', 'NO_KEY')