Add SOG and STW live-log actions and capitalize motor labels.

SOG prefills from GPS speed when available; STW is entered manually. Motor journal entries now read “Motor Start” / “Motor Stop”.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-31 21:17:51 +02:00
parent 5b47415d55
commit 74282f50d0
11 changed files with 165 additions and 16 deletions
+7
View File
@@ -3311,6 +3311,13 @@ html.theme-cupertino .events-scroll-container {
font-size: 17px;
}
.live-log-modal-hint {
margin: -8px 0 12px;
font-size: 13px;
color: var(--app-text-muted);
line-height: 1.4;
}
.live-log-sail-pills {
margin-bottom: 16px;
}
+56 -2
View File
@@ -10,6 +10,7 @@ import {
Droplets,
FileText,
Fuel,
Gauge,
MapPin,
MessageSquare,
Radio,
@@ -38,6 +39,8 @@ import {
liveFuelRemark,
livePrecipRemark,
liveSailsRemark,
liveSogRemark,
liveStwRemark,
liveTempRemark,
liveWaterRemark
} from '../utils/liveEventCodes.js'
@@ -63,6 +66,8 @@ type LiveModal =
| 'course'
| 'fuel'
| 'water'
| 'sog'
| 'stw'
const AUTO_POSITION_INTERVAL_MS = 3 * 60 * 60 * 1000
const AUTO_POSITION_CHECK_MS = 60_000
@@ -235,6 +240,17 @@ export default function LiveLogView({
setModal(type)
}
const openSogModal = async () => {
let prefill = ''
try {
const pos = await getCurrentPosition()
if (pos.speedKn != null) prefill = String(pos.speedKn)
} catch {
// Manual entry when GPS speed unavailable
}
openValueModal('sog', prefill)
}
const handleMotorToggle = () => {
hapticPulse()
void runQuickAction(async () => {
@@ -405,6 +421,28 @@ export default function LiveLogView({
}, 'live_water')
break
}
case 'sog': {
const speedKn = parseFloat(primary.replace(',', '.'))
if (!Number.isFinite(speedKn) || speedKn < 0) return
setModal('none')
void runQuickAction(async () => {
await appendQuickEvent(logbookId, entryId, {
remarks: liveSogRemark(String(speedKn))
})
}, 'live_sog')
break
}
case 'stw': {
const speedKn = parseFloat(primary.replace(',', '.'))
if (!Number.isFinite(speedKn) || speedKn < 0) return
setModal('none')
void runQuickAction(async () => {
await appendQuickEvent(logbookId, entryId, {
remarks: liveStwRemark(String(speedKn))
})
}, 'live_stw')
break
}
default:
break
}
@@ -479,6 +517,14 @@ export default function LiveLogView({
<Compass size={18} />
{t('logs.live_course_btn')}
</button>
<button type="button" className="live-log-action-btn" onClick={() => void openSogModal()} disabled={busy}>
<Gauge size={18} />
{t('logs.live_sog_btn')}
</button>
<button type="button" className="live-log-action-btn" onClick={() => openValueModal('stw')} disabled={busy}>
<Gauge size={18} style={{ transform: 'scaleX(-1)' }} />
{t('logs.live_stw_btn')}
</button>
<button type="button" className="live-log-action-btn" onClick={() => openValueModal('fuel')} disabled={busy}>
<Fuel size={18} />
{t('logs.live_fuel_btn')}
@@ -610,7 +656,7 @@ export default function LiveLogView({
</div>
)}
{['pressure', 'temp', 'precip', 'sea_state', 'course', 'fuel', 'water'].includes(modal) && (
{['pressure', 'temp', 'precip', 'sea_state', 'course', 'fuel', 'water', 'sog', 'stw'].includes(modal) && (
<div className="live-log-modal-backdrop" onClick={() => setModal('none')}>
<div className="live-log-modal glass" onClick={(e) => e.stopPropagation()}>
<h3>
@@ -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')}
</h3>
{modal === 'sog' && (
<p className="live-log-modal-hint">{t('logs.live_sog_hint')}</p>
)}
<input
type="text"
inputMode="decimal"
className="input-text"
value={valueInput}
onChange={(e) => 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() }}
+9 -2
View File
@@ -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?",
+9 -2
View File
@@ -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?",
+9 -2
View File
@@ -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?",
+9 -2
View File
@@ -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?",
+9 -2
View File
@@ -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?",
+22 -3
View File
@@ -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<string, unknown>) => {
const map: Record<string, string> = {
'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<string, unknown>) => {
'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')
})
})
+8
View File
@@ -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
+9 -1
View File
@@ -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<GeoCoordinates> {
@@ -12,9 +16,13 @@ export function getCurrentPosition(timeoutMs = 15000): Promise<GeoCoordinates> {
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),
+18
View File
@@ -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 }>,