refactor(live-log): Position-Terminologie und Modal-UX vereinheitlichen
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 <cursoragent@cursor.com>
This commit is contained in:
+8
-8
@@ -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;
|
||||
|
||||
@@ -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')}
|
||||
>
|
||||
<X size={18} />
|
||||
</button>
|
||||
@@ -287,7 +287,7 @@ export default function LiveCameraCapture({
|
||||
|
||||
<div className="live-log-modal-actions live-camera-actions">
|
||||
<button type="button" className="btn secondary" onClick={onClose} disabled={busy}>
|
||||
{t('logs.confirm_no')}
|
||||
{t('logs.live_cancel')}
|
||||
</button>
|
||||
|
||||
{showPreview ? (
|
||||
|
||||
@@ -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<string[]>([])
|
||||
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 && <div className="auth-error mb-4">{error}</div>}
|
||||
|
||||
{!hasPositionFix && (
|
||||
{!hasLoggedPosition && (
|
||||
<p className="live-log-gps-hint" role="status">
|
||||
<MapPin size={16} aria-hidden />
|
||||
{t('logs.live_gps_start_hint')}
|
||||
@@ -1036,9 +1036,9 @@ export default function LiveLogView({
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button type="button" className="live-log-action-btn" onClick={() => void openFixModal()} disabled={busy}>
|
||||
<button type="button" className="live-log-action-btn" onClick={() => void openPositionModal()} disabled={busy}>
|
||||
<MapPin size={18} />
|
||||
{t('logs.live_fix')}
|
||||
{t('logs.live_position')}
|
||||
</button>
|
||||
<button type="button" className="live-log-action-btn" onClick={() => { setCommentText(''); setModal('comment') }} disabled={busy}>
|
||||
<MessageSquare size={18} />
|
||||
@@ -1145,7 +1145,7 @@ export default function LiveLogView({
|
||||
</p>
|
||||
)}
|
||||
<div className="live-log-modal-actions">
|
||||
<button type="button" className="btn secondary" onClick={closeModal}>{t('logs.confirm_no')}</button>
|
||||
<button type="button" className="btn secondary" onClick={closeModal}>{t('logs.live_cancel')}</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn primary"
|
||||
@@ -1161,68 +1161,68 @@ export default function LiveLogView({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{modal === 'fix' && (
|
||||
{modal === 'position' && (
|
||||
<div
|
||||
className="live-log-modal-backdrop"
|
||||
onClick={(e) => { if (e.target === e.currentTarget) closeModal() }}
|
||||
>
|
||||
<div className="live-log-modal" onClick={(e) => e.stopPropagation()}>
|
||||
<h3>{t('logs.live_fix')}</h3>
|
||||
{fixGpsUnavailable && (
|
||||
<h3>{t('logs.live_position')}</h3>
|
||||
{positionGpsUnavailable && (
|
||||
<>
|
||||
<p className="live-log-modal-hint live-log-gps-hint-modal">{t('logs.live_gps_start_hint')}</p>
|
||||
<p className="live-log-modal-hint">{t('logs.live_fix_manual_hint')}</p>
|
||||
<p className="live-log-modal-hint">{t('logs.live_position_manual_hint')}</p>
|
||||
</>
|
||||
)}
|
||||
<fieldset className="live-log-fix-coords" disabled={busy}>
|
||||
<legend className="live-log-fix-label">{t('logs.event_gps')}</legend>
|
||||
<div className="live-log-fix-coords-row">
|
||||
<label className="live-log-fix-field">
|
||||
<span className="live-log-fix-field-label">{t('logs.live_fix_lat_placeholder')}</span>
|
||||
<fieldset className="live-log-position-coords" disabled={busy}>
|
||||
<legend className="live-log-position-label">{t('logs.event_gps')}</legend>
|
||||
<div className="live-log-position-coords-row">
|
||||
<label className="live-log-position-field">
|
||||
<span className="live-log-position-field-label">{t('logs.live_position_lat_placeholder')}</span>
|
||||
<input
|
||||
type="text"
|
||||
inputMode="decimal"
|
||||
className="input-text"
|
||||
placeholder="54.123456"
|
||||
value={fixLat}
|
||||
onChange={(e) => setFixLat(e.target.value)}
|
||||
value={positionLat}
|
||||
onChange={(e) => setPositionLat(e.target.value)}
|
||||
autoFocus
|
||||
/>
|
||||
</label>
|
||||
<label className="live-log-fix-field">
|
||||
<span className="live-log-fix-field-label">{t('logs.live_fix_lng_placeholder')}</span>
|
||||
<label className="live-log-position-field">
|
||||
<span className="live-log-position-field-label">{t('logs.live_position_lng_placeholder')}</span>
|
||||
<input
|
||||
type="text"
|
||||
inputMode="decimal"
|
||||
className="input-text"
|
||||
placeholder="10.654321"
|
||||
value={fixLng}
|
||||
onChange={(e) => setFixLng(e.target.value)}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter') confirmFix() }}
|
||||
value={positionLng}
|
||||
onChange={(e) => setPositionLng(e.target.value)}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter') confirmPosition() }}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className="live-log-fix-gps-row">
|
||||
<div className="live-log-position-gps-row">
|
||||
<button
|
||||
type="button"
|
||||
className="btn secondary live-log-fix-gps-btn"
|
||||
onClick={() => void retryFixGps()}
|
||||
className="btn secondary live-log-position-gps-btn"
|
||||
onClick={() => void retryPositionGps()}
|
||||
title={t('logs.gps_btn')}
|
||||
disabled={fixGpsLoading}
|
||||
disabled={positionGpsLoading}
|
||||
aria-label={t('logs.gps_btn')}
|
||||
>
|
||||
<MapPin size={16} />
|
||||
<span>{fixGpsLoading ? t('logs.live_fix_gps_loading') : t('logs.gps_btn')}</span>
|
||||
<span>{positionGpsLoading ? t('logs.live_position_gps_loading') : t('logs.gps_btn')}</span>
|
||||
</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
<div className="live-log-modal-actions">
|
||||
<button type="button" className="btn secondary" onClick={closeModal}>{t('logs.confirm_no')}</button>
|
||||
<button type="button" className="btn secondary" onClick={closeModal}>{t('logs.live_cancel')}</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn primary"
|
||||
onClick={confirmFix}
|
||||
disabled={busy || !normalizeGpsCoordinates(fixLat, fixLng)}
|
||||
onClick={confirmPosition}
|
||||
disabled={busy || !normalizeGpsCoordinates(positionLat, positionLng)}
|
||||
>
|
||||
{t('logs.live_sails_confirm')}
|
||||
</button>
|
||||
@@ -1237,7 +1237,7 @@ export default function LiveLogView({
|
||||
<h3>{t('logs.live_comment_btn')}</h3>
|
||||
<input type="text" className="input-text" value={commentText} onChange={(e) => setCommentText(e.target.value)} placeholder={t('logs.live_comment_placeholder')} autoFocus onKeyDown={(e) => { if (e.key === 'Enter') confirmComment() }} />
|
||||
<div className="live-log-modal-actions">
|
||||
<button type="button" className="btn secondary" onClick={() => setModal('none')}>{t('logs.confirm_no')}</button>
|
||||
<button type="button" className="btn secondary" onClick={() => setModal('none')}>{t('logs.live_cancel')}</button>
|
||||
<button type="button" className="btn primary" onClick={confirmComment} disabled={!commentText.trim()}>{t('logs.live_comment_confirm')}</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1271,7 +1271,7 @@ export default function LiveLogView({
|
||||
/>
|
||||
</div>
|
||||
<div className="live-log-modal-actions">
|
||||
<button type="button" className="btn secondary" onClick={() => setModal('none')}>{t('logs.confirm_no')}</button>
|
||||
<button type="button" className="btn secondary" onClick={() => setModal('none')}>{t('logs.live_cancel')}</button>
|
||||
<button type="button" className="btn primary" onClick={confirmValueModal}>{t('logs.live_sails_confirm')}</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1293,7 +1293,7 @@ export default function LiveLogView({
|
||||
/>
|
||||
</div>
|
||||
<div className="live-log-modal-actions">
|
||||
<button type="button" className="btn secondary" onClick={() => setModal('none')}>{t('logs.confirm_no')}</button>
|
||||
<button type="button" className="btn secondary" onClick={() => setModal('none')}>{t('logs.live_cancel')}</button>
|
||||
<button type="button" className="btn primary" onClick={confirmValueModal}>{t('logs.live_sails_confirm')}</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1338,7 +1338,7 @@ export default function LiveLogView({
|
||||
onKeyDown={(e) => { if (e.key === 'Enter') confirmValueModal() }}
|
||||
/>
|
||||
<div className="live-log-modal-actions">
|
||||
<button type="button" className="btn secondary" onClick={() => setModal('none')}>{t('logs.confirm_no')}</button>
|
||||
<button type="button" className="btn secondary" onClick={() => setModal('none')}>{t('logs.live_cancel')}</button>
|
||||
<button type="button" className="btn primary" onClick={confirmValueModal}>{t('logs.live_sails_confirm')}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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')}
|
||||
>
|
||||
<X size={18} />
|
||||
</button>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -21,8 +21,8 @@ const t = (key: string, opts?: Record<string, unknown>) => {
|
||||
'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', () => {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
describe('live log position', () => {
|
||||
it('returns latest position with coordinates', () => {
|
||||
const entryDate = '2026-06-01'
|
||||
|
||||
describe('live log position fix', () => {
|
||||
it('returns latest fix with coordinates', () => {
|
||||
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()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user