diff --git a/client/src/components/LiveLogView.tsx b/client/src/components/LiveLogView.tsx index 92c935b..d04c0f0 100644 --- a/client/src/components/LiveLogView.tsx +++ b/client/src/components/LiveLogView.tsx @@ -26,6 +26,7 @@ import { decryptJson } from '../services/crypto.js' import { PlausibleEvents, trackPlausibleEvent } from '../services/analytics.js' import { appendQuickEvent, + appendQuickEvents, appendTankRefill, findOrCreateTodayEntry, loadEntry, @@ -397,7 +398,7 @@ export default function LiveLogView({ } const handleFetchOwmWeather = () => { - if (!entryId || busy || weatherOwmLoading) return + if (!entryId || weatherOwmLoading) return const position = getLastPositionFixWithin( events, @@ -416,51 +417,62 @@ export default function LiveLogView({ } const { lat, lng } = position + const id = entryId setWeatherOwmLoading(true) - void runQuickAction(async () => { - let data: Record + setError(null) + void (async () => { try { - data = await fetchOpenWeatherCurrent({ lat, lon: lng }) - } catch (err) { - if (err instanceof WeatherApiError && err.code === 'NO_KEY') { - await showAlert(t('settings.no_key'), t('logs.live_weather_owm_btn')) - return false + let data: Record + try { + data = await fetchOpenWeatherCurrent({ lat, lon: lng }) + } catch (err) { + 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) - if (parsed.windDirection || parsed.windStrength) { - await appendQuickEvent(logbookId, entryId, { - windDirection: parsed.windDirection, - windStrength: parsed.windStrength, - weatherIcon: parsed.weatherIcon || undefined, - remarks: LIVE_EVENT_CODES.WIND - }) - } - if (parsed.windPressure) { - await appendQuickEvent(logbookId, entryId, { - windPressure: parsed.windPressure, - remarks: LIVE_EVENT_CODES.PRESSURE - }) - } - if (parsed.tempC) { - await appendQuickEvent(logbookId, entryId, { - remarks: liveTempRemark(parsed.tempC) - }) - } - if (parsed.precipText) { - await appendQuickEvent(logbookId, entryId, { - remarks: livePrecipRemark(parsed.precipText) - }) - } + const parsed = parseOwmCurrentWeather(data) + const partials: Partial[] = [] + if (parsed.windDirection || parsed.windStrength) { + partials.push({ + windDirection: parsed.windDirection, + windStrength: parsed.windStrength, + weatherIcon: parsed.weatherIcon || undefined, + remarks: LIVE_EVENT_CODES.WIND + }) + } + if (parsed.windPressure) { + partials.push({ + windPressure: parsed.windPressure, + remarks: LIVE_EVENT_CODES.PRESSURE + }) + } + if (parsed.tempC) { + partials.push({ remarks: liveTempRemark(parsed.tempC) }) + } + if (parsed.precipText) { + partials.push({ 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')) - }, 'weather_owm').finally(() => { - setWeatherOwmLoading(false) - }) + await appendQuickEvents(logbookId, id, partials) + await refreshEntry(id) + 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 = () => { @@ -729,7 +741,8 @@ export default function LiveLogView({ type="button" className="live-log-subaction-btn live-log-subaction-btn-owm" onClick={handleFetchOwmWeather} - disabled={busy || weatherOwmLoading} + disabled={weatherOwmLoading} + aria-busy={weatherOwmLoading} > {weatherOwmLoading ? t('logs.live_weather_owm_loading') : t('logs.live_weather_owm_btn')} diff --git a/client/src/services/quickEventLog.ts b/client/src/services/quickEventLog.ts index 14ce206..d85a88e 100644 --- a/client/src/services/quickEventLog.ts +++ b/client/src/services/quickEventLog.ts @@ -249,6 +249,35 @@ export async function appendQuickEvent( 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[] +): Promise { + 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( logbookId: string, entryId: string, diff --git a/client/src/services/weather.ts b/client/src/services/weather.ts index 5b18016..3854f60 100644 --- a/client/src/services/weather.ts +++ b/client/src/services/weather.ts @@ -11,6 +11,8 @@ export class WeatherApiError extends Error { } } +const OWM_FETCH_TIMEOUT_MS = 20_000 + export async function fetchOpenWeatherCurrent(params: { lat?: string lon?: string @@ -31,7 +33,22 @@ export async function fetchOpenWeatherCurrent(params: { const headers: Record = {} 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) { throw new WeatherApiError('No OpenWeatherMap API key configured', 'NO_KEY')