e.stopPropagation()}>
@@ -621,9 +667,15 @@ export default function LiveLogView({
{modal === 'course' && t('logs.live_course_btn')}
{modal === 'fuel' && t('logs.live_fuel_btn')}
{modal === 'water' && t('logs.live_water_btn')}
+ {modal === 'sog' && t('logs.live_sog_btn')}
+ {modal === 'stw' && t('logs.live_stw_btn')}
+ {modal === 'sog' && (
+
{t('logs.live_sog_hint')}
+ )}
setValueInput(e.target.value)}
@@ -634,7 +686,9 @@ export default function LiveLogView({
: modal === 'sea_state' ? t('logs.live_sea_state_placeholder')
: modal === 'course' ? t('logs.live_course_placeholder')
: modal === 'fuel' ? t('logs.live_fuel_placeholder')
- : t('logs.live_water_placeholder')
+ : modal === 'water' ? t('logs.live_water_placeholder')
+ : modal === 'sog' ? t('logs.live_sog_placeholder')
+ : t('logs.live_stw_placeholder')
}
autoFocus
onKeyDown={(e) => { if (e.key === 'Enter') confirmValueModal() }}
diff --git a/client/src/i18n/locales/da.json b/client/src/i18n/locales/da.json
index 5c621c1..d530476 100644
--- a/client/src/i18n/locales/da.json
+++ b/client/src/i18n/locales/da.json
@@ -209,8 +209,8 @@
"live_stream_label": "Hændelseslog",
"live_stream_title": "Journal",
"live_no_events": "Ingen indtastninger endnu — tryk på en handling.",
- "live_motor_start": "Motor start",
- "live_motor_stop": "Motor stop",
+ "live_motor_start": "Motor Start",
+ "live_motor_stop": "Motor Stop",
"live_cast_off": "Afsejling",
"live_moor": "Anløb",
"live_sails_btn": "Sejl",
@@ -251,6 +251,13 @@
"live_course_placeholder": "f.eks. 245",
"live_fuel_placeholder": "Optankede liter",
"live_water_placeholder": "Optankede liter",
+ "live_sog_btn": "SOG",
+ "live_stw_btn": "STW",
+ "live_sog_entry": "SOG {{speed}} kn",
+ "live_stw_entry": "STW {{speed}} kn",
+ "live_sog_placeholder": "f.eks. 5,2",
+ "live_stw_placeholder": "f.eks. 4,8",
+ "live_sog_hint": "Fart over grund (kn) — GPS-værdi forudfyldes, hvis tilgængelig.",
"delete_entry": "Slet tag",
"delete_confirm": "Er du sikker på, at du vil slette denne rejsedag permanent?",
"carry_over_tanks_title": "Overføre data fra den foregående dag?",
diff --git a/client/src/i18n/locales/de.json b/client/src/i18n/locales/de.json
index 72b9387..4b9bb8c 100644
--- a/client/src/i18n/locales/de.json
+++ b/client/src/i18n/locales/de.json
@@ -209,8 +209,8 @@
"live_stream_label": "Ereignisprotokoll",
"live_stream_title": "Journal",
"live_no_events": "Noch keine Einträge — tippe auf eine Aktion.",
- "live_motor_start": "Motor start",
- "live_motor_stop": "Motor stop",
+ "live_motor_start": "Motor Start",
+ "live_motor_stop": "Motor Stop",
"live_cast_off": "Ablegen",
"live_moor": "Anlegen",
"live_sails_btn": "Segel",
@@ -251,6 +251,13 @@
"live_course_placeholder": "z. B. 245",
"live_fuel_placeholder": "Nachgefüllte Liter",
"live_water_placeholder": "Nachgefüllte Liter",
+ "live_sog_btn": "SOG",
+ "live_stw_btn": "STW",
+ "live_sog_entry": "SOG {{speed}} kn",
+ "live_stw_entry": "STW {{speed}} kn",
+ "live_sog_placeholder": "z. B. 5,2",
+ "live_stw_placeholder": "z. B. 4,8",
+ "live_sog_hint": "Fahrt über Grund (kn) — GPS-Wert wird vorgefüllt, wenn verfügbar.",
"delete_entry": "Tag löschen",
"delete_confirm": "Bist du sicher, dass du diesen Reisetag unwiderruflich löschen möchtest?",
"carry_over_tanks_title": "Daten vom Vortag übernehmen?",
diff --git a/client/src/i18n/locales/en.json b/client/src/i18n/locales/en.json
index 19055e4..449dc76 100644
--- a/client/src/i18n/locales/en.json
+++ b/client/src/i18n/locales/en.json
@@ -209,8 +209,8 @@
"live_stream_label": "Event log",
"live_stream_title": "Journal",
"live_no_events": "No entries yet — tap an action.",
- "live_motor_start": "Engine start",
- "live_motor_stop": "Engine stop",
+ "live_motor_start": "Engine Start",
+ "live_motor_stop": "Engine Stop",
"live_cast_off": "Cast off",
"live_moor": "Moor",
"live_sails_btn": "Sails",
@@ -251,6 +251,13 @@
"live_course_placeholder": "e.g. 245",
"live_fuel_placeholder": "Liters refilled",
"live_water_placeholder": "Liters refilled",
+ "live_sog_btn": "SOG",
+ "live_stw_btn": "STW",
+ "live_sog_entry": "SOG {{speed}} kn",
+ "live_stw_entry": "STW {{speed}} kn",
+ "live_sog_placeholder": "e.g. 5.2",
+ "live_stw_placeholder": "e.g. 4.8",
+ "live_sog_hint": "Speed over ground (kn) — prefilled from GPS when available.",
"delete_entry": "Delete Day",
"delete_confirm": "Are you sure you want to permanently delete this travel day?",
"carry_over_tanks_title": "Carry over from previous day?",
diff --git a/client/src/i18n/locales/nb.json b/client/src/i18n/locales/nb.json
index 6133937..c3ed7a3 100644
--- a/client/src/i18n/locales/nb.json
+++ b/client/src/i18n/locales/nb.json
@@ -209,8 +209,8 @@
"live_stream_label": "Hendelseslogg",
"live_stream_title": "Journal",
"live_no_events": "Ingen oppføringer ennå — trykk på en handling.",
- "live_motor_start": "Motor start",
- "live_motor_stop": "Motor stopp",
+ "live_motor_start": "Motor Start",
+ "live_motor_stop": "Motor Stopp",
"live_cast_off": "Avreise",
"live_moor": "Anløp",
"live_sails_btn": "Seil",
@@ -251,6 +251,13 @@
"live_course_placeholder": "f.eks. 245",
"live_fuel_placeholder": "Påfylte liter",
"live_water_placeholder": "Påfylte liter",
+ "live_sog_btn": "SOG",
+ "live_stw_btn": "STW",
+ "live_sog_entry": "SOG {{speed}} kn",
+ "live_stw_entry": "STW {{speed}} kn",
+ "live_sog_placeholder": "f.eks. 5,2",
+ "live_stw_placeholder": "f.eks. 4,8",
+ "live_sog_hint": "Fart over grunn (kn) — GPS-verdi fylles inn hvis tilgjengelig.",
"delete_entry": "Slett tagg",
"delete_confirm": "Er du sikker på at du vil slette denne reisedagen permanent?",
"carry_over_tanks_title": "Overføre data fra dagen før?",
diff --git a/client/src/i18n/locales/sv.json b/client/src/i18n/locales/sv.json
index 989fc72..cd5d75e 100644
--- a/client/src/i18n/locales/sv.json
+++ b/client/src/i18n/locales/sv.json
@@ -209,8 +209,8 @@
"live_stream_label": "Händelselogg",
"live_stream_title": "Journal",
"live_no_events": "Inga poster ännu — tryck på en åtgärd.",
- "live_motor_start": "Motor start",
- "live_motor_stop": "Motor stopp",
+ "live_motor_start": "Motor Start",
+ "live_motor_stop": "Motor Stopp",
"live_cast_off": "Avgång",
"live_moor": "Anlöp",
"live_sails_btn": "Segel",
@@ -251,6 +251,13 @@
"live_course_placeholder": "t.ex. 245",
"live_fuel_placeholder": "Påfyllda liter",
"live_water_placeholder": "Påfyllda liter",
+ "live_sog_btn": "SOG",
+ "live_stw_btn": "STW",
+ "live_sog_entry": "SOG {{speed}} kn",
+ "live_stw_entry": "STW {{speed}} kn",
+ "live_sog_placeholder": "t.ex. 5,2",
+ "live_stw_placeholder": "t.ex. 4,8",
+ "live_sog_hint": "Fart över grund (kn) — GPS-värde fylls i om tillgängligt.",
"delete_entry": "Ta bort tagg",
"delete_confirm": "Är du säker på att du vill radera den här resedagen permanent?",
"carry_over_tanks_title": "Överföra data från föregående dag?",
diff --git a/client/src/utils/formatEventSummary.test.ts b/client/src/utils/formatEventSummary.test.ts
index b24b8a3..1215084 100644
--- a/client/src/utils/formatEventSummary.test.ts
+++ b/client/src/utils/formatEventSummary.test.ts
@@ -4,6 +4,7 @@ import {
LIVE_EVENT_CODES,
liveCommentRemark,
liveSailsRemark,
+ liveSogRemark,
parseLiveCommentRemark,
parseLiveSailsRemark
} from './liveEventCodes.js'
@@ -12,8 +13,8 @@ import { normalizeLogEvent } from './logEntryPayload.js'
const t = (key: string, opts?: Record
) => {
const map: Record = {
- 'logs.live_motor_start': 'Motor start',
- 'logs.live_motor_stop': 'Motor stop',
+ 'logs.live_motor_start': 'Motor Start',
+ 'logs.live_motor_stop': 'Motor Stop',
'logs.live_cast_off': 'Cast off',
'logs.live_moor': 'Moor',
'logs.live_sails': `Sails: ${opts?.sails ?? ''}`,
@@ -24,6 +25,8 @@ const t = (key: string, opts?: Record) => {
'logs.live_pressure_entry': `Pressure ${opts?.value} hPa`,
'logs.live_wind_entry': `Wind ${opts?.value}`,
'logs.live_course_entry': `Course ${opts?.course}`,
+ 'logs.live_sog_entry': `SOG ${opts?.speed} kn`,
+ 'logs.live_stw_entry': `STW ${opts?.speed} kn`,
'logs.event_mgk': 'Course',
'logs.event_wind_pressure': 'Pressure'
}
@@ -57,7 +60,7 @@ describe('liveEventCodes', () => {
describe('formatEventSummary', () => {
it('formats live motor start', () => {
const event = normalizeLogEvent({ time: '08:10', remarks: LIVE_EVENT_CODES.MOTOR_START })
- expect(formatEventSummary(event, t)).toBe('Motor start')
+ expect(formatEventSummary(event, t)).toBe('Motor Start')
})
it('formats sails remark', () => {
@@ -87,4 +90,20 @@ describe('formatEventSummary', () => {
})
expect(formatEventSummary(event, t)).toBe('Pressure 1013 hPa')
})
+
+ it('formats SOG entry', () => {
+ const event = normalizeLogEvent({
+ time: '10:15',
+ remarks: liveSogRemark('5.2')
+ })
+ expect(formatEventSummary(event, t)).toBe('SOG 5.2 kn')
+ })
+
+ it('formats STW entry', () => {
+ const event = normalizeLogEvent({
+ time: '10:20',
+ remarks: '__live:stw:4.8'
+ })
+ expect(formatEventSummary(event, t)).toBe('STW 4.8 kn')
+ })
})
diff --git a/client/src/utils/formatEventSummary.ts b/client/src/utils/formatEventSummary.ts
index 315b22a..05a8d84 100644
--- a/client/src/utils/formatEventSummary.ts
+++ b/client/src/utils/formatEventSummary.ts
@@ -6,6 +6,8 @@ import {
parseLiveFuelRemark,
parseLivePrecipRemark,
parseLiveSailsRemark,
+ parseLiveSogRemark,
+ parseLiveStwRemark,
parseLiveTempRemark,
parseLiveWaterRemark
} from './liveEventCodes.js'
@@ -36,6 +38,12 @@ export function formatEventSummary(event: LogEventPayload, t: TFunction): string
const water = parseLiveWaterRemark(code)
if (water) return t('logs.live_water_entry', { liters: water })
+ const sog = parseLiveSogRemark(code)
+ if (sog) return t('logs.live_sog_entry', { speed: sog })
+
+ 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 (event.gpsLat && event.gpsLng) {
const label = code === LIVE_EVENT_CODES.AUTO_POSITION
diff --git a/client/src/utils/geolocation.ts b/client/src/utils/geolocation.ts
index fc614db..52048bc 100644
--- a/client/src/utils/geolocation.ts
+++ b/client/src/utils/geolocation.ts
@@ -1,6 +1,10 @@
+const MPS_TO_KNOTS = 1.9438444924406
+
export interface GeoCoordinates {
lat: string
lng: string
+ /** SOG from GPS when available (kn), otherwise null. */
+ speedKn: number | null
}
export function getCurrentPosition(timeoutMs = 15000): Promise {
@@ -12,9 +16,13 @@ export function getCurrentPosition(timeoutMs = 15000): Promise {
navigator.geolocation.getCurrentPosition(
(pos) => {
+ const speedKn = pos.coords.speed != null && Number.isFinite(pos.coords.speed)
+ ? Number((pos.coords.speed * MPS_TO_KNOTS).toFixed(1))
+ : null
resolve({
lat: pos.coords.latitude.toFixed(6),
- lng: pos.coords.longitude.toFixed(6)
+ lng: pos.coords.longitude.toFixed(6),
+ speedKn
})
},
(err) => reject(err),
diff --git a/client/src/utils/liveEventCodes.ts b/client/src/utils/liveEventCodes.ts
index 230081e..78ff22b 100644
--- a/client/src/utils/liveEventCodes.ts
+++ b/client/src/utils/liveEventCodes.ts
@@ -38,6 +38,14 @@ export function liveWaterRemark(liters: string): string {
return `__live:water:${liters}`
}
+export function liveSogRemark(speedKn: string): string {
+ return `__live:sog:${speedKn}`
+}
+
+export function liveStwRemark(speedKn: string): string {
+ return `__live:stw:${speedKn}`
+}
+
export function parseLiveSailsRemark(remarks: string): string | null {
const prefix = '__live:sails:'
return remarks.startsWith(prefix) ? remarks.slice(prefix.length) : null
@@ -68,6 +76,16 @@ export function parseLiveWaterRemark(remarks: string): string | null {
return remarks.startsWith(prefix) ? remarks.slice(prefix.length) : null
}
+export function parseLiveSogRemark(remarks: string): string | null {
+ const prefix = '__live:sog:'
+ return remarks.startsWith(prefix) ? remarks.slice(prefix.length) : null
+}
+
+export function parseLiveStwRemark(remarks: string): string | null {
+ const prefix = '__live:stw:'
+ return remarks.startsWith(prefix) ? remarks.slice(prefix.length) : null
+}
+
/** Derive motor running state from event history (survives reload). */
export function isMotorRunningFromEvents(
events: Array<{ remarks: string }>,