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:
@@ -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,24 +417,28 @@ export default function LiveLogView({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { lat, lng } = position
|
const { lat, lng } = position
|
||||||
|
const id = entryId
|
||||||
setWeatherOwmLoading(true)
|
setWeatherOwmLoading(true)
|
||||||
void runQuickAction(async () => {
|
setError(null)
|
||||||
|
void (async () => {
|
||||||
|
try {
|
||||||
let data: Record<string, unknown>
|
let data: Record<string, unknown>
|
||||||
try {
|
try {
|
||||||
data = await fetchOpenWeatherCurrent({ lat, lon: lng })
|
data = await fetchOpenWeatherCurrent({ lat, lon: lng })
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof WeatherApiError && err.code === 'NO_KEY') {
|
if (err instanceof WeatherApiError && err.code === 'NO_KEY') {
|
||||||
await showAlert(t('settings.no_key'), t('logs.live_weather_owm_btn'))
|
void showAlert(t('settings.no_key'), t('logs.live_weather_owm_btn'))
|
||||||
return false
|
return
|
||||||
}
|
}
|
||||||
console.error('Live log OWM weather failed:', err)
|
console.error('Live log OWM weather failed:', err)
|
||||||
await showAlert(t('settings.weather_error'), t('logs.live_weather_owm_btn'))
|
void showAlert(t('settings.weather_error'), t('logs.live_weather_owm_btn'))
|
||||||
return false
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const parsed = parseOwmCurrentWeather(data)
|
const parsed = parseOwmCurrentWeather(data)
|
||||||
|
const partials: Partial<LogEventPayload>[] = []
|
||||||
if (parsed.windDirection || parsed.windStrength) {
|
if (parsed.windDirection || parsed.windStrength) {
|
||||||
await appendQuickEvent(logbookId, entryId, {
|
partials.push({
|
||||||
windDirection: parsed.windDirection,
|
windDirection: parsed.windDirection,
|
||||||
windStrength: parsed.windStrength,
|
windStrength: parsed.windStrength,
|
||||||
weatherIcon: parsed.weatherIcon || undefined,
|
weatherIcon: parsed.weatherIcon || undefined,
|
||||||
@@ -441,26 +446,33 @@ export default function LiveLogView({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (parsed.windPressure) {
|
if (parsed.windPressure) {
|
||||||
await appendQuickEvent(logbookId, entryId, {
|
partials.push({
|
||||||
windPressure: parsed.windPressure,
|
windPressure: parsed.windPressure,
|
||||||
remarks: LIVE_EVENT_CODES.PRESSURE
|
remarks: LIVE_EVENT_CODES.PRESSURE
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (parsed.tempC) {
|
if (parsed.tempC) {
|
||||||
await appendQuickEvent(logbookId, entryId, {
|
partials.push({ remarks: liveTempRemark(parsed.tempC) })
|
||||||
remarks: liveTempRemark(parsed.tempC)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
if (parsed.precipText) {
|
if (parsed.precipText) {
|
||||||
await appendQuickEvent(logbookId, entryId, {
|
partials.push({ remarks: livePrecipRemark(parsed.precipText) })
|
||||||
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)
|
||||||
|
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)
|
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>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
Reference in New Issue
Block a user