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({
)}
-
)}
-
{t('logs.confirm_no')}
+
{t('logs.live_cancel')}
)}
- {modal === 'fix' && (
+ {modal === 'position' && (
{ if (e.target === e.currentTarget) closeModal() }}
>
e.stopPropagation()}>
-
{t('logs.live_fix')}
- {fixGpsUnavailable && (
+
{t('logs.live_position')}
+ {positionGpsUnavailable && (
<>
{t('logs.live_gps_start_hint')}
-
{t('logs.live_fix_manual_hint')}
+
{t('logs.live_position_manual_hint')}
>
)}
-
- setModal('none')}>{t('logs.confirm_no')}
+ setModal('none')}>{t('logs.live_cancel')}
{t('logs.live_sails_confirm')}
@@ -1338,7 +1338,7 @@ export default function LiveLogView({
onKeyDown={(e) => { if (e.key === 'Enter') confirmValueModal() }}
/>
- setModal('none')}>{t('logs.confirm_no')}
+ setModal('none')}>{t('logs.live_cancel')}
{t('logs.live_sails_confirm')}
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