diff --git a/client/src/App.css b/client/src/App.css index e09d8d9..4795f52 100644 --- a/client/src/App.css +++ b/client/src/App.css @@ -3359,6 +3359,31 @@ html.theme-cupertino .events-scroll-container { color: var(--app-text-muted); } +.live-log-gps-hint { + display: flex; + align-items: flex-start; + gap: 8px; + margin: 0 0 16px; + padding: 10px 12px; + border-radius: 8px; + font-size: 14px; + line-height: 1.45; + color: var(--app-text-muted); + background: rgba(59, 130, 246, 0.1); + border: 1px solid rgba(59, 130, 246, 0.25); +} + +.live-log-gps-hint svg { + flex-shrink: 0; + margin-top: 2px; + color: var(--app-accent-light, #93c5fd); +} + +.live-log-gps-hint-modal { + font-weight: 500; + color: var(--app-text, inherit); +} + .live-log-layout { display: grid; grid-template-columns: minmax(148px, 200px) 1fr; diff --git a/client/src/components/LiveLogView.tsx b/client/src/components/LiveLogView.tsx index 3e26e85..3660e13 100644 --- a/client/src/components/LiveLogView.tsx +++ b/client/src/components/LiveLogView.tsx @@ -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(null) const undoTimerRef = useRef(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>>) => { 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, @@ -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 &&
{error}
} + {!hasPositionFix && ( +

+ + {t('logs.live_gps_start_hint')} +

+ )} +