fix(live-log): prevent freeze without GPS and prompt for day-start position

Harden geolocation with watchdog timeouts and permission checks so
desktop browsers without GPS no longer hang Live-Log. Show a hint to
log a position when none exists for the day.

Return 503 when crew-pool Prisma models are missing instead of crashing.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-01 19:20:34 +02:00
co-authored by Cursor
parent 98c0ed81d4
commit 2304f95ac1
13 changed files with 324 additions and 36 deletions
+94 -18
View File
@@ -52,7 +52,11 @@ import {
} from '../utils/liveEventCodes.js'
import { parseOwmCurrentWeather } from '../utils/openWeatherMap.js'
import { fetchOpenWeatherCurrent, WeatherApiError } from '../services/weather.js'
import { getCurrentPosition, normalizeGpsCoordinates } from '../utils/geolocation.js'
import {
getCurrentPosition,
normalizeGpsCoordinates,
queryGeolocationPermission
} from '../utils/geolocation.js'
import { sortLogEventsByTime, type LogEventPayload } from '../utils/logEntryPayload.js'
import {
dedupeSailNames,
@@ -167,11 +171,13 @@ export default function LiveLogView({
const undoPhotoIdRef = useRef<string | null>(null)
const undoTimerRef = useRef<number | null>(null)
const autoPositionBusyRef = useRef(false)
const busyRef = useRef(busy)
const initSeqRef = useRef(0)
const eventsRef = useRef(events)
const dateRef = useRef(date)
eventsRef.current = events
dateRef.current = date
busyRef.current = busy
const defaultSails = useMemo(
() => (i18n.language === 'de'
@@ -185,6 +191,10 @@ export default function LiveLogView({
)
const motorRunning = isMotorRunningFromEvents(events)
const motorLabel = t('logs.motor_propulsion')
const hasPositionFix = useMemo(
() => (date ? getLatestPositionFix(events, date) != null : false),
[events, date]
)
const applyLoadedEntry = useCallback((loaded: NonNullable<Awaited<ReturnType<typeof loadEntry>>>) => {
const entryEvents = (loaded.data.events as LogEventPayload[]) || []
@@ -276,7 +286,9 @@ export default function LiveLogView({
return () => {
initSeqRef.current += 1
}
}, [runInit])
// Only re-init when the logbook changes — not when i18n `t` identity changes.
// eslint-disable-next-line react-hooks/exhaustive-deps -- runInit
}, [logbookId])
useEffect(() => {
if (!loading && entryId) {
@@ -297,15 +309,34 @@ export default function LiveLogView({
useEffect(() => {
if (!entryId || loading) return
let cancelled = false
let startTimer: number | undefined
let intervalRef: number | undefined
const maybeAutoPosition = async () => {
if (document.visibilityState !== 'visible' || autoPositionBusyRef.current || busy) return
if (
cancelled
|| document.visibilityState !== 'visible'
|| autoPositionBusyRef.current
|| busyRef.current
) {
return
}
const permission = await queryGeolocationPermission()
if (cancelled || permission !== 'granted') return
const lastMs = getLastAutoPositionMs(eventsRef.current, dateRef.current)
if (lastMs != null && Date.now() - lastMs < AUTO_POSITION_INTERVAL_MS) return
autoPositionBusyRef.current = true
try {
const coords = await getCurrentPosition(8000)
const coords = await getCurrentPosition({
timeoutMs: 8000,
enableHighAccuracy: false,
maximumAge: 120_000
})
if (cancelled || busyRef.current) return
await appendQuickEvent(logbookId, entryId, {
gpsLat: coords.lat,
gpsLng: coords.lng,
@@ -313,23 +344,26 @@ export default function LiveLogView({
})
await refreshEntry(entryId)
} catch {
// Silent — auto-position is best-effort
// Best-effort; hint banner shows when no position fix exists yet.
} finally {
autoPositionBusyRef.current = false
}
}
let intervalRef: number | undefined
const startTimer = window.setTimeout(() => {
void maybeAutoPosition()
intervalRef = window.setInterval(() => void maybeAutoPosition(), AUTO_POSITION_CHECK_MS)
}, AUTO_POSITION_START_DELAY_MS)
void queryGeolocationPermission().then((permission) => {
if (cancelled || permission !== 'granted') return
startTimer = window.setTimeout(() => {
void maybeAutoPosition()
intervalRef = window.setInterval(() => void maybeAutoPosition(), AUTO_POSITION_CHECK_MS)
}, AUTO_POSITION_START_DELAY_MS)
})
return () => {
window.clearTimeout(startTimer)
cancelled = true
if (startTimer !== undefined) window.clearTimeout(startTimer)
if (intervalRef !== undefined) window.clearInterval(intervalRef)
}
}, [entryId, loading, logbookId, refreshEntry, busy])
}, [entryId, loading, logbookId, refreshEntry])
const runQuickAction = async (
action: () => Promise<boolean | void>,
@@ -364,8 +398,15 @@ export default function LiveLogView({
const openSogModal = async () => {
let prefill = ''
try {
const pos = await getCurrentPosition()
if (pos.speedKn != null) prefill = String(pos.speedKn)
const permission = await queryGeolocationPermission()
if (permission === 'granted') {
const pos = await getCurrentPosition({
timeoutMs: 8000,
enableHighAccuracy: false,
maximumAge: 60_000
})
if (pos.speedKn != null) prefill = String(pos.speedKn)
}
} catch {
// Manual entry when GPS speed unavailable
}
@@ -405,7 +446,16 @@ export default function LiveLogView({
setFixGpsLoading(true)
setModal('fix')
try {
const coords = await getCurrentPosition()
const permission = await queryGeolocationPermission()
if (permission !== 'granted') {
setFixGpsUnavailable(true)
return
}
const coords = await getCurrentPosition({
timeoutMs: 10_000,
enableHighAccuracy: false,
maximumAge: 60_000
})
setFixLat(coords.lat)
setFixLng(coords.lng)
} catch {
@@ -419,12 +469,28 @@ export default function LiveLogView({
setFixGpsLoading(true)
setFixGpsUnavailable(false)
try {
const coords = await getCurrentPosition()
const permission = await queryGeolocationPermission()
if (permission !== 'granted') {
setFixGpsUnavailable(true)
await showAlert(
`${t('logs.live_gps_error')}\n\n${t('logs.live_gps_start_hint')}`,
t('logs.live_fix')
)
return
}
const coords = await getCurrentPosition({
timeoutMs: 10_000,
enableHighAccuracy: false,
maximumAge: 60_000
})
setFixLat(coords.lat)
setFixLng(coords.lng)
} catch {
setFixGpsUnavailable(true)
await showAlert(t('logs.live_gps_error'), t('logs.live_fix'))
await showAlert(
`${t('logs.live_gps_error')}\n\n${t('logs.live_gps_start_hint')}`,
t('logs.live_fix')
)
} finally {
setFixGpsLoading(false)
}
@@ -786,6 +852,13 @@ export default function LiveLogView({
{error && <div className="auth-error mb-4">{error}</div>}
{!hasPositionFix && (
<p className="live-log-gps-hint" role="status">
<MapPin size={16} aria-hidden />
{t('logs.live_gps_start_hint')}
</p>
)}
<div className="live-log-layout">
<aside className="live-log-actions" aria-label={t('logs.live_actions_label')}>
<button type="button" className={`live-log-action-btn ${motorRunning ? 'is-active' : ''}`} onClick={handleMotorToggle} disabled={busy}>
@@ -974,7 +1047,10 @@ export default function LiveLogView({
<div className="live-log-modal" onClick={(e) => e.stopPropagation()}>
<h3>{t('logs.live_fix')}</h3>
{fixGpsUnavailable && (
<p className="live-log-modal-hint">{t('logs.live_fix_manual_hint')}</p>
<>
<p className="live-log-modal-hint live-log-gps-hint-modal">{t('logs.live_gps_start_hint')}</p>
<p className="live-log-modal-hint">{t('logs.live_fix_manual_hint')}</p>
</>
)}
<fieldset className="live-log-fix-coords" disabled={busy}>
<legend className="live-log-fix-label">{t('logs.event_gps')}</legend>