From e014e997de3f6cc49e44b4a43f860124db2c1665 Mon Sep 17 00:00:00 2001 From: elpatron Date: Wed, 3 Jun 2026 17:42:08 +0200 Subject: [PATCH] refactor(live-log): Position-Terminologie und Modal-UX vereinheitlichen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix/Standort heißen überall Position (__live:position, Legacy __live:fix). Nachfüll-Buttons + Diesel/+ Wasser, Abbruch statt Nein in Live-Modals. Co-authored-by: Cursor --- VERSION | 2 +- client/src/App.css | 16 +-- client/src/components/LiveCameraCapture.tsx | 4 +- client/src/components/LiveLogView.tsx | 144 ++++++++++---------- client/src/components/LiveVoiceCapture.tsx | 2 +- client/src/i18n/locales/da.json | 27 ++-- client/src/i18n/locales/de.json | 29 ++-- client/src/i18n/locales/en.json | 29 ++-- client/src/i18n/locales/nb.json | 27 ++-- client/src/i18n/locales/sv.json | 27 ++-- client/src/utils/formatEventSummary.test.ts | 10 +- client/src/utils/formatEventSummary.ts | 7 +- client/src/utils/liveEventCodes.ts | 35 +++-- client/src/utils/liveLogPosition.test.ts | 61 +++++---- docs/plausible-events.md | 4 +- 15 files changed, 225 insertions(+), 199 deletions(-) diff --git a/VERSION b/VERSION index 5a2c686..5cff5f1 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.1.0.112 +0.1.1.0 diff --git a/client/src/App.css b/client/src/App.css index c694e94..4578d23 100644 --- a/client/src/App.css +++ b/client/src/App.css @@ -3865,14 +3865,14 @@ html.theme-cupertino .events-scroll-container { max-width: min(100%, 420px); } -.live-log-fix-coords { +.live-log-position-coords { margin: 0; padding: 0; border: none; min-width: 0; } -.live-log-fix-label { +.live-log-position-label { display: block; margin: 0 0 10px; padding: 0; @@ -3881,35 +3881,35 @@ html.theme-cupertino .events-scroll-container { color: var(--app-text-muted); } -.live-log-fix-coords-row { +.live-log-position-coords-row { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; min-width: 0; } -.live-log-fix-field { +.live-log-position-field { display: flex; flex-direction: column; gap: 6px; min-width: 0; } -.live-log-fix-field-label { +.live-log-position-field-label { font-size: 12px; color: var(--app-text-muted); } -.live-log-fix-field .input-text { +.live-log-position-field .input-text { width: 100%; box-sizing: border-box; } -.live-log-fix-gps-row { +.live-log-position-gps-row { margin-top: 10px; } -.live-log-fix-gps-btn { +.live-log-position-gps-btn { width: 100%; box-sizing: border-box; display: flex; diff --git a/client/src/components/LiveCameraCapture.tsx b/client/src/components/LiveCameraCapture.tsx index 1733e9a..bfad442 100644 --- a/client/src/components/LiveCameraCapture.tsx +++ b/client/src/components/LiveCameraCapture.tsx @@ -216,7 +216,7 @@ export default function LiveCameraCapture({ className="btn secondary live-camera-close" onClick={onClose} disabled={busy} - aria-label={t('logs.confirm_no')} + aria-label={t('logs.live_cancel')} > @@ -287,7 +287,7 @@ export default function LiveCameraCapture({
{showPreview ? ( diff --git a/client/src/components/LiveLogView.tsx b/client/src/components/LiveLogView.tsx index fb8685a..54ea4d9 100644 --- a/client/src/components/LiveLogView.tsx +++ b/client/src/components/LiveLogView.tsx @@ -33,8 +33,8 @@ import { import { formatEventSummary } from '../utils/formatEventSummary.js' import { getLastAutoPositionMs, - getLastPositionFixWithin, - getLatestPositionFix, + getLastLoggedPositionWithin, + getLatestLoggedPosition, isMotorRunningFromEvents, LIVE_EVENT_CODES, LIVE_LOG_WEATHER_POSITION_MAX_AGE_MS, @@ -96,7 +96,7 @@ type LiveModal = | 'water' | 'sog' | 'stw' - | 'fix' + | 'position' | 'photo' | 'voice' @@ -167,10 +167,10 @@ export default function LiveLogView({ const [valueInputSecondary, setValueInputSecondary] = useState('') const [selectedSails, setSelectedSails] = useState([]) const [undoVisible, setUndoVisible] = useState(false) - const [fixLat, setFixLat] = useState('') - const [fixLng, setFixLng] = useState('') - const [fixGpsLoading, setFixGpsLoading] = useState(false) - const [fixGpsUnavailable, setFixGpsUnavailable] = useState(false) + const [positionLat, setPositionLat] = useState('') + const [positionLng, setPositionLng] = useState('') + const [positionGpsLoading, setPositionGpsLoading] = useState(false) + const [positionGpsUnavailable, setPositionGpsUnavailable] = useState(false) const [photoCaption, setPhotoCaption] = useState('') const [photoSaving, setPhotoSaving] = useState(false) const [voiceCaption, setVoiceCaption] = useState('') @@ -202,8 +202,8 @@ export default function LiveLogView({ ) const motorRunning = isMotorRunningFromEvents(events) const motorLabel = t('logs.motor_propulsion') - const hasPositionFix = useMemo( - () => (date ? getLatestPositionFix(events, date) != null : false), + const hasLoggedPosition = useMemo( + () => (date ? getLatestLoggedPosition(events, date) != null : false), [events, date] ) const voiceMemoLookup = useEntryVoiceMemos(logbookId, entryId) @@ -358,7 +358,7 @@ export default function LiveLogView({ }) await refreshEntry(entryId) } catch { - // Best-effort; hint banner shows when no position fix exists yet. + // Best-effort; hint banner shows when no position has been logged yet. } finally { autoPositionBusyRef.current = false } @@ -453,16 +453,16 @@ export default function LiveLogView({ }, 'moor') } - const openFixModal = async () => { - setFixLat('') - setFixLng('') - setFixGpsUnavailable(false) - setFixGpsLoading(true) - setModal('fix') + const openPositionModal = async () => { + setPositionLat('') + setPositionLng('') + setPositionGpsUnavailable(false) + setPositionGpsLoading(true) + setModal('position') try { const permission = await queryGeolocationPermission() if (permission !== 'granted') { - setFixGpsUnavailable(true) + setPositionGpsUnavailable(true) return } const coords = await getCurrentPosition({ @@ -470,25 +470,25 @@ export default function LiveLogView({ enableHighAccuracy: false, maximumAge: 60_000 }) - setFixLat(coords.lat) - setFixLng(coords.lng) + setPositionLat(coords.lat) + setPositionLng(coords.lng) } catch { - setFixGpsUnavailable(true) + setPositionGpsUnavailable(true) } finally { - setFixGpsLoading(false) + setPositionGpsLoading(false) } } - const retryFixGps = async () => { - setFixGpsLoading(true) - setFixGpsUnavailable(false) + const retryPositionGps = async () => { + setPositionGpsLoading(true) + setPositionGpsUnavailable(false) try { const permission = await queryGeolocationPermission() if (permission !== 'granted') { - setFixGpsUnavailable(true) + setPositionGpsUnavailable(true) await showAlert( `${t('logs.live_gps_error')}\n\n${t('logs.live_gps_start_hint')}`, - t('logs.live_fix') + t('logs.live_position') ) return } @@ -497,23 +497,23 @@ export default function LiveLogView({ enableHighAccuracy: false, maximumAge: 60_000 }) - setFixLat(coords.lat) - setFixLng(coords.lng) + setPositionLat(coords.lat) + setPositionLng(coords.lng) } catch { - setFixGpsUnavailable(true) + setPositionGpsUnavailable(true) await showAlert( `${t('logs.live_gps_error')}\n\n${t('logs.live_gps_start_hint')}`, - t('logs.live_fix') + t('logs.live_position') ) } finally { - setFixGpsLoading(false) + setPositionGpsLoading(false) } } - const confirmFix = () => { - const coords = normalizeGpsCoordinates(fixLat, fixLng) + const confirmPosition = () => { + const coords = normalizeGpsCoordinates(positionLat, positionLng) if (!coords) { - void showAlert(t('logs.live_fix_invalid'), t('logs.live_fix')) + void showAlert(t('logs.live_position_invalid'), t('logs.live_position')) return } setModal('none') @@ -522,9 +522,9 @@ export default function LiveLogView({ await appendQuickEvent(logbookId, entryId, { gpsLat: coords.lat, gpsLng: coords.lng, - remarks: LIVE_EVENT_CODES.FIX + remarks: LIVE_EVENT_CODES.POSITION }) - }, 'fix') + }, 'position') } const handleFetchOwmWeather = () => { @@ -534,17 +534,17 @@ export default function LiveLogView({ return } - const position = getLastPositionFixWithin( + const position = getLastLoggedPositionWithin( events, date, LIVE_LOG_WEATHER_POSITION_MAX_AGE_MS ) if (!position) { - const latest = getLatestPositionFix(events, date) + const latest = getLatestLoggedPosition(events, date) void showAlert( latest - ? t('logs.live_weather_fix_stale') - : t('logs.live_weather_fix_required'), + ? t('logs.live_weather_position_stale') + : t('logs.live_weather_position_required'), t('logs.live_weather_owm_btn') ) return @@ -945,7 +945,7 @@ export default function LiveLogView({ {error &&
{error}
} - {!hasPositionFix && ( + {!hasLoggedPosition && (

{t('logs.live_gps_start_hint')} @@ -1036,9 +1036,9 @@ export default function LiveLogView({ )}

- +
- + @@ -1237,7 +1237,7 @@ export default function LiveLogView({

{t('logs.live_comment_btn')}

setCommentText(e.target.value)} placeholder={t('logs.live_comment_placeholder')} autoFocus onKeyDown={(e) => { if (e.key === 'Enter') confirmComment() }} />
- +
@@ -1271,7 +1271,7 @@ export default function LiveLogView({ />
- +
@@ -1293,7 +1293,7 @@ export default function LiveLogView({ />
- +
@@ -1338,7 +1338,7 @@ export default function LiveLogView({ onKeyDown={(e) => { if (e.key === 'Enter') confirmValueModal() }} />
- +
diff --git a/client/src/components/LiveVoiceCapture.tsx b/client/src/components/LiveVoiceCapture.tsx index 63b3f6e..24677fc 100644 --- a/client/src/components/LiveVoiceCapture.tsx +++ b/client/src/components/LiveVoiceCapture.tsx @@ -199,7 +199,7 @@ export default function LiveVoiceCapture({ className="btn-icon" onClick={onClose} disabled={busy || saving || phase === 'recording'} - aria-label={t('logs.confirm_no')} + aria-label={t('logs.live_cancel')} > diff --git a/client/src/i18n/locales/da.json b/client/src/i18n/locales/da.json index 4cb50db..a331150 100644 --- a/client/src/i18n/locales/da.json +++ b/client/src/i18n/locales/da.json @@ -249,13 +249,13 @@ "live_sails_confirm": "Indtast", "live_sails_confirm_count": "Indtast ({{count}})", "live_sails": "Sejl: {{sails}}", - "live_fix": "Fix", - "live_fix_coords": "Fix {{lat}}, {{lng}}", - "live_fix_manual_hint": "GPS ikke tilgængelig. Indtast bredde- og længdegrad manuelt, eller prøv igen med GPS-knappen.", - "live_fix_gps_loading": "Henter GPS-position…", - "live_fix_invalid": "Indtast gyldige koordinater (bredde −90…90, længde −180…180).", - "live_fix_lat_placeholder": "Bredde (Lat)", - "live_fix_lng_placeholder": "Længde (Lng)", + "live_position": "Position", + "live_position_coords": "Position {{lat}}, {{lng}}", + "live_position_manual_hint": "GPS ikke tilgængelig. Indtast bredde- og længdegrad manuelt, eller prøv igen med GPS-knappen.", + "live_position_gps_loading": "Henter GPS-position…", + "live_position_invalid": "Indtast gyldige koordinater (bredde −90…90, længde −180…180).", + "live_position_lat_placeholder": "Bredde (Lat)", + "live_position_lng_placeholder": "Længde (Lng)", "live_photo_btn": "Foto (kamera)", "live_photo_capture_btn": "Tag billede", "live_photo_save_btn": "Gem", @@ -297,8 +297,8 @@ "live_weather_btn": "Vejr", "live_weather_owm_btn": "Hent OpenWeatherMap-vejr", "live_weather_owm_loading": "Henter vejr…", - "live_weather_fix_required": "Log først en GPS-fix (Fix-knap) for at hente OpenWeatherMap-vejr. Positionen må højst være 6 timer gammel.", - "live_weather_fix_stale": "Den seneste GPS-fix er ældre end 6 timer. Log en ny fix, før du henter vejr.", + "live_weather_position_required": "Log først en position (Position-knap) for at hente OpenWeatherMap-vejr. Positionen må højst være 6 timer gammel.", + "live_weather_position_stale": "Den seneste position er ældre end 6 timer. Log en ny position, før du henter vejr.", "live_wind_btn": "Vind", "live_temp_btn": "T °C", "live_pressure_btn": "Lufttryk", @@ -306,8 +306,8 @@ "live_sea_state_btn": "Søgang", "live_visibility_btn": "Sigtbarhed", "live_course_btn": "Kurs", - "live_fuel_btn": "Diesel", - "live_water_btn": "Vand", + "live_fuel_btn": "+ Diesel", + "live_water_btn": "+ Vand", "live_wind_entry": "Vind {{value}}", "live_temp_entry": "Temperatur {{temp}} °C", "live_pressure_entry": "Lufttryk {{value}} hPa", @@ -320,6 +320,7 @@ "live_auto_position": "Auto-position", "live_undo_hint": "Indtastning gemt", "live_undo_btn": "Fortryd", + "live_cancel": "Annuller", "live_pressure_placeholder": "f.eks. 1013", "live_temp_placeholder": "f.eks. 18", "live_precip_placeholder": "f.eks. let regn", @@ -484,8 +485,8 @@ "nmea_change_engine_stop": "Engine off", "nmea_change_autopilot_on": "Autopilot on", "nmea_change_autopilot_off": "Autopilot off", - "nmea_change_gps_lost": "GPS fix lost", - "nmea_change_gps_regained": "GPS fix restored", + "nmea_change_gps_lost": "GPS-position mistet", + "nmea_change_gps_regained": "GPS-position gendannet", "nmea_change_water_temp": "Water temp. {{from}} → {{to}} °C", "nmea_change_departure": "Departure / underway", "nmea_change_anchor": "Anchored / stop", diff --git a/client/src/i18n/locales/de.json b/client/src/i18n/locales/de.json index ea5b5f2..65966ac 100644 --- a/client/src/i18n/locales/de.json +++ b/client/src/i18n/locales/de.json @@ -249,13 +249,13 @@ "live_sails_confirm": "Eintragen", "live_sails_confirm_count": "Eintragen ({{count}})", "live_sails": "Segel: {{sails}}", - "live_fix": "Fix", - "live_fix_coords": "Fix {{lat}}, {{lng}}", - "live_fix_manual_hint": "GPS nicht verfügbar. Breiten- und Längengrad manuell eingeben oder erneut per GPS-Knopf versuchen.", - "live_fix_gps_loading": "GPS-Position wird ermittelt…", - "live_fix_invalid": "Bitte gültige Koordinaten eingeben (Breite −90…90, Länge −180…180).", - "live_fix_lat_placeholder": "Breite (Lat)", - "live_fix_lng_placeholder": "Länge (Lng)", + "live_position": "Position", + "live_position_coords": "Position {{lat}}, {{lng}}", + "live_position_manual_hint": "GPS nicht verfügbar. Breiten- und Längengrad manuell eingeben oder erneut per GPS-Knopf versuchen.", + "live_position_gps_loading": "GPS-Position wird ermittelt…", + "live_position_invalid": "Bitte gültige Koordinaten eingeben (Breite −90…90, Länge −180…180).", + "live_position_lat_placeholder": "Breite (Lat)", + "live_position_lng_placeholder": "Länge (Lng)", "live_photo_btn": "Foto (Kamera)", "live_photo_capture_btn": "Aufnehmen", "live_photo_save_btn": "Speichern", @@ -292,13 +292,13 @@ "live_comment_placeholder": "Freitext eingeben…", "live_comment_confirm": "Eintragen", "live_gps_error": "GPS-Position konnte nicht ermittelt werden.", - "live_gps_start_hint": "Beginne deine Tagesreise immer mit einem Standort.", + "live_gps_start_hint": "Beginne deine Tagesreise immer mit einer Position.", "live_event_generic": "Ereignis", "live_weather_btn": "Wetter", "live_weather_owm_btn": "OpenWeatherMap Wetter abrufen", "live_weather_owm_loading": "Wetter wird geladen…", - "live_weather_fix_required": "Für Wetter von OpenWeatherMap zuerst einen GPS-Fix eintragen (Schaltfläche „Fix“). Die Position darf höchstens 6 Stunden alt sein.", - "live_weather_fix_stale": "Der letzte GPS-Fix ist älter als 6 Stunden. Bitte erneut einen Fix loggen, bevor du Wetter abrufst.", + "live_weather_position_required": "Für Wetter von OpenWeatherMap zuerst eine Position eintragen (Schaltfläche „Position“). Die Position darf höchstens 6 Stunden alt sein.", + "live_weather_position_stale": "Die letzte Position ist älter als 6 Stunden. Bitte erneut eine Position loggen, bevor du Wetter abrufst.", "live_wind_btn": "Wind", "live_temp_btn": "T °C", "live_pressure_btn": "Luftdruck", @@ -306,8 +306,8 @@ "live_sea_state_btn": "Seegang", "live_visibility_btn": "Sichtweite", "live_course_btn": "Kurs", - "live_fuel_btn": "Diesel", - "live_water_btn": "Wasser", + "live_fuel_btn": "+ Diesel", + "live_water_btn": "+ Wasser", "live_wind_entry": "Wind {{value}}", "live_temp_entry": "Temperatur {{temp}} °C", "live_pressure_entry": "Luftdruck {{value}} hPa", @@ -320,6 +320,7 @@ "live_auto_position": "Auto-Position", "live_undo_hint": "Eintrag gespeichert", "live_undo_btn": "Rückgängig", + "live_cancel": "Abbruch", "live_pressure_placeholder": "z. B. 1013", "live_temp_placeholder": "z. B. 18", "live_precip_placeholder": "z. B. leichter Regen", @@ -474,8 +475,8 @@ "nmea_change_engine_stop": "Motor aus", "nmea_change_autopilot_on": "Autopilot ein", "nmea_change_autopilot_off": "Autopilot aus", - "nmea_change_gps_lost": "GPS-Fix verloren", - "nmea_change_gps_regained": "GPS-Fix wiederhergestellt", + "nmea_change_gps_lost": "GPS-Position verloren", + "nmea_change_gps_regained": "GPS-Position wiederhergestellt", "nmea_change_water_temp": "Wassertemp. {{from}} → {{to}} °C", "nmea_change_departure": "Abfahrt / Fahrtbeginn", "nmea_change_anchor": "Ankern / Stop", diff --git a/client/src/i18n/locales/en.json b/client/src/i18n/locales/en.json index 4d2982a..ab7b9ad 100644 --- a/client/src/i18n/locales/en.json +++ b/client/src/i18n/locales/en.json @@ -249,13 +249,13 @@ "live_sails_confirm": "Log entry", "live_sails_confirm_count": "Log entry ({{count}})", "live_sails": "Sails: {{sails}}", - "live_fix": "Fix", - "live_fix_coords": "Fix {{lat}}, {{lng}}", - "live_fix_manual_hint": "GPS unavailable. Enter latitude and longitude manually, or try again with the GPS button.", - "live_fix_gps_loading": "Getting GPS position…", - "live_fix_invalid": "Please enter valid coordinates (latitude −90…90, longitude −180…180).", - "live_fix_lat_placeholder": "Latitude (Lat)", - "live_fix_lng_placeholder": "Longitude (Lng)", + "live_position": "Position", + "live_position_coords": "Position {{lat}}, {{lng}}", + "live_position_manual_hint": "GPS unavailable. Enter latitude and longitude manually, or try again with the GPS button.", + "live_position_gps_loading": "Getting GPS position…", + "live_position_invalid": "Please enter valid coordinates (latitude −90…90, longitude −180…180).", + "live_position_lat_placeholder": "Latitude (Lat)", + "live_position_lng_placeholder": "Longitude (Lng)", "live_photo_btn": "Photo (camera)", "live_photo_capture_btn": "Capture", "live_photo_save_btn": "Save", @@ -292,13 +292,13 @@ "live_comment_placeholder": "Enter text…", "live_comment_confirm": "Log entry", "live_gps_error": "Could not determine GPS position.", - "live_gps_start_hint": "Always start your day's voyage with a position fix.", + "live_gps_start_hint": "Always start your day's voyage with a position.", "live_event_generic": "Event", "live_weather_btn": "Weather", "live_weather_owm_btn": "Fetch OpenWeatherMap weather", "live_weather_owm_loading": "Loading weather…", - "live_weather_fix_required": "Log a GPS fix first (Fix button) to fetch OpenWeatherMap weather. The position must be at most 6 hours old.", - "live_weather_fix_stale": "The last GPS fix is older than 6 hours. Log a new fix before fetching weather.", + "live_weather_position_required": "Log a position first (Position button) to fetch OpenWeatherMap weather. The position must be at most 6 hours old.", + "live_weather_position_stale": "The last position is older than 6 hours. Log a new position before fetching weather.", "live_wind_btn": "Wind", "live_temp_btn": "Temp °C", "live_pressure_btn": "Pressure", @@ -306,8 +306,8 @@ "live_sea_state_btn": "Sea state", "live_visibility_btn": "Visibility", "live_course_btn": "Course", - "live_fuel_btn": "Fuel", - "live_water_btn": "Water", + "live_fuel_btn": "+ Fuel", + "live_water_btn": "+ Water", "live_wind_entry": "Wind {{value}}", "live_temp_entry": "Temperature {{temp}} °C", "live_pressure_entry": "Pressure {{value}} hPa", @@ -320,6 +320,7 @@ "live_auto_position": "Auto position", "live_undo_hint": "Entry saved", "live_undo_btn": "Undo", + "live_cancel": "Cancel", "live_pressure_placeholder": "e.g. 1013", "live_temp_placeholder": "e.g. 18", "live_precip_placeholder": "e.g. light rain", @@ -474,8 +475,8 @@ "nmea_change_engine_stop": "Engine off", "nmea_change_autopilot_on": "Autopilot on", "nmea_change_autopilot_off": "Autopilot off", - "nmea_change_gps_lost": "GPS fix lost", - "nmea_change_gps_regained": "GPS fix restored", + "nmea_change_gps_lost": "GPS position lost", + "nmea_change_gps_regained": "GPS position restored", "nmea_change_water_temp": "Water temp. {{from}} → {{to}} °C", "nmea_change_departure": "Departure / underway", "nmea_change_anchor": "Anchored / stop", diff --git a/client/src/i18n/locales/nb.json b/client/src/i18n/locales/nb.json index 08d9f4f..e2817b7 100644 --- a/client/src/i18n/locales/nb.json +++ b/client/src/i18n/locales/nb.json @@ -249,13 +249,13 @@ "live_sails_confirm": "Loggfør", "live_sails_confirm_count": "Loggfør ({{count}})", "live_sails": "Seil: {{sails}}", - "live_fix": "Fix", - "live_fix_coords": "Fix {{lat}}, {{lng}}", - "live_fix_manual_hint": "GPS ikke tilgjengelig. Skriv inn bredde- og lengdegrad manuelt, eller prøv igjen med GPS-knappen.", - "live_fix_gps_loading": "Henter GPS-posisjon…", - "live_fix_invalid": "Skriv inn gyldige koordinater (bredde −90…90, lengde −180…180).", - "live_fix_lat_placeholder": "Bredde (Lat)", - "live_fix_lng_placeholder": "Lengde (Lng)", + "live_position": "Posisjon", + "live_position_coords": "Posisjon {{lat}}, {{lng}}", + "live_position_manual_hint": "GPS ikke tilgjengelig. Skriv inn bredde- og lengdegrad manuelt, eller prøv igjen med GPS-knappen.", + "live_position_gps_loading": "Henter GPS-posisjon…", + "live_position_invalid": "Skriv inn gyldige koordinater (bredde −90…90, lengde −180…180).", + "live_position_lat_placeholder": "Bredde (Lat)", + "live_position_lng_placeholder": "Lengde (Lng)", "live_photo_btn": "Foto (kamera)", "live_photo_capture_btn": "Ta bilde", "live_photo_save_btn": "Lagre", @@ -297,8 +297,8 @@ "live_weather_btn": "Vær", "live_weather_owm_btn": "Hent OpenWeatherMap-vær", "live_weather_owm_loading": "Henter vær…", - "live_weather_fix_required": "Logg først en GPS-fix (Fix-knapp) for å hente OpenWeatherMap-vær. Posisjonen må være maks 6 timer gammel.", - "live_weather_fix_stale": "Siste GPS-fix er eldre enn 6 timer. Logg en ny fix før du henter vær.", + "live_weather_position_required": "Logg først en posisjon (Posisjon-knapp) for å hente OpenWeatherMap-vær. Posisjonen må være maks 6 timer gammel.", + "live_weather_position_stale": "Siste posisjon er eldre enn 6 timer. Logg en ny posisjon før du henter vær.", "live_wind_btn": "Vind", "live_temp_btn": "T °C", "live_pressure_btn": "Lufttrykk", @@ -306,8 +306,8 @@ "live_sea_state_btn": "Sjøgang", "live_visibility_btn": "Sikt", "live_course_btn": "Kurs", - "live_fuel_btn": "Diesel", - "live_water_btn": "Vann", + "live_fuel_btn": "+ Diesel", + "live_water_btn": "+ Vann", "live_wind_entry": "Vind {{value}}", "live_temp_entry": "Temperatur {{temp}} °C", "live_pressure_entry": "Lufttrykk {{value}} hPa", @@ -320,6 +320,7 @@ "live_auto_position": "Auto-posisjon", "live_undo_hint": "Oppføring lagret", "live_undo_btn": "Angre", + "live_cancel": "Avbryt", "live_pressure_placeholder": "f.eks. 1013", "live_temp_placeholder": "f.eks. 18", "live_precip_placeholder": "f.eks. lett regn", @@ -484,8 +485,8 @@ "nmea_change_engine_stop": "Engine off", "nmea_change_autopilot_on": "Autopilot on", "nmea_change_autopilot_off": "Autopilot off", - "nmea_change_gps_lost": "GPS fix lost", - "nmea_change_gps_regained": "GPS fix restored", + "nmea_change_gps_lost": "GPS-posisjon tapt", + "nmea_change_gps_regained": "GPS-posisjon gjenopprettet", "nmea_change_water_temp": "Water temp. {{from}} → {{to}} °C", "nmea_change_departure": "Departure / underway", "nmea_change_anchor": "Anchored / stop", diff --git a/client/src/i18n/locales/sv.json b/client/src/i18n/locales/sv.json index b5b781e..d9ced0e 100644 --- a/client/src/i18n/locales/sv.json +++ b/client/src/i18n/locales/sv.json @@ -249,13 +249,13 @@ "live_sails_confirm": "Logga", "live_sails_confirm_count": "Logga ({{count}})", "live_sails": "Segel: {{sails}}", - "live_fix": "Fix", - "live_fix_coords": "Fix {{lat}}, {{lng}}", - "live_fix_manual_hint": "GPS ej tillgänglig. Ange latitud och longitud manuellt, eller försök igen med GPS-knappen.", - "live_fix_gps_loading": "Hämtar GPS-position…", - "live_fix_invalid": "Ange giltiga koordinater (latitud −90…90, longitud −180…180).", - "live_fix_lat_placeholder": "Latitud (Lat)", - "live_fix_lng_placeholder": "Longitud (Lng)", + "live_position": "Position", + "live_position_coords": "Position {{lat}}, {{lng}}", + "live_position_manual_hint": "GPS ej tillgänglig. Ange latitud och longitud manuellt, eller försök igen med GPS-knappen.", + "live_position_gps_loading": "Hämtar GPS-position…", + "live_position_invalid": "Ange giltiga koordinater (latitud −90…90, longitud −180…180).", + "live_position_lat_placeholder": "Latitud (Lat)", + "live_position_lng_placeholder": "Longitud (Lng)", "live_photo_btn": "Foto (kamera)", "live_photo_capture_btn": "Ta foto", "live_photo_save_btn": "Spara", @@ -297,8 +297,8 @@ "live_weather_btn": "Väder", "live_weather_owm_btn": "Hämta OpenWeatherMap-väder", "live_weather_owm_loading": "Hämtar väder…", - "live_weather_fix_required": "Logga först en GPS-fix (Fix-knappen) för att hämta OpenWeatherMap-väder. Positionen får högst vara 6 timmar gammal.", - "live_weather_fix_stale": "Senaste GPS-fixen är äldre än 6 timmar. Logga en ny fix innan du hämtar väder.", + "live_weather_position_required": "Logga först en position (Position-knappen) för att hämta OpenWeatherMap-väder. Positionen får högst vara 6 timmar gammal.", + "live_weather_position_stale": "Senaste positionen är äldre än 6 timmar. Logga en ny position innan du hämtar väder.", "live_wind_btn": "Vind", "live_temp_btn": "T °C", "live_pressure_btn": "Lufttryck", @@ -306,8 +306,8 @@ "live_sea_state_btn": "Sjögang", "live_visibility_btn": "Sikt", "live_course_btn": "Kurs", - "live_fuel_btn": "Diesel", - "live_water_btn": "Vatten", + "live_fuel_btn": "+ Diesel", + "live_water_btn": "+ Vatten", "live_wind_entry": "Vind {{value}}", "live_temp_entry": "Temperatur {{temp}} °C", "live_pressure_entry": "Lufttryck {{value}} hPa", @@ -320,6 +320,7 @@ "live_auto_position": "Auto-position", "live_undo_hint": "Post sparad", "live_undo_btn": "Ångra", + "live_cancel": "Avbryt", "live_pressure_placeholder": "t.ex. 1013", "live_temp_placeholder": "t.ex. 18", "live_precip_placeholder": "t.ex. lätt regn", @@ -484,8 +485,8 @@ "nmea_change_engine_stop": "Engine off", "nmea_change_autopilot_on": "Autopilot on", "nmea_change_autopilot_off": "Autopilot off", - "nmea_change_gps_lost": "GPS fix lost", - "nmea_change_gps_regained": "GPS fix restored", + "nmea_change_gps_lost": "GPS-position förlorad", + "nmea_change_gps_regained": "GPS-position återställd", "nmea_change_water_temp": "Water temp. {{from}} → {{to}} °C", "nmea_change_departure": "Departure / underway", "nmea_change_anchor": "Anchored / stop", diff --git a/client/src/utils/formatEventSummary.test.ts b/client/src/utils/formatEventSummary.test.ts index 3d8a408..44d39ae 100644 --- a/client/src/utils/formatEventSummary.test.ts +++ b/client/src/utils/formatEventSummary.test.ts @@ -21,8 +21,8 @@ const t = (key: string, opts?: Record) => { 'logs.live_cast_off': 'Cast off', 'logs.live_moor': 'Moor', 'logs.live_sails': `Sails: ${opts?.sails ?? ''}`, - 'logs.live_fix': 'Fix', - 'logs.live_fix_coords': `Fix ${opts?.lat}, ${opts?.lng}`, + 'logs.live_position': 'Position', + 'logs.live_position_coords': `Position ${opts?.lat}, ${opts?.lng}`, 'logs.live_event_generic': 'Event', 'logs.live_temp_entry': `Temperature ${opts?.temp} °C`, 'logs.live_pressure_entry': `Pressure ${opts?.value} hPa`, @@ -85,14 +85,14 @@ describe('formatEventSummary', () => { expect(formatEventSummary(event, t)).toBe('Sails: Main + Genoa') }) - it('formats fix with coordinates', () => { + it('formats position with coordinates', () => { const event = normalizeLogEvent({ time: '09:00', - remarks: LIVE_EVENT_CODES.FIX, + remarks: LIVE_EVENT_CODES.POSITION, gpsLat: '54.323000', gpsLng: '10.145000' }) - expect(formatEventSummary(event, t)).toBe('Fix 54.323000, 10.145000') + expect(formatEventSummary(event, t)).toBe('Position 54.323000, 10.145000') }) it('formats pressure entry', () => { diff --git a/client/src/utils/formatEventSummary.ts b/client/src/utils/formatEventSummary.ts index 7e62007..86821b1 100644 --- a/client/src/utils/formatEventSummary.ts +++ b/client/src/utils/formatEventSummary.ts @@ -1,6 +1,7 @@ import type { TFunction } from 'i18next' import type { LogEventPayload } from './logEntryPayload.js' import { + isManualPositionEventCode, LIVE_EVENT_CODES, parseLiveCommentRemark, parseLiveFuelRemark, @@ -58,16 +59,16 @@ export function formatEventSummary(event: LogEventPayload, t: TFunction): string const stw = parseLiveStwRemark(code) if (stw) return t('logs.live_stw_entry', { speed: stw }) - if (code === LIVE_EVENT_CODES.FIX || code === LIVE_EVENT_CODES.AUTO_POSITION) { + if (isManualPositionEventCode(code) || code === LIVE_EVENT_CODES.AUTO_POSITION) { if (event.gpsLat && event.gpsLng) { const label = code === LIVE_EVENT_CODES.AUTO_POSITION ? t('logs.live_auto_position') - : t('logs.live_fix') + : t('logs.live_position') return `${label} ${event.gpsLat}, ${event.gpsLng}` } return code === LIVE_EVENT_CODES.AUTO_POSITION ? t('logs.live_auto_position') - : t('logs.live_fix') + : t('logs.live_position') } if (code === LIVE_EVENT_CODES.COURSE && event.mgk) { diff --git a/client/src/utils/liveEventCodes.ts b/client/src/utils/liveEventCodes.ts index 43f363e..183908f 100644 --- a/client/src/utils/liveEventCodes.ts +++ b/client/src/utils/liveEventCodes.ts @@ -4,7 +4,7 @@ export const LIVE_EVENT_CODES = { MOTOR_STOP: '__live:motor_stop', CAST_OFF: '__live:cast_off', MOOR: '__live:moor', - FIX: '__live:fix', + POSITION: '__live:position', AUTO_POSITION: '__live:auto_position', COURSE: '__live:course', WIND: '__live:wind', @@ -13,6 +13,9 @@ export const LIVE_EVENT_CODES = { VISIBILITY: '__live:visibility' } as const +/** @deprecated Stored in older log entries; still recognized when reading events. */ +export const LEGACY_LIVE_POSITION_REMARK = '__live:fix' + export type LiveEventCode = (typeof LIVE_EVENT_CODES)[keyof typeof LIVE_EVENT_CODES] export function liveSailsRemark(sails: string): string { @@ -148,27 +151,31 @@ export function getLastAutoPositionMs( return null } -/** Max age of a logged GPS fix for OpenWeatherMap lookups in live log. */ +/** Max age of a logged position for OpenWeatherMap lookups in live log. */ export const LIVE_LOG_WEATHER_POSITION_MAX_AGE_MS = 6 * 60 * 60 * 1000 -export type LiveLogPositionSource = 'fix' | 'auto_position' +export type LiveLogPositionSource = 'position' | 'auto_position' -export interface LiveLogPositionFix { +export interface LiveLogPosition { lat: string lng: string loggedAtMs: number source: LiveLogPositionSource } -function isPositionEventCode(code: string): boolean { - return code === LIVE_EVENT_CODES.FIX || code === LIVE_EVENT_CODES.AUTO_POSITION +export function isManualPositionEventCode(code: string): boolean { + return code === LIVE_EVENT_CODES.POSITION || code === LEGACY_LIVE_POSITION_REMARK } -/** Latest FIX or auto-position event with GPS coordinates (any age). */ -export function getLatestPositionFix( +function isPositionEventCode(code: string): boolean { + return isManualPositionEventCode(code) || code === LIVE_EVENT_CODES.AUTO_POSITION +} + +/** Latest manual or auto-position event with GPS coordinates (any age). */ +export function getLatestLoggedPosition( events: Array<{ remarks: string; time: string; gpsLat?: string; gpsLng?: string }>, entryDate: string -): LiveLogPositionFix | null { +): LiveLogPosition | null { for (let i = events.length - 1; i >= 0; i--) { const event = events[i] const code = event.remarks.trim() @@ -182,20 +189,20 @@ export function getLatestPositionFix( lat, lng, loggedAtMs, - source: code === LIVE_EVENT_CODES.FIX ? 'fix' : 'auto_position' + source: isManualPositionEventCode(code) ? 'position' : 'auto_position' } } return null } -/** GPS fix for weather if logged within `maxAgeMs` (default 6 h). */ -export function getLastPositionFixWithin( +/** Logged position for weather if recorded within `maxAgeMs` (default 6 h). */ +export function getLastLoggedPositionWithin( events: Array<{ remarks: string; time: string; gpsLat?: string; gpsLng?: string }>, entryDate: string, maxAgeMs: number = LIVE_LOG_WEATHER_POSITION_MAX_AGE_MS, nowMs: number = Date.now() -): LiveLogPositionFix | null { - const latest = getLatestPositionFix(events, entryDate) +): LiveLogPosition | null { + const latest = getLatestLoggedPosition(events, entryDate) if (!latest) return null if (nowMs - latest.loggedAtMs > maxAgeMs) return null return latest diff --git a/client/src/utils/liveLogPosition.test.ts b/client/src/utils/liveLogPosition.test.ts index d1a3ae3..ce54b8c 100644 --- a/client/src/utils/liveLogPosition.test.ts +++ b/client/src/utils/liveLogPosition.test.ts @@ -1,54 +1,67 @@ import { describe, expect, it } from 'vitest' import { - getLastPositionFixWithin, - getLatestPositionFix, + getLastLoggedPositionWithin, + getLatestLoggedPosition, + LEGACY_LIVE_POSITION_REMARK, LIVE_EVENT_CODES, LIVE_LOG_WEATHER_POSITION_MAX_AGE_MS } from './liveEventCodes.js' -const entryDate = '2026-06-01' - -describe('live log position fix', () => { - it('returns latest fix with coordinates', () => { +describe('live log position', () => { + it('returns latest position with coordinates', () => { + const entryDate = '2026-06-01' const events = [ - { remarks: LIVE_EVENT_CODES.FIX, time: '08:00', gpsLat: '54.1', gpsLng: '10.2' }, - { remarks: LIVE_EVENT_CODES.FIX, time: '12:30', gpsLat: '54.2', gpsLng: '10.3' } + { remarks: LIVE_EVENT_CODES.POSITION, time: '08:00', gpsLat: '54.1', gpsLng: '10.2' }, + { remarks: LIVE_EVENT_CODES.POSITION, time: '12:30', gpsLat: '54.2', gpsLng: '10.3' } ] - const fix = getLatestPositionFix(events, entryDate) - expect(fix?.lat).toBe('54.2') - expect(fix?.source).toBe('fix') + const position = getLatestLoggedPosition(events, entryDate) + expect(position?.lat).toBe('54.2') + expect(position?.source).toBe('position') }) - it('accepts auto-position with GPS', () => { + it('reads legacy __live:fix remarks', () => { + const entryDate = '2026-06-01' + const events = [ + { remarks: LEGACY_LIVE_POSITION_REMARK, time: '09:00', gpsLat: '54.5', gpsLng: '10.5' } + ] + const position = getLatestLoggedPosition(events, entryDate) + expect(position?.lat).toBe('54.5') + expect(position?.source).toBe('position') + }) + + it('prefers auto-position source when applicable', () => { + const entryDate = '2026-06-01' const events = [ { remarks: LIVE_EVENT_CODES.AUTO_POSITION, time: '14:00', - gpsLat: '55.0', - gpsLng: '11.0' + gpsLat: '54.3', + gpsLng: '10.4' } ] - expect(getLatestPositionFix(events, entryDate)?.source).toBe('auto_position') + expect(getLatestLoggedPosition(events, entryDate)?.source).toBe('auto_position') }) - it('rejects fix older than max age for weather', () => { - const noon = new Date(`${entryDate}T12:00:00`).getTime() + it('rejects position older than max age for weather', () => { + const entryDate = '2026-06-01' + const noon = new Date('2026-06-01T12:00:00').getTime() const events = [ - { remarks: LIVE_EVENT_CODES.FIX, time: '05:00', gpsLat: '54.0', gpsLng: '10.0' } + { remarks: LIVE_EVENT_CODES.POSITION, time: '05:00', gpsLat: '54.0', gpsLng: '10.0' } ] expect( - getLastPositionFixWithin(events, entryDate, LIVE_LOG_WEATHER_POSITION_MAX_AGE_MS, noon) + getLastLoggedPositionWithin(events, entryDate, LIVE_LOG_WEATHER_POSITION_MAX_AGE_MS, noon) ).toBeNull() - expect(getLatestPositionFix(events, entryDate)).not.toBeNull() + expect(getLatestLoggedPosition(events, entryDate)).not.toBeNull() }) - it('accepts fix within six hours', () => { - const noon = new Date(`${entryDate}T12:00:00`).getTime() + it('accepts position within six hours', () => { + const entryDate = '2026-06-01' + const noon = new Date('2026-06-01T12:00:00').getTime() const events = [ - { remarks: LIVE_EVENT_CODES.FIX, time: '07:00', gpsLat: '54.0', gpsLng: '10.0' } + { remarks: LIVE_EVENT_CODES.POSITION, time: '07:00', gpsLat: '54.0', gpsLng: '10.0' } ] expect( - getLastPositionFixWithin(events, entryDate, LIVE_LOG_WEATHER_POSITION_MAX_AGE_MS, noon) + getLastLoggedPositionWithin(events, entryDate, LIVE_LOG_WEATHER_POSITION_MAX_AGE_MS, noon) ).not.toBeNull() }) }) diff --git a/docs/plausible-events.md b/docs/plausible-events.md index b392f6f..6834c50 100644 --- a/docs/plausible-events.md +++ b/docs/plausible-events.md @@ -84,7 +84,7 @@ Property `action` bei **Live Log Event Logged** — stabile englische Schlüssel | `temp` | Temperatur | | `precip` | Niederschlag | | `sea_state` | Seegang | -| `fix` | GPS-Fix (manuell) | +| `position` | GPS-Position (manuell) | | `comment` | Kommentar | | `voice` | Sprachnotiz (Modal gespeichert) | | `undo` | Letztes Ereignis rückgängig | @@ -137,7 +137,7 @@ Empfohlene Goal-Ketten für Auswertung (nur Business!): 7. **Kontosicherheit:** Profile Opened → Passkey Added / Local PIN Set / Recovery Rotated; Last Passkey Remove Hinted → Account Deleted (selten, aber aussagekräftig) 8. **Internationalisierung:** Language Changed (Verteilung `to`, Pfade mit Übersetzungs-Feedback) 9. **NMEA-Import:** NMEA Uploaded → NMEA Imported (Modus, `events`, optional Track; Upload-Funnel vs. Abbruch) -10. **Live-Journal:** Live Log Opened → Live Log Event Logged (Verteilung `action`; z. B. `fix`, `course`, `motor_start`) → Photo Uploaded / Voice Memo Uploaded (Filter `context`: `live_log`) +10. **Live-Journal:** Live Log Opened → Live Log Event Logged (Verteilung `action`; z. B. `position`, `course`, `motor_start`) → Photo Uploaded / Voice Memo Uploaded (Filter `context`: `live_log`) 11. **OpenWeatherMap:** OWM Weather Fetched (Verteilung `source`; Live-Journal vs. Reisetag-Editor) 12. **PWA-Stabilitaet:** PWA Boot Watchdog Soft → PWA Boot Watchdog Hard → PWA Boot Watchdog Fallback → PWA Boot Watchdog Manual Repair