import { useCallback, useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { Anchor, ChevronDown, ChevronLeft, ChevronUp, CloudSun, Compass, Droplets, FileText, Fuel, Gauge, MapPin, MessageSquare, Radio, Sailboat, Undo2, Zap } from 'lucide-react' import { db } from '../services/db.js' import { getActiveMasterKey } from '../services/auth.js' import { getLogbookKey } from '../services/logbookKeys.js' import { decryptJson } from '../services/crypto.js' import { PlausibleEvents, trackPlausibleEvent } from '../services/analytics.js' import { appendQuickEvent, appendTankRefill, findOrCreateTodayEntry, loadEntry, removeLastEvent } from '../services/quickEventLog.js' import { formatEventSummary } from '../utils/formatEventSummary.js' import { getLastAutoPositionMs, isMotorRunningFromEvents, LIVE_EVENT_CODES, liveCommentRemark, liveFuelRemark, livePrecipRemark, liveSailsRemark, liveSogRemark, liveStwRemark, liveTempRemark, liveWaterRemark } from '../utils/liveEventCodes.js' import { getCurrentPosition } from '../utils/geolocation.js' import { sortLogEventsByTime, type LogEventPayload } from '../utils/logEntryPayload.js' import { useDialog } from './ModalDialog.tsx' import CourseDialInput from './CourseDialInput.tsx' interface LiveLogViewProps { logbookId: string onOpenEditor: (entryId: string) => void onSwitchToList: () => void } type LiveModal = | 'none' | 'sails' | 'comment' | 'wind' | 'pressure' | 'temp' | 'precip' | 'sea_state' | 'course' | 'fuel' | 'water' | 'sog' | 'stw' const AUTO_POSITION_INTERVAL_MS = 3 * 60 * 60 * 1000 const AUTO_POSITION_CHECK_MS = 60_000 const UNDO_TIMEOUT_MS = 5000 function hapticPulse() { navigator.vibrate?.(40) } function lastCourseFromEvents(events: LogEventPayload[]): string { for (let i = events.length - 1; i >= 0; i--) { if (events[i].mgk.trim()) return events[i].mgk } return '' } function lastWindDirectionFromEvents(events: LogEventPayload[]): string { for (let i = events.length - 1; i >= 0; i--) { if (events[i].windDirection.trim()) return events[i].windDirection } return '' } export default function LiveLogView({ logbookId, onOpenEditor, onSwitchToList }: LiveLogViewProps) { const { t, i18n } = useTranslation() const { showAlert } = useDialog() const [entryId, setEntryId] = useState(null) const [dayOfTravel, setDayOfTravel] = useState('') const [date, setDate] = useState('') const [events, setEvents] = useState([]) const [yachtSails, setYachtSails] = useState([]) const [loading, setLoading] = useState(true) const [busy, setBusy] = useState(false) const [error, setError] = useState(null) const [modal, setModal] = useState('none') const [weatherExpanded, setWeatherExpanded] = useState(false) const [commentText, setCommentText] = useState('') const [valueInput, setValueInput] = useState('') const [valueInputSecondary, setValueInputSecondary] = useState('') const [selectedSails, setSelectedSails] = useState([]) const [undoVisible, setUndoVisible] = useState(false) const streamEndRef = useRef(null) const undoTimerRef = useRef(null) const autoPositionBusyRef = useRef(false) const defaultSails = i18n.language === 'de' ? ['Großsegel', 'Genua', 'Fock', 'Spinnaker', 'Gennaker'] : ['Mainsail', 'Genoa', 'Jib', 'Spinnaker', 'Gennaker'] const sailOptions = yachtSails.length > 0 ? yachtSails : defaultSails const motorRunning = isMotorRunningFromEvents(events) const motorLabel = t('logs.motor_propulsion') const refreshEntry = useCallback(async (id: string) => { const loaded = await loadEntry(logbookId, id) if (!loaded) return const entryEvents = (loaded.data.events as LogEventPayload[]) || [] setDayOfTravel(String(loaded.data.dayOfTravel || '')) setDate(String(loaded.data.date || '')) setEvents(sortLogEventsByTime(entryEvents.map((e) => ({ ...e })))) }, [logbookId]) const showUndo = useCallback(() => { setUndoVisible(true) if (undoTimerRef.current) window.clearTimeout(undoTimerRef.current) undoTimerRef.current = window.setTimeout(() => { setUndoVisible(false) undoTimerRef.current = null }, UNDO_TIMEOUT_MS) }, []) useEffect(() => { let cancelled = false async function init() { setLoading(true) setError(null) try { const id = await findOrCreateTodayEntry(logbookId) if (cancelled) return setEntryId(id) const masterKey = await getLogbookKey(logbookId) || getActiveMasterKey() if (masterKey) { const yacht = await db.yachts.get(logbookId) if (yacht) { const decrypted = await decryptJson(yacht.encryptedData, yacht.iv, yacht.tag, masterKey) if (decrypted?.sails && Array.isArray(decrypted.sails)) { setYachtSails(decrypted.sails as string[]) } } } await refreshEntry(id) } catch (err: unknown) { if (!cancelled) { console.error('Failed to init live log:', err) setError(err instanceof Error ? err.message : t('logs.live_load_error')) } } finally { if (!cancelled) setLoading(false) } } void init() return () => { cancelled = true } }, [logbookId, refreshEntry, t]) useEffect(() => { streamEndRef.current?.scrollIntoView({ behavior: 'smooth' }) }, [events.length]) useEffect(() => { return () => { if (undoTimerRef.current) window.clearTimeout(undoTimerRef.current) } }, []) useEffect(() => { if (!entryId || loading) return const maybeAutoPosition = async () => { if (document.visibilityState !== 'visible' || autoPositionBusyRef.current || busy) return const lastMs = getLastAutoPositionMs(events, date) if (lastMs != null && Date.now() - lastMs < AUTO_POSITION_INTERVAL_MS) return autoPositionBusyRef.current = true try { const coords = await getCurrentPosition() await appendQuickEvent(logbookId, entryId, { gpsLat: coords.lat, gpsLng: coords.lng, remarks: LIVE_EVENT_CODES.AUTO_POSITION }) await refreshEntry(entryId) } catch { // Silent — auto-position is best-effort } finally { autoPositionBusyRef.current = false } } const interval = window.setInterval(() => void maybeAutoPosition(), AUTO_POSITION_CHECK_MS) return () => window.clearInterval(interval) }, [entryId, loading, events, date, logbookId, refreshEntry, busy]) const runQuickAction = async ( action: () => Promise, trackEvent?: string, withUndo = true ) => { if (!entryId || busy) return setBusy(true) setError(null) try { await action() await refreshEntry(entryId) if (withUndo) showUndo() if (trackEvent) trackPlausibleEvent(PlausibleEvents.TRAVEL_DAY_SAVED, { context: trackEvent }) } catch (err: unknown) { console.error('Live log action failed:', err) setError(err instanceof Error ? err.message : t('logs.live_action_error')) } finally { setBusy(false) } } const openValueModal = (type: LiveModal, primary = '', secondary = '') => { setValueInput(primary) setValueInputSecondary(secondary) 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 () => { if (!entryId) return const starting = !motorRunning await appendQuickEvent(logbookId, entryId, { sailsOrMotor: starting ? motorLabel : '', remarks: starting ? LIVE_EVENT_CODES.MOTOR_START : LIVE_EVENT_CODES.MOTOR_STOP }) }, 'live_motor') } const handleCastOff = () => { void runQuickAction(async () => { if (!entryId) return await appendQuickEvent(logbookId, entryId, { remarks: LIVE_EVENT_CODES.CAST_OFF }) }, 'live_cast_off') } const handleMoor = () => { void runQuickAction(async () => { if (!entryId) return await appendQuickEvent(logbookId, entryId, { remarks: LIVE_EVENT_CODES.MOOR }) }, 'live_moor') } const handleFix = () => { void runQuickAction(async () => { if (!entryId) return try { const coords = await getCurrentPosition() await appendQuickEvent(logbookId, entryId, { gpsLat: coords.lat, gpsLng: coords.lng, remarks: LIVE_EVENT_CODES.FIX }) } catch { await showAlert(t('logs.live_gps_error'), t('logs.live_fix')) } }, 'live_fix') } const handleUndo = () => { if (!entryId || busy) return setUndoVisible(false) if (undoTimerRef.current) { window.clearTimeout(undoTimerRef.current) undoTimerRef.current = null } void runQuickAction(async () => { await removeLastEvent(logbookId, entryId) }, 'live_undo', false) } const confirmSails = () => { if (selectedSails.length === 0) { setModal('none') return } const sailsLabel = selectedSails.join(' + ') setModal('none') setSelectedSails([]) void runQuickAction(async () => { if (!entryId) return await appendQuickEvent(logbookId, entryId, { sailsOrMotor: sailsLabel, remarks: liveSailsRemark(sailsLabel) }) }, 'live_sails') } const confirmComment = () => { const text = commentText.trim() if (!text) { setModal('none') return } setModal('none') setCommentText('') void runQuickAction(async () => { if (!entryId) return await appendQuickEvent(logbookId, entryId, { remarks: liveCommentRemark(text) }) }, 'live_comment') } const confirmValueModal = () => { if (!entryId) return const primary = valueInput.trim() const secondary = valueInputSecondary.trim() switch (modal) { case 'wind': if (!primary && !secondary) return setModal('none') void runQuickAction(async () => { await appendQuickEvent(logbookId, entryId, { windDirection: primary, windStrength: secondary, remarks: LIVE_EVENT_CODES.WIND }) }, 'live_wind') break case 'pressure': if (!primary) return setModal('none') void runQuickAction(async () => { await appendQuickEvent(logbookId, entryId, { windPressure: primary, remarks: LIVE_EVENT_CODES.PRESSURE }) }, 'live_pressure') break case 'temp': if (!primary) return setModal('none') void runQuickAction(async () => { await appendQuickEvent(logbookId, entryId, { remarks: liveTempRemark(primary) }) }, 'live_temp') break case 'precip': if (!primary) return setModal('none') void runQuickAction(async () => { await appendQuickEvent(logbookId, entryId, { remarks: livePrecipRemark(primary) }) }, 'live_precip') break case 'sea_state': if (!primary) return setModal('none') void runQuickAction(async () => { await appendQuickEvent(logbookId, entryId, { seaState: primary, remarks: LIVE_EVENT_CODES.SEA_STATE }) }, 'live_sea_state') break case 'course': { const course = primary || lastCourseFromEvents(events) if (!course) return setModal('none') void runQuickAction(async () => { await appendQuickEvent(logbookId, entryId, { mgk: course, remarks: LIVE_EVENT_CODES.COURSE }) }, 'live_course') break } case 'fuel': { const liters = parseFloat(primary) if (!Number.isFinite(liters) || liters <= 0) return setModal('none') void runQuickAction(async () => { await appendTankRefill(logbookId, entryId, 'fuel', liters, { remarks: liveFuelRemark(String(liters)) }) }, 'live_fuel') break } case 'water': { const liters = parseFloat(primary) if (!Number.isFinite(liters) || liters <= 0) return setModal('none') void runQuickAction(async () => { await appendTankRefill(logbookId, entryId, 'freshwater', liters, { remarks: liveWaterRemark(String(liters)) }) }, '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 } } const toggleSailSelection = (sail: string) => { setSelectedSails((prev) => prev.some((s) => s.toLowerCase() === sail.toLowerCase()) ? prev.filter((s) => s.toLowerCase() !== sail.toLowerCase()) : [...prev, sail] ) } if (loading) { return (

{t('logs.live_loading')}

) } return (

{t('logs.live_title')}

{date && (

{t('logs.day_of_travel')} {dayOfTravel} · {new Date(date).toLocaleDateString()}

)}
{entryId && ( )}
{error &&
{error}
}

{t('logs.live_stream_title')}

{events.length === 0 ? (

{t('logs.live_no_events')}

) : (
    {events.map((event, index) => (
  1. {formatEventSummary(event, t)}
  2. ))}
)}
{undoVisible && events.length > 0 && (
{t('logs.live_undo_hint')}
)} {modal === 'sails' && (
setModal('none')}>
e.stopPropagation()}>

{t('logs.live_sails_pick')}

{sailOptions.map((sail) => ( ))}
)} {modal === 'comment' && (
setModal('none')}>
e.stopPropagation()}>

{t('logs.live_comment_btn')}

setCommentText(e.target.value)} placeholder={t('logs.live_comment_placeholder')} autoFocus onKeyDown={(e) => { if (e.key === 'Enter') confirmComment() }} />
)} {modal === 'wind' && (
setModal('none')}>
e.stopPropagation()}>

{t('logs.live_wind_btn')}

setValueInputSecondary(e.target.value)} placeholder="e.g. 4 Bft" />
)} {modal === 'course' && (
setModal('none')}>
e.stopPropagation()}>

{t('logs.live_course_btn')}

)} {['pressure', 'temp', 'precip', 'sea_state', 'fuel', 'water', 'sog', 'stw'].includes(modal) && (
setModal('none')}>
e.stopPropagation()}>

{modal === 'pressure' && t('logs.live_pressure_btn')} {modal === 'temp' && t('logs.live_temp_btn')} {modal === 'precip' && t('logs.live_precip_btn')} {modal === 'sea_state' && t('logs.live_sea_state_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)} placeholder={ modal === 'pressure' ? t('logs.live_pressure_placeholder') : modal === 'temp' ? t('logs.live_temp_placeholder') : modal === 'precip' ? t('logs.live_precip_placeholder') : modal === 'sea_state' ? t('logs.live_sea_state_placeholder') : modal === 'fuel' ? t('logs.live_fuel_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() }} />
)}
) }