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:
@@ -3311,6 +3311,13 @@ html.theme-cupertino .events-scroll-container {
|
|||||||
font-size: 17px;
|
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 {
|
.live-log-sail-pills {
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
Droplets,
|
Droplets,
|
||||||
FileText,
|
FileText,
|
||||||
Fuel,
|
Fuel,
|
||||||
|
Gauge,
|
||||||
MapPin,
|
MapPin,
|
||||||
MessageSquare,
|
MessageSquare,
|
||||||
Radio,
|
Radio,
|
||||||
@@ -38,6 +39,8 @@ import {
|
|||||||
liveFuelRemark,
|
liveFuelRemark,
|
||||||
livePrecipRemark,
|
livePrecipRemark,
|
||||||
liveSailsRemark,
|
liveSailsRemark,
|
||||||
|
liveSogRemark,
|
||||||
|
liveStwRemark,
|
||||||
liveTempRemark,
|
liveTempRemark,
|
||||||
liveWaterRemark
|
liveWaterRemark
|
||||||
} from '../utils/liveEventCodes.js'
|
} from '../utils/liveEventCodes.js'
|
||||||
@@ -63,6 +66,8 @@ type LiveModal =
|
|||||||
| 'course'
|
| 'course'
|
||||||
| 'fuel'
|
| 'fuel'
|
||||||
| 'water'
|
| 'water'
|
||||||
|
| 'sog'
|
||||||
|
| 'stw'
|
||||||
|
|
||||||
const AUTO_POSITION_INTERVAL_MS = 3 * 60 * 60 * 1000
|
const AUTO_POSITION_INTERVAL_MS = 3 * 60 * 60 * 1000
|
||||||
const AUTO_POSITION_CHECK_MS = 60_000
|
const AUTO_POSITION_CHECK_MS = 60_000
|
||||||
@@ -235,6 +240,17 @@ export default function LiveLogView({
|
|||||||
setModal(type)
|
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 = () => {
|
const handleMotorToggle = () => {
|
||||||
hapticPulse()
|
hapticPulse()
|
||||||
void runQuickAction(async () => {
|
void runQuickAction(async () => {
|
||||||
@@ -405,6 +421,28 @@ export default function LiveLogView({
|
|||||||
}, 'live_water')
|
}, 'live_water')
|
||||||
break
|
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:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -479,6 +517,14 @@ export default function LiveLogView({
|
|||||||
<Compass size={18} />
|
<Compass size={18} />
|
||||||
{t('logs.live_course_btn')}
|
{t('logs.live_course_btn')}
|
||||||
</button>
|
</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}>
|
<button type="button" className="live-log-action-btn" onClick={() => openValueModal('fuel')} disabled={busy}>
|
||||||
<Fuel size={18} />
|
<Fuel size={18} />
|
||||||
{t('logs.live_fuel_btn')}
|
{t('logs.live_fuel_btn')}
|
||||||
@@ -610,7 +656,7 @@ export default function LiveLogView({
|
|||||||
</div>
|
</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-backdrop" onClick={() => setModal('none')}>
|
||||||
<div className="live-log-modal glass" onClick={(e) => e.stopPropagation()}>
|
<div className="live-log-modal glass" onClick={(e) => e.stopPropagation()}>
|
||||||
<h3>
|
<h3>
|
||||||
@@ -621,9 +667,15 @@ export default function LiveLogView({
|
|||||||
{modal === 'course' && t('logs.live_course_btn')}
|
{modal === 'course' && t('logs.live_course_btn')}
|
||||||
{modal === 'fuel' && t('logs.live_fuel_btn')}
|
{modal === 'fuel' && t('logs.live_fuel_btn')}
|
||||||
{modal === 'water' && t('logs.live_water_btn')}
|
{modal === 'water' && t('logs.live_water_btn')}
|
||||||
|
{modal === 'sog' && t('logs.live_sog_btn')}
|
||||||
|
{modal === 'stw' && t('logs.live_stw_btn')}
|
||||||
</h3>
|
</h3>
|
||||||
|
{modal === 'sog' && (
|
||||||
|
<p className="live-log-modal-hint">{t('logs.live_sog_hint')}</p>
|
||||||
|
)}
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
|
inputMode="decimal"
|
||||||
className="input-text"
|
className="input-text"
|
||||||
value={valueInput}
|
value={valueInput}
|
||||||
onChange={(e) => setValueInput(e.target.value)}
|
onChange={(e) => setValueInput(e.target.value)}
|
||||||
@@ -634,7 +686,9 @@ export default function LiveLogView({
|
|||||||
: modal === 'sea_state' ? t('logs.live_sea_state_placeholder')
|
: modal === 'sea_state' ? t('logs.live_sea_state_placeholder')
|
||||||
: modal === 'course' ? t('logs.live_course_placeholder')
|
: modal === 'course' ? t('logs.live_course_placeholder')
|
||||||
: modal === 'fuel' ? t('logs.live_fuel_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
|
autoFocus
|
||||||
onKeyDown={(e) => { if (e.key === 'Enter') confirmValueModal() }}
|
onKeyDown={(e) => { if (e.key === 'Enter') confirmValueModal() }}
|
||||||
|
|||||||
@@ -209,8 +209,8 @@
|
|||||||
"live_stream_label": "Hændelseslog",
|
"live_stream_label": "Hændelseslog",
|
||||||
"live_stream_title": "Journal",
|
"live_stream_title": "Journal",
|
||||||
"live_no_events": "Ingen indtastninger endnu — tryk på en handling.",
|
"live_no_events": "Ingen indtastninger endnu — tryk på en handling.",
|
||||||
"live_motor_start": "Motor start",
|
"live_motor_start": "Motor Start",
|
||||||
"live_motor_stop": "Motor stop",
|
"live_motor_stop": "Motor Stop",
|
||||||
"live_cast_off": "Afsejling",
|
"live_cast_off": "Afsejling",
|
||||||
"live_moor": "Anløb",
|
"live_moor": "Anløb",
|
||||||
"live_sails_btn": "Sejl",
|
"live_sails_btn": "Sejl",
|
||||||
@@ -251,6 +251,13 @@
|
|||||||
"live_course_placeholder": "f.eks. 245",
|
"live_course_placeholder": "f.eks. 245",
|
||||||
"live_fuel_placeholder": "Optankede liter",
|
"live_fuel_placeholder": "Optankede liter",
|
||||||
"live_water_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_entry": "Slet tag",
|
||||||
"delete_confirm": "Er du sikker på, at du vil slette denne rejsedag permanent?",
|
"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?",
|
"carry_over_tanks_title": "Overføre data fra den foregående dag?",
|
||||||
|
|||||||
@@ -209,8 +209,8 @@
|
|||||||
"live_stream_label": "Ereignisprotokoll",
|
"live_stream_label": "Ereignisprotokoll",
|
||||||
"live_stream_title": "Journal",
|
"live_stream_title": "Journal",
|
||||||
"live_no_events": "Noch keine Einträge — tippe auf eine Aktion.",
|
"live_no_events": "Noch keine Einträge — tippe auf eine Aktion.",
|
||||||
"live_motor_start": "Motor start",
|
"live_motor_start": "Motor Start",
|
||||||
"live_motor_stop": "Motor stop",
|
"live_motor_stop": "Motor Stop",
|
||||||
"live_cast_off": "Ablegen",
|
"live_cast_off": "Ablegen",
|
||||||
"live_moor": "Anlegen",
|
"live_moor": "Anlegen",
|
||||||
"live_sails_btn": "Segel",
|
"live_sails_btn": "Segel",
|
||||||
@@ -251,6 +251,13 @@
|
|||||||
"live_course_placeholder": "z. B. 245",
|
"live_course_placeholder": "z. B. 245",
|
||||||
"live_fuel_placeholder": "Nachgefüllte Liter",
|
"live_fuel_placeholder": "Nachgefüllte Liter",
|
||||||
"live_water_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_entry": "Tag löschen",
|
||||||
"delete_confirm": "Bist du sicher, dass du diesen Reisetag unwiderruflich löschen möchtest?",
|
"delete_confirm": "Bist du sicher, dass du diesen Reisetag unwiderruflich löschen möchtest?",
|
||||||
"carry_over_tanks_title": "Daten vom Vortag übernehmen?",
|
"carry_over_tanks_title": "Daten vom Vortag übernehmen?",
|
||||||
|
|||||||
@@ -209,8 +209,8 @@
|
|||||||
"live_stream_label": "Event log",
|
"live_stream_label": "Event log",
|
||||||
"live_stream_title": "Journal",
|
"live_stream_title": "Journal",
|
||||||
"live_no_events": "No entries yet — tap an action.",
|
"live_no_events": "No entries yet — tap an action.",
|
||||||
"live_motor_start": "Engine start",
|
"live_motor_start": "Engine Start",
|
||||||
"live_motor_stop": "Engine stop",
|
"live_motor_stop": "Engine Stop",
|
||||||
"live_cast_off": "Cast off",
|
"live_cast_off": "Cast off",
|
||||||
"live_moor": "Moor",
|
"live_moor": "Moor",
|
||||||
"live_sails_btn": "Sails",
|
"live_sails_btn": "Sails",
|
||||||
@@ -251,6 +251,13 @@
|
|||||||
"live_course_placeholder": "e.g. 245",
|
"live_course_placeholder": "e.g. 245",
|
||||||
"live_fuel_placeholder": "Liters refilled",
|
"live_fuel_placeholder": "Liters refilled",
|
||||||
"live_water_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_entry": "Delete Day",
|
||||||
"delete_confirm": "Are you sure you want to permanently delete this travel day?",
|
"delete_confirm": "Are you sure you want to permanently delete this travel day?",
|
||||||
"carry_over_tanks_title": "Carry over from previous day?",
|
"carry_over_tanks_title": "Carry over from previous day?",
|
||||||
|
|||||||
@@ -209,8 +209,8 @@
|
|||||||
"live_stream_label": "Hendelseslogg",
|
"live_stream_label": "Hendelseslogg",
|
||||||
"live_stream_title": "Journal",
|
"live_stream_title": "Journal",
|
||||||
"live_no_events": "Ingen oppføringer ennå — trykk på en handling.",
|
"live_no_events": "Ingen oppføringer ennå — trykk på en handling.",
|
||||||
"live_motor_start": "Motor start",
|
"live_motor_start": "Motor Start",
|
||||||
"live_motor_stop": "Motor stopp",
|
"live_motor_stop": "Motor Stopp",
|
||||||
"live_cast_off": "Avreise",
|
"live_cast_off": "Avreise",
|
||||||
"live_moor": "Anløp",
|
"live_moor": "Anløp",
|
||||||
"live_sails_btn": "Seil",
|
"live_sails_btn": "Seil",
|
||||||
@@ -251,6 +251,13 @@
|
|||||||
"live_course_placeholder": "f.eks. 245",
|
"live_course_placeholder": "f.eks. 245",
|
||||||
"live_fuel_placeholder": "Påfylte liter",
|
"live_fuel_placeholder": "Påfylte liter",
|
||||||
"live_water_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_entry": "Slett tagg",
|
||||||
"delete_confirm": "Er du sikker på at du vil slette denne reisedagen permanent?",
|
"delete_confirm": "Er du sikker på at du vil slette denne reisedagen permanent?",
|
||||||
"carry_over_tanks_title": "Overføre data fra dagen før?",
|
"carry_over_tanks_title": "Overføre data fra dagen før?",
|
||||||
|
|||||||
@@ -209,8 +209,8 @@
|
|||||||
"live_stream_label": "Händelselogg",
|
"live_stream_label": "Händelselogg",
|
||||||
"live_stream_title": "Journal",
|
"live_stream_title": "Journal",
|
||||||
"live_no_events": "Inga poster ännu — tryck på en åtgärd.",
|
"live_no_events": "Inga poster ännu — tryck på en åtgärd.",
|
||||||
"live_motor_start": "Motor start",
|
"live_motor_start": "Motor Start",
|
||||||
"live_motor_stop": "Motor stopp",
|
"live_motor_stop": "Motor Stopp",
|
||||||
"live_cast_off": "Avgång",
|
"live_cast_off": "Avgång",
|
||||||
"live_moor": "Anlöp",
|
"live_moor": "Anlöp",
|
||||||
"live_sails_btn": "Segel",
|
"live_sails_btn": "Segel",
|
||||||
@@ -251,6 +251,13 @@
|
|||||||
"live_course_placeholder": "t.ex. 245",
|
"live_course_placeholder": "t.ex. 245",
|
||||||
"live_fuel_placeholder": "Påfyllda liter",
|
"live_fuel_placeholder": "Påfyllda liter",
|
||||||
"live_water_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_entry": "Ta bort tagg",
|
||||||
"delete_confirm": "Är du säker på att du vill radera den här resedagen permanent?",
|
"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?",
|
"carry_over_tanks_title": "Överföra data från föregående dag?",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
LIVE_EVENT_CODES,
|
LIVE_EVENT_CODES,
|
||||||
liveCommentRemark,
|
liveCommentRemark,
|
||||||
liveSailsRemark,
|
liveSailsRemark,
|
||||||
|
liveSogRemark,
|
||||||
parseLiveCommentRemark,
|
parseLiveCommentRemark,
|
||||||
parseLiveSailsRemark
|
parseLiveSailsRemark
|
||||||
} from './liveEventCodes.js'
|
} from './liveEventCodes.js'
|
||||||
@@ -12,8 +13,8 @@ import { normalizeLogEvent } from './logEntryPayload.js'
|
|||||||
|
|
||||||
const t = (key: string, opts?: Record<string, unknown>) => {
|
const t = (key: string, opts?: Record<string, unknown>) => {
|
||||||
const map: Record<string, string> = {
|
const map: Record<string, string> = {
|
||||||
'logs.live_motor_start': 'Motor start',
|
'logs.live_motor_start': 'Motor Start',
|
||||||
'logs.live_motor_stop': 'Motor stop',
|
'logs.live_motor_stop': 'Motor Stop',
|
||||||
'logs.live_cast_off': 'Cast off',
|
'logs.live_cast_off': 'Cast off',
|
||||||
'logs.live_moor': 'Moor',
|
'logs.live_moor': 'Moor',
|
||||||
'logs.live_sails': `Sails: ${opts?.sails ?? ''}`,
|
'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_pressure_entry': `Pressure ${opts?.value} hPa`,
|
||||||
'logs.live_wind_entry': `Wind ${opts?.value}`,
|
'logs.live_wind_entry': `Wind ${opts?.value}`,
|
||||||
'logs.live_course_entry': `Course ${opts?.course}`,
|
'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_mgk': 'Course',
|
||||||
'logs.event_wind_pressure': 'Pressure'
|
'logs.event_wind_pressure': 'Pressure'
|
||||||
}
|
}
|
||||||
@@ -57,7 +60,7 @@ describe('liveEventCodes', () => {
|
|||||||
describe('formatEventSummary', () => {
|
describe('formatEventSummary', () => {
|
||||||
it('formats live motor start', () => {
|
it('formats live motor start', () => {
|
||||||
const event = normalizeLogEvent({ time: '08:10', remarks: LIVE_EVENT_CODES.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', () => {
|
it('formats sails remark', () => {
|
||||||
@@ -87,4 +90,20 @@ describe('formatEventSummary', () => {
|
|||||||
})
|
})
|
||||||
expect(formatEventSummary(event, t)).toBe('Pressure 1013 hPa')
|
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')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import {
|
|||||||
parseLiveFuelRemark,
|
parseLiveFuelRemark,
|
||||||
parseLivePrecipRemark,
|
parseLivePrecipRemark,
|
||||||
parseLiveSailsRemark,
|
parseLiveSailsRemark,
|
||||||
|
parseLiveSogRemark,
|
||||||
|
parseLiveStwRemark,
|
||||||
parseLiveTempRemark,
|
parseLiveTempRemark,
|
||||||
parseLiveWaterRemark
|
parseLiveWaterRemark
|
||||||
} from './liveEventCodes.js'
|
} from './liveEventCodes.js'
|
||||||
@@ -36,6 +38,12 @@ export function formatEventSummary(event: LogEventPayload, t: TFunction): string
|
|||||||
const water = parseLiveWaterRemark(code)
|
const water = parseLiveWaterRemark(code)
|
||||||
if (water) return t('logs.live_water_entry', { liters: water })
|
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 (code === LIVE_EVENT_CODES.FIX || code === LIVE_EVENT_CODES.AUTO_POSITION) {
|
||||||
if (event.gpsLat && event.gpsLng) {
|
if (event.gpsLat && event.gpsLng) {
|
||||||
const label = code === LIVE_EVENT_CODES.AUTO_POSITION
|
const label = code === LIVE_EVENT_CODES.AUTO_POSITION
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
|
const MPS_TO_KNOTS = 1.9438444924406
|
||||||
|
|
||||||
export interface GeoCoordinates {
|
export interface GeoCoordinates {
|
||||||
lat: string
|
lat: string
|
||||||
lng: string
|
lng: string
|
||||||
|
/** SOG from GPS when available (kn), otherwise null. */
|
||||||
|
speedKn: number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCurrentPosition(timeoutMs = 15000): Promise<GeoCoordinates> {
|
export function getCurrentPosition(timeoutMs = 15000): Promise<GeoCoordinates> {
|
||||||
@@ -12,9 +16,13 @@ export function getCurrentPosition(timeoutMs = 15000): Promise<GeoCoordinates> {
|
|||||||
|
|
||||||
navigator.geolocation.getCurrentPosition(
|
navigator.geolocation.getCurrentPosition(
|
||||||
(pos) => {
|
(pos) => {
|
||||||
|
const speedKn = pos.coords.speed != null && Number.isFinite(pos.coords.speed)
|
||||||
|
? Number((pos.coords.speed * MPS_TO_KNOTS).toFixed(1))
|
||||||
|
: null
|
||||||
resolve({
|
resolve({
|
||||||
lat: pos.coords.latitude.toFixed(6),
|
lat: pos.coords.latitude.toFixed(6),
|
||||||
lng: pos.coords.longitude.toFixed(6)
|
lng: pos.coords.longitude.toFixed(6),
|
||||||
|
speedKn
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
(err) => reject(err),
|
(err) => reject(err),
|
||||||
|
|||||||
@@ -38,6 +38,14 @@ export function liveWaterRemark(liters: string): string {
|
|||||||
return `__live:water:${liters}`
|
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 {
|
export function parseLiveSailsRemark(remarks: string): string | null {
|
||||||
const prefix = '__live:sails:'
|
const prefix = '__live:sails:'
|
||||||
return remarks.startsWith(prefix) ? remarks.slice(prefix.length) : null
|
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
|
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). */
|
/** Derive motor running state from event history (survives reload). */
|
||||||
export function isMotorRunningFromEvents(
|
export function isMotorRunningFromEvents(
|
||||||
events: Array<{ remarks: string }>,
|
events: Array<{ remarks: string }>,
|
||||||
|
|||||||
Reference in New Issue
Block a user