import { useState, useEffect } from 'react' import { useTranslation } from 'react-i18next' import { cycleAppLanguage, getNextLanguage, isGermanLocale } from '../utils/i18nLanguages.js' import { decryptJson } from '../services/crypto.js' import { PlausibleEvents, trackPlausibleEvent } from '../services/analytics.js' import LogbookVesselPicker from './LogbookVesselPicker.tsx' import LogbookCrewPicker from './LogbookCrewPicker.tsx' import type { LogbookVesselSelectionData } from '../types/vessel.js' import { emptyLogbookVesselSelection } from '../types/vessel.js' import type { LogbookCrewSelectionData } from '../types/person.js' import { emptyLogbookCrewSelection } from '../types/person.js' import { legacyCrewRecordsToLogbookSelection } from '../utils/personSnapshots.js' import type { PersonData } from '../types/person.js' import LogEntriesList from './LogEntriesList.tsx' import { Ship, Users, FileText, Lock, AlertCircle, Globe } from 'lucide-react' interface ReadOnlyViewerProps { token: string hexKey: string } // Convert Hex String back to ArrayBuffer const hexToBuffer = (hex: string): ArrayBuffer => { const bytes = new Uint8Array(hex.length / 2) for (let i = 0; i < bytes.length; i++) { bytes[i] = parseInt(hex.substring(i * 2, i * 2 + 2), 16) } return bytes.buffer } export default function ReadOnlyViewer({ token, hexKey }: ReadOnlyViewerProps) { const { t, i18n } = useTranslation() const [activeTab, setActiveTab] = useState<'vessel' | 'crew' | 'logs'>('logs') const [loading, setLoading] = useState(true) const [error, setError] = useState(null) // Logbook data states const [logbookTitle, setLogbookTitle] = useState('Logbook') const [yacht, setYacht] = useState(null) const [logbookCrewSelection, setLogbookCrewSelection] = useState( emptyLogbookCrewSelection() ) const [logbookVesselSelection, setLogbookVesselSelection] = useState( emptyLogbookVesselSelection() ) const [legacyCrews, setLegacyCrews] = useState([]) const [entries, setEntries] = useState([]) const [photos, setPhotos] = useState([]) const [gpsTracks, setGpsTracks] = useState([]) useEffect(() => { loadData() }, [token, hexKey]) const loadData = async () => { setLoading(true) setError(null) try { const keyBuffer = hexToBuffer(hexKey) const res = await fetch(`/api/collaboration/share-pull?token=${token}`) if (!res.ok) { if (res.status === 410) { throw new Error(isGermanLocale(i18n.language) ? 'Dieser Freigabelink ist abgelaufen.' : 'This share link has expired.') } throw new Error(isGermanLocale(i18n.language) ? 'Fehler beim Laden des freigegebenen Logbuchs.' : 'Failed to fetch shared logbook data.') } const data = await res.json() // Decrypt Title let decryptedTitle = 'Shared Logbook' if (data.title) { const parsed = JSON.parse(data.title) decryptedTitle = await decryptJson(parsed.ciphertext, parsed.iv, parsed.tag, keyBuffer) } setLogbookTitle(decryptedTitle) // Decrypt Yacht let decYacht = null if (data.yacht) { decYacht = await decryptJson(data.yacht.encryptedData, data.yacht.iv, data.yacht.tag, keyBuffer) } setYacht(decYacht) if (data.logbookCrewSelection) { const decSel = await decryptJson( data.logbookCrewSelection.encryptedData, data.logbookCrewSelection.iv, data.logbookCrewSelection.tag, keyBuffer ) if (decSel) { setLogbookCrewSelection({ activeSkipperId: decSel.activeSkipperId ?? null, activeCrewIds: Array.isArray(decSel.activeCrewIds) ? decSel.activeCrewIds : [], snapshotsById: decSel.snapshotsById && typeof decSel.snapshotsById === 'object' ? decSel.snapshotsById : {} }) } } if (data.logbookVesselSelection) { const decVessel = await decryptJson( data.logbookVesselSelection.encryptedData, data.logbookVesselSelection.iv, data.logbookVesselSelection.tag, keyBuffer ) if (decVessel) { setLogbookVesselSelection({ activeVesselId: decVessel.activeVesselId ?? null, vesselSnapshot: decVessel.vesselSnapshot ?? null }) } } else if (decYacht) { const legacy = decYacht as Record setLogbookVesselSelection({ activeVesselId: 'legacy-yacht', vesselSnapshot: { id: 'legacy-yacht', name: typeof legacy.name === 'string' ? legacy.name : '', ...legacy } as import('../types/vessel.js').VesselSnapshot }) } const decCrews: Array<{ payloadId: string; data: PersonData }> = [] if (data.crews) { for (const c of data.crews) { const dec = await decryptJson(c.encryptedData, c.iv, c.tag, keyBuffer) if (dec) { decCrews.push({ payloadId: c.payloadId, data: dec as PersonData }) } } } setLegacyCrews(decCrews) if (!data.logbookCrewSelection && decCrews.length > 0) { setLogbookCrewSelection(legacyCrewRecordsToLogbookSelection(decCrews)) } // Decrypt Entries const decEntries = [] if (data.entries) { for (const e of data.entries) { const dec = await decryptJson(e.encryptedData, e.iv, e.tag, keyBuffer) decEntries.push({ payloadId: e.payloadId, ...dec }) } } setEntries(decEntries) // Decrypt Photos const decPhotos = [] if (data.photos) { for (const p of data.photos) { const dec = await decryptJson(p.encryptedData, p.iv, p.tag, keyBuffer) decPhotos.push({ payloadId: p.payloadId, entryId: p.entryId, image: dec.image, caption: dec.caption || '', updatedAt: p.updatedAt }) } } setPhotos(decPhotos) // Decrypt GPS Tracks const decGpsTracks = [] if (data.gpsTracks) { for (const tr of data.gpsTracks) { const dec = await decryptJson(tr.encryptedData, tr.iv, tr.tag, keyBuffer) decGpsTracks.push({ entryId: tr.entryId, waypoints: dec.waypoints || dec || [], filename: dec.filename || 'track.gpx' }) } } setGpsTracks(decGpsTracks) trackPlausibleEvent(PlausibleEvents.PUBLIC_LINK_OPENED) } catch (err: any) { console.error(err) setError(err.message || 'Failed to decrypt logbook details.') } finally { setLoading(false) } } const toggleLanguage = () => { cycleAppLanguage(i18n) } if (loading) { return (

{isGermanLocale(i18n.language) ? 'Lade freigegebenes Logbuch...' : 'Loading shared logbook...'}

) } if (error) { return (

{isGermanLocale(i18n.language) ? 'Verbindungsfehler' : 'Access Error'}

{error}

) } return (
{/* Top Banner indicating read-only shared view */}

{logbookTitle}

{isGermanLocale(i18n.language) ? 'Schreibgeschützte Ansicht (Ende-zu-Ende verschlüsselt)' : 'Read-Only View (End-to-End Encrypted)'}

{activeTab === 'logs' && ( )} {activeTab === 'vessel' && ( )} {activeTab === 'crew' && ( 0 ? legacyCrews : undefined} preloadedSelection={logbookCrewSelection} /> )}
) }