From 3c7aec1573bc0e05ea2620be11f6b2e4bd844dfe Mon Sep 17 00:00:00 2001 From: elpatron Date: Fri, 29 May 2026 15:25:39 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20Daagbok-Branding,=20Track-Upload=20statt?= =?UTF-8?q?=20GPS-Tracker=20und=20zentrale=20Account-L=C3=B6schung.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ersetzt gpsTracker/Leaflet durch trackUpload, korrigiert App-Bezeichner und Login-Abstände, und macht die Account-Gefahrenzone auf dem Dashboard erreichbar. Co-authored-by: Cursor --- client/index.html | 2 +- client/package-lock.json | 25 --- client/package.json | 2 - client/src/App.css | 62 ++++-- client/src/components/AccountDangerZone.tsx | 63 ++++++ client/src/components/AuthOnboarding.tsx | 2 +- client/src/components/LogEntriesList.tsx | 2 +- client/src/components/LogEntryEditor.tsx | 210 +++++------------- client/src/components/LogbookDashboard.tsx | 5 + client/src/components/SettingsForm.tsx | 56 +---- client/src/i18n/locales/de.json | 14 +- client/src/i18n/locales/en.json | 16 +- client/src/services/csvExport.ts | 2 +- .../{gpsTracker.ts => trackUpload.ts} | 149 +++++-------- client/vite.config.ts | 4 +- scripts/start-dev-docker.sh | 2 +- scripts/start-dev.sh | 2 +- scripts/update-prod.sh | 2 +- server/package.json | 2 +- server/src/index.ts | 4 +- server/src/routes/auth.ts | 2 +- 21 files changed, 261 insertions(+), 367 deletions(-) create mode 100644 client/src/components/AccountDangerZone.tsx rename client/src/services/{gpsTracker.ts => trackUpload.ts} (60%) diff --git a/client/index.html b/client/index.html index ceb1063..0cb597d 100644 --- a/client/index.html +++ b/client/index.html @@ -7,7 +7,7 @@ - + Kapteins Daagbok diff --git a/client/package-lock.json b/client/package-lock.json index cc2f065..5230d5c 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -15,7 +15,6 @@ "i18next": "^26.3.0", "i18next-browser-languagedetector": "^8.2.1", "jspdf": "^4.2.1", - "leaflet": "^1.9.4", "lucide-react": "^1.16.0", "react": "^19.2.6", "react-dom": "^19.2.6", @@ -23,7 +22,6 @@ }, "devDependencies": { "@eslint/js": "^10.0.1", - "@types/leaflet": "^1.9.21", "@types/node": "^24.12.3", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", @@ -2717,13 +2715,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/geojson": { - "version": "7946.0.16", - "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", - "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -2731,16 +2722,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/leaflet": { - "version": "1.9.21", - "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.21.tgz", - "integrity": "sha512-TbAd9DaPGSnzp6QvtYngntMZgcRk+igFELwR2N99XZn7RXUdKgsXMR+28bUO0rPsWp8MIu/f47luLIQuSLYv/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/geojson": "*" - } - }, "node_modules/@types/node": { "version": "24.12.4", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.4.tgz", @@ -5318,12 +5299,6 @@ "json-buffer": "3.0.1" } }, - "node_modules/leaflet": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", - "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==", - "license": "BSD-2-Clause" - }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", diff --git a/client/package.json b/client/package.json index c1c80d7..fa572cb 100644 --- a/client/package.json +++ b/client/package.json @@ -17,7 +17,6 @@ "i18next": "^26.3.0", "i18next-browser-languagedetector": "^8.2.1", "jspdf": "^4.2.1", - "leaflet": "^1.9.4", "lucide-react": "^1.16.0", "react": "^19.2.6", "react-dom": "^19.2.6", @@ -25,7 +24,6 @@ }, "devDependencies": { "@eslint/js": "^10.0.1", - "@types/leaflet": "^1.9.21", "@types/node": "^24.12.3", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", diff --git a/client/src/App.css b/client/src/App.css index 6f30f07..8219cae 100644 --- a/client/src/App.css +++ b/client/src/App.css @@ -1,4 +1,4 @@ -/* Kapteins Daagbox App styling */ +/* Kapteins Daagbok App styling */ body { background: radial-gradient(circle at center, #1b264f 0%, #0b0c10 100%); @@ -55,7 +55,8 @@ body { -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; - margin: 0 0 8px 0; + margin: 0 0 14px 0; + line-height: 1.25; letter-spacing: -0.5px; } @@ -63,7 +64,7 @@ body { font-size: 14.5px; color: #94a3b8; margin: 0; - line-height: 140%; + line-height: 1.45; } .auth-form { @@ -449,6 +450,51 @@ body { } } +.dashboard-account-section { + width: 100%; + max-width: 720px; + margin: 40px auto 48px; + padding-bottom: calc(32px + env(safe-area-inset-bottom, 0px)); +} + +.account-danger-zone { + border-top: 1px solid rgba(239, 68, 68, 0.2); + padding-top: 24px; +} + +.account-danger-zone__header { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 12px; +} + +.account-danger-zone__icon { + color: #ef4444; + flex-shrink: 0; +} + +.account-danger-zone__title { + margin: 0; + color: #ef4444; + font-size: 16px; +} + +.account-danger-zone__desc { + font-size: 13.5px; + color: #94a3b8; + line-height: 1.45; + margin: 0 0 16px 0; +} + +.account-danger-zone__actions { + justify-content: flex-start; +} + +.account-danger-zone__actions .btn { + width: auto; +} + .create-section { background: rgba(11, 12, 16, 0.6); backdrop-filter: blur(20px); @@ -1894,16 +1940,6 @@ body:has(.theme-cupertino) { margin-top: 4px; } -#openseamap-container { - width: 100%; - height: 400px; - border-radius: 12px; - border: 1px solid rgba(212, 175, 55, 0.2); - position: relative; - z-index: 1; - box-sizing: border-box; -} - .track-info-header { display: flex; justify-content: space-between; diff --git a/client/src/components/AccountDangerZone.tsx b/client/src/components/AccountDangerZone.tsx new file mode 100644 index 0000000..eeea281 --- /dev/null +++ b/client/src/components/AccountDangerZone.tsx @@ -0,0 +1,63 @@ +import { useState } from 'react' +import { useTranslation } from 'react-i18next' +import { Trash2, AlertTriangle } from 'lucide-react' +import { useDialog } from './ModalDialog.tsx' +import { deleteAccount } from '../services/auth.js' + +interface AccountDangerZoneProps { + className?: string +} + +export default function AccountDangerZone({ className = '' }: AccountDangerZoneProps) { + const { t } = useTranslation() + const { showConfirm, showAlert } = useDialog() + const [deleting, setDeleting] = useState(false) + + const handleDeleteAccount = async () => { + const confirmed = await showConfirm( + t('settings.delete_account_confirm_desc'), + t('settings.delete_account_confirm_title'), + t('settings.delete_account_confirm_yes'), + t('settings.delete_account_confirm_no') + ) + + if (!confirmed) return + + setDeleting(true) + try { + const success = await deleteAccount() + if (success) { + window.location.reload() + } else { + showAlert(t('settings.delete_account_failed')) + } + } catch (err: any) { + showAlert(err.message || t('settings.delete_account_failed')) + } finally { + setDeleting(false) + } + } + + return ( +
+
+ +

{t('settings.danger_zone_title')}

+
+ +

{t('settings.danger_zone_desc')}

+ +
+ +
+
+ ) +} diff --git a/client/src/components/AuthOnboarding.tsx b/client/src/components/AuthOnboarding.tsx index 73f1c00..fd35341 100644 --- a/client/src/components/AuthOnboarding.tsx +++ b/client/src/components/AuthOnboarding.tsx @@ -382,7 +382,7 @@ export default function AuthOnboarding({ onAuthenticated }: AuthOnboardingProps) return (
- Kapteins Daagbox + Kapteins Daagbok

{t('app.name')}

{t('auth.tagline')}

diff --git a/client/src/components/LogEntriesList.tsx b/client/src/components/LogEntriesList.tsx index 248d4d9..72e9b58 100644 --- a/client/src/components/LogEntriesList.tsx +++ b/client/src/components/LogEntriesList.tsx @@ -275,7 +275,7 @@ export default function LogEntriesList({ readOnly={readOnly} preloadedEntry={preloadedEntries?.find(entry => (entry.payloadId || entry.id) === selectedEntryId)} preloadedPhotos={preloadedPhotos} - preloadedGpsTrack={preloadedGpsTracks?.find(track => track.entryId === selectedEntryId)} + preloadedTrack={preloadedGpsTracks?.find(track => track.entryId === selectedEntryId)} /> ) } diff --git a/client/src/components/LogEntryEditor.tsx b/client/src/components/LogEntryEditor.tsx index e7dc886..5e6447a 100644 --- a/client/src/components/LogEntryEditor.tsx +++ b/client/src/components/LogEntryEditor.tsx @@ -6,20 +6,17 @@ import { getLogbookKey } from '../services/logbookKeys.js' import { encryptJson, decryptJson } from '../services/crypto.js' import { syncLogbook } from '../services/sync.js' import { downloadLogbookPagePdf } from '../services/pdfExport.js' -import { FileText, Save, ChevronLeft, Check, Compass, Plus, Trash2, MapPin, CloudSun, Clock, Download, Navigation } from 'lucide-react' +import { FileText, Save, ChevronLeft, Check, Compass, Plus, Trash2, MapPin, CloudSun, Clock, Download, Upload } from 'lucide-react' import PhotoCapture from './PhotoCapture.tsx' import { useDialog } from './ModalDialog.tsx' -import L from 'leaflet' -import 'leaflet/dist/leaflet.css' import { - getDecryptedGpsTrack, - saveUploadedGpsTrack, - deleteGpsTrack, + getDecryptedTrack, + saveUploadedTrack, + deleteTrack, downloadTrackFile, parseTrackFile, - type GpsWaypoint, - type SavedGpsTrack -} from '../services/gpsTracker.js' + type SavedTrack +} from '../services/trackUpload.js' interface LogEntryEditorProps { entryId: string @@ -28,7 +25,7 @@ interface LogEntryEditorProps { readOnly?: boolean preloadedEntry?: any preloadedPhotos?: any[] - preloadedGpsTrack?: any + preloadedTrack?: any preloadedYacht?: any } @@ -58,7 +55,7 @@ export default function LogEntryEditor({ readOnly = false, preloadedEntry, preloadedPhotos, - preloadedGpsTrack, + preloadedTrack, preloadedYacht }: LogEntryEditorProps) { const { t, i18n } = useTranslation() @@ -116,15 +113,12 @@ export default function LogEntryEditor({ const [error, setError] = useState(null) const [weatherLoading, setWeatherLoading] = useState(false) - // GPS Tracking States - const [savedTrack, setSavedTrack] = useState(null) + // Track file upload + const [savedTrack, setSavedTrack] = useState(null) const [dragOver, setDragOver] = useState(false) const [uploadError, setUploadError] = useState(null) const fileInputRef = useRef(null) - const mapContainerRef = useRef(null) - const mapInstanceRef = useRef(null) - // Auto-calculate Freshwater Consumption useEffect(() => { const morning = parseFloat(fwMorning) || 0 @@ -236,91 +230,24 @@ export default function LogEntryEditor({ loadEntry() }, [entryId, preloadedEntry]) - // GPS Track Loader - const loadGpsTrack = async () => { - if (readOnly && preloadedGpsTrack) { - setSavedTrack(preloadedGpsTrack) + const loadTrack = async () => { + if (readOnly && preloadedTrack) { + setSavedTrack(preloadedTrack) return } try { - const track = await getDecryptedGpsTrack(entryId) + const track = await getDecryptedTrack(entryId) setSavedTrack(track) } catch (e) { - console.warn('Failed to load GPS track:', e) + console.warn('Failed to load track file:', e) } } useEffect(() => { - loadGpsTrack() - }, [entryId, preloadedGpsTrack]) + loadTrack() + }, [entryId, preloadedTrack]) - // Leaflet Map Initialization and Rendering - useEffect(() => { - if (!savedTrack || !savedTrack.waypoints || savedTrack.waypoints.length === 0 || !mapContainerRef.current) { - if (mapInstanceRef.current) { - mapInstanceRef.current.remove() - mapInstanceRef.current = null - } - return - } - - const startWp = savedTrack.waypoints[0] - const map = L.map(mapContainerRef.current).setView([startWp.lat, startWp.lng], 13) - mapInstanceRef.current = map - - L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { - maxZoom: 19, - attribution: '© OpenStreetMap contributors' - }).addTo(map) - - L.tileLayer('https://tiles.openseamap.org/seamark/{z}/{x}/{y}.png', { - maxZoom: 18, - attribution: 'Map data © OpenSeaMap contributors' - }).addTo(map) - - const latLngs = savedTrack.waypoints.map((wp) => [wp.lat, wp.lng] as [number, number]) - - const polyline = L.polyline(latLngs, { - color: '#fbbf24', - weight: 4, - opacity: 0.85 - }).addTo(map) - - map.fitBounds(polyline.getBounds(), { padding: [20, 20] }) - - if (savedTrack.waypoints.length > 0) { - L.circleMarker(latLngs[0], { - radius: 8, - fillColor: '#10b981', - fillOpacity: 0.9, - color: '#ffffff', - weight: 2 - }).addTo(map).bindPopup('Start Position') - - if (savedTrack.waypoints.length > 1) { - L.circleMarker(latLngs[latLngs.length - 1], { - radius: 8, - fillColor: '#ef4444', - fillOpacity: 0.9, - color: '#ffffff', - weight: 2 - }).addTo(map).bindPopup('End Position') - } - } - - setTimeout(() => { - map.invalidateSize() - }, 100) - - return () => { - if (mapInstanceRef.current) { - mapInstanceRef.current.remove() - mapInstanceRef.current = null - } - } - }, [savedTrack]) - - // GPX/KML/GeoJSON Upload Handlers + // Track file upload handlers const handleFileUpload = async (file: File) => { if (readOnly) return setUploadError(null) @@ -338,8 +265,8 @@ export default function LogEntryEditor({ throw new Error('No coordinates found in file. Supported formats: GPX, KML, GeoJSON.') } - await saveUploadedGpsTrack(logbookId, entryId, text, parsedWps, file.name, fileType) - await loadGpsTrack() + await saveUploadedTrack(logbookId, entryId, text, parsedWps, file.name, fileType) + await loadTrack() } catch (err: any) { console.error('File parsing failed:', err) setUploadError(err.message || 'Failed to parse track file.') @@ -380,7 +307,7 @@ export default function LogEntryEditor({ return } try { - await deleteGpsTrack(logbookId, entryId) + await deleteTrack(logbookId, entryId) setSavedTrack(null) setUploadError(null) } catch (err: any) { @@ -388,30 +315,6 @@ export default function LogEntryEditor({ } } - const calculateTrackDistance = (wps: GpsWaypoint[]) => { - if (wps.length < 2) return 0 - let totalMeters = 0 - for (let i = 1; i < wps.length; i++) { - const lat1 = wps[i - 1].lat - const lon1 = wps[i - 1].lng - const lat2 = wps[i].lat - const lon2 = wps[i].lng - - const R = 6371e3 - const phi1 = (lat1 * Math.PI) / 180 - const phi2 = (lat2 * Math.PI) / 180 - const deltaPhi = ((lat2 - lat1) * Math.PI) / 180 - const deltaLambda = ((lon2 - lon1) * Math.PI) / 180 - - const a = - Math.sin(deltaPhi / 2) * Math.sin(deltaPhi / 2) + - Math.cos(phi1) * Math.cos(phi2) * Math.sin(deltaLambda / 2) * Math.sin(deltaLambda / 2) - const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)) - totalMeters += R * c - } - return Number((totalMeters / 1852).toFixed(2)) - } - const handleGetGps = () => { if (readOnly) return const lookupFallback = async () => { @@ -1234,17 +1137,16 @@ export default function LogEntryEditor({ )}
- {/* GPS Track Upload & Map Visualization */} + {/* Track file upload */}
- -

{t('logs.gps_tracking_title')}

+ +

{t('logs.track_upload_title')}

{uploadError &&
{uploadError}
} {!savedTrack ? ( - /* Upload Zone when no track is loaded */
- +
{t('logs.gps_track_upload_btn')}
{t('logs.gps_track_upload_help')}
) : ( - /* Map and Details when track is loaded */ -
-
-
- - {savedTrack.filename || 'track'} -
-
- - {t('logs.gps_tracking_stat_distance')}: {calculateTrackDistance(savedTrack.waypoints)} sm - - - {t('logs.gps_tracking_stat_waypoints')}: {savedTrack.waypoints.length} - -
-
+
+
+ + {savedTrack.filename || 'track'} + + {savedTrack.fileType.toUpperCase()} + {savedTrack.waypoints.length > 0 && ( + <> · {savedTrack.waypoints.length} {t('logs.track_upload_points')} + )} + +
+
+ + {!readOnly && ( - {!readOnly && ( - - )} -
+ )}
- - {/* Leaflet Map Div */} -
)}
diff --git a/client/src/components/LogbookDashboard.tsx b/client/src/components/LogbookDashboard.tsx index 044c57a..76f7792 100644 --- a/client/src/components/LogbookDashboard.tsx +++ b/client/src/components/LogbookDashboard.tsx @@ -5,6 +5,7 @@ import { db } from '../services/db.js' import { fetchLogbooks, createLogbook, deleteLogbook, type DecryptedLogbook } from '../services/logbook.js' import { logoutUser } from '../services/auth.js' import { useDialog } from './ModalDialog.tsx' +import AccountDangerZone from './AccountDangerZone.tsx' import { BookOpen, Plus, Trash2, LogOut, Languages, RefreshCw, Ship, User, Wifi, WifiOff } from 'lucide-react' interface LogbookDashboardProps { @@ -227,6 +228,10 @@ export default function LogbookDashboard({ onSelectLogbook, onLogout }: LogbookD )} + +
+ +
) } diff --git a/client/src/components/SettingsForm.tsx b/client/src/components/SettingsForm.tsx index 3979bc4..70498a5 100644 --- a/client/src/components/SettingsForm.tsx +++ b/client/src/components/SettingsForm.tsx @@ -1,10 +1,10 @@ import React, { useState, useEffect } from 'react' import { useTranslation } from 'react-i18next' -import { Settings as SettingsIcon, Save, Check, Users, Trash2, Copy, Link as LinkIcon, AlertTriangle } from 'lucide-react' +import { Settings as SettingsIcon, Save, Check, Users, Trash2, Copy, Link as LinkIcon } from 'lucide-react' import { ensureLogbookKey } from '../services/logbookKeys.js' -import { useDialog } from './ModalDialog.tsx' +import AccountDangerZone from './AccountDangerZone.tsx' import PwaInstallPrompt from './PwaInstallPrompt.tsx' -import { deleteAccount } from '../services/auth.js' +import { useDialog } from './ModalDialog.tsx' interface SettingsFormProps { logbookId?: string | null @@ -48,31 +48,6 @@ export default function SettingsForm({ logbookId }: SettingsFormProps) { const [shareCopied, setShareCopied] = useState(false) const [loadingShareLink, setLoadingShareLink] = useState(false) - const handleDeleteAccount = async () => { - const confirmed = await showConfirm( - t('settings.delete_account_confirm_desc'), - t('settings.delete_account_confirm_title'), - t('settings.delete_account_confirm_yes'), - t('settings.delete_account_confirm_no') - ) - - if (confirmed) { - setSaving(true) - try { - const success = await deleteAccount() - if (success) { - window.location.reload() - } else { - showAlert(t('settings.delete_account_failed')) - } - } catch (err: any) { - showAlert(err.message || t('settings.delete_account_failed')) - } finally { - setSaving(false) - } - } - } - useEffect(() => { if (logbookId) { loadCollaborators() @@ -514,30 +489,7 @@ export default function SettingsForm({ logbookId }: SettingsFormProps) {
)} {/* Danger Zone / Account Deletion */} -
-
- -

- {t('settings.danger_zone_title')} -

-
- -

- {t('settings.danger_zone_desc')} -

- -
- -
-
+
) } diff --git a/client/src/i18n/locales/de.json b/client/src/i18n/locales/de.json index caaf826..3d23597 100644 --- a/client/src/i18n/locales/de.json +++ b/client/src/i18n/locales/de.json @@ -1,7 +1,7 @@ { "translation": { "app": { - "name": "Kapteins Daagbox", + "name": "Kapteins Daagbok", "tagline": "Privates Yacht-Logbuch" }, "nav": { @@ -13,7 +13,7 @@ "settings": "Einstellungen" }, "auth": { - "welcome": "Willkommen bei Kapteins Daagbox", + "welcome": "Willkommen bei Kapteins Daagbok", "tagline": "Sicheres, E2E-verschlüsseltes maritimes Logbuch.", "register": "Mit Passkey registrieren", "login": "Mit Passkey anmelden", @@ -55,7 +55,7 @@ }, "pwa": { "title": "App installieren", - "generic_benefit": "Installieren Sie Kapteins Daagbox auf Ihrem Gerät für schnelleren Zugriff, Offline-Nutzung und dauerhafte Datenspeicherung.", + "generic_benefit": "Installieren Sie Kapteins Daagbok auf Ihrem Gerät für schnelleren Zugriff, Offline-Nutzung und dauerhafte Datenspeicherung.", "ios_instructions": "Auf dem iPad/iPhone: Fügen Sie die App zum Home-Bildschirm hinzu, damit Ihre Logbuchdaten geschützt bleiben und die App wie eine native App startet.", "ios_step_share": "Teilen-Symbol in der Safari-Leiste antippen", "ios_step_add": "„Zum Home-Bildschirm“ wählen", @@ -157,10 +157,9 @@ "photo_delete_confirm": "Sind Sie sicher, dass Sie dieses Foto unwiderruflich löschen möchten?", "confirm_yes": "Ja", "confirm_no": "Nein", - "gps_tracking_title": "GPS-Route (OpenSeaMap)", + "track_upload_title": "GPS-Track (Datei)", + "track_upload_points": "Punkte", "gps_tracking_btn_gpx": "Track-Datei herunterladen", - "gps_tracking_stat_distance": "Track-Distanz", - "gps_tracking_stat_waypoints": "Wegpunkte", "gps_track_upload_help": "Ziehen Sie eine GPX-, KML- oder GeoJSON-Datei hierher oder klicken Sie zum Auswählen", "gps_track_upload_btn": "GPS-Track hochladen", "gps_track_delete": "Track-Datei löschen", @@ -253,7 +252,8 @@ "delete_account_confirm_desc": "Sind Sie absolut sicher, dass Sie Ihr Konto und alle zugehörigen Logbücher und E2E-verschlüsselten Daten unwiderruflich löschen möchten?", "delete_account_confirm_yes": "Ja, Konto und alle Daten löschen", "delete_account_confirm_no": "Abbrechen", - "delete_account_failed": "Konto konnte nicht gelöscht werden. Bitte versuchen Sie es erneut." + "delete_account_failed": "Konto konnte nicht gelöscht werden. Bitte versuchen Sie es erneut.", + "deleting_account": "Konto wird gelöscht…" } } } diff --git a/client/src/i18n/locales/en.json b/client/src/i18n/locales/en.json index b8880c9..47c47e9 100644 --- a/client/src/i18n/locales/en.json +++ b/client/src/i18n/locales/en.json @@ -1,7 +1,7 @@ { "translation": { "app": { - "name": "Kapteins Daagbox", + "name": "Kapteins Daagbok", "tagline": "Private Yacht Logbook" }, "nav": { @@ -13,7 +13,7 @@ "settings": "Settings" }, "auth": { - "welcome": "Welcome to Kapteins Daagbox", + "welcome": "Welcome to Kapteins Daagbok", "tagline": "Secure, E2E encrypted maritime logbook.", "register": "Register with Passkey", "login": "Login with Passkey", @@ -55,7 +55,7 @@ }, "pwa": { "title": "Install app", - "generic_benefit": "Install Kapteins Daagbox on your device for faster access, offline use, and persistent data storage.", + "generic_benefit": "Install Kapteins Daagbok on your device for faster access, offline use, and persistent data storage.", "ios_instructions": "On iPad/iPhone: Add the app to your Home Screen so your logbook data stays protected and the app launches like a native app.", "ios_step_share": "Tap the Share button in the Safari toolbar", "ios_step_add": "Choose “Add to Home Screen”", @@ -157,10 +157,9 @@ "photo_delete_confirm": "Are you sure you want to permanently delete this photo?", "confirm_yes": "Yes", "confirm_no": "No", - "gps_tracking_title": "GPS Route (OpenSeaMap)", - "gps_tracking_btn_gpx": "Download Track File", - "gps_tracking_stat_distance": "Track Distance", - "gps_tracking_stat_waypoints": "Points", + "track_upload_title": "GPS track file", + "track_upload_points": "points", + "gps_tracking_btn_gpx": "Download track file", "gps_track_upload_help": "Drag & drop a GPX, KML, or GeoJSON file here, or click to select", "gps_track_upload_btn": "Upload GPS Track File", "gps_track_delete": "Delete Track File", @@ -253,7 +252,8 @@ "delete_account_confirm_desc": "Are you absolutely sure you want to permanently delete your account and all associated logbooks and E2E-encrypted data?", "delete_account_confirm_yes": "Yes, Delete Account and All Data", "delete_account_confirm_no": "Cancel", - "delete_account_failed": "Failed to delete account. Please try again." + "delete_account_failed": "Failed to delete account. Please try again.", + "deleting_account": "Deleting account…" } } } diff --git a/client/src/services/csvExport.ts b/client/src/services/csvExport.ts index 7fd07fc..657b7d5 100644 --- a/client/src/services/csvExport.ts +++ b/client/src/services/csvExport.ts @@ -166,7 +166,7 @@ export async function shareCsv(logbookId: string, title: string, preloadedData?: try { await navigator.share({ files: [file], - title: `Kapteins Daagbox - ${title}`, + title: `Kapteins Daagbok - ${title}`, text: `Logbook export for yacht ${title}` }); } catch (e: any) { diff --git a/client/src/services/gpsTracker.ts b/client/src/services/trackUpload.ts similarity index 60% rename from client/src/services/gpsTracker.ts rename to client/src/services/trackUpload.ts index e23cd5e..c3ba039 100644 --- a/client/src/services/gpsTracker.ts +++ b/client/src/services/trackUpload.ts @@ -4,7 +4,7 @@ import { getLogbookKey } from './logbookKeys.js' import { encryptJson, decryptJson } from './crypto.js' import { syncLogbook } from './sync.js' -export interface GpsWaypoint { +export interface TrackWaypoint { timestamp: number lat: number lng: number @@ -12,15 +12,14 @@ export interface GpsWaypoint { heading?: number } -export interface SavedGpsTrack { - waypoints: GpsWaypoint[] - gpxContent: string // Holds the raw text file content (GPX, KML or GeoJSON) +export interface SavedTrack { + waypoints: TrackWaypoint[] + gpxContent: string filename: string - fileType: string // 'gpx' | 'kml' | 'geojson' + fileType: string } -// Get the decrypted track data for a journal entry (with legacy array format compatibility) -export async function getDecryptedGpsTrack(entryId: string): Promise { +export async function getDecryptedTrack(entryId: string): Promise { const record = await db.gpsTracks.get(entryId) if (!record) return null @@ -33,45 +32,41 @@ export async function getDecryptedGpsTrack(entryId: string): Promise { const masterKey = await getLogbookKey(logbookId) || getActiveMasterKey() if (!masterKey) throw new Error('Encryption key not found. Please log in.') - const trackData: SavedGpsTrack = { + const trackData: SavedTrack = { waypoints, - gpxContent, + gpxContent: fileContent, filename, fileType } - // Encrypt JSON const encrypted = await encryptJson(trackData, masterKey) const now = new Date().toISOString() - // Save to Dexie await db.gpsTracks.put({ entryId, logbookId, @@ -81,7 +76,6 @@ export async function saveUploadedGpsTrack( updatedAt: now }) - // Add to Sync queue (payloadId is entryId) await db.syncQueue.put({ action: 'create', type: 'gpsTrack', @@ -91,18 +85,14 @@ export async function saveUploadedGpsTrack( updatedAt: now }) - // Trigger sync syncLogbook(logbookId).catch((err) => console.warn('Background sync failed:', err)) } -// Delete GPS track from local DB and sync queue -export async function deleteGpsTrack(logbookId: string, entryId: string): Promise { +export async function deleteTrack(logbookId: string, entryId: string): Promise { const now = new Date().toISOString() - // Delete from Dexie await db.gpsTracks.delete(entryId) - // Add to Sync queue await db.syncQueue.put({ action: 'delete', type: 'gpsTrack', @@ -112,12 +102,10 @@ export async function deleteGpsTrack(logbookId: string, entryId: string): Promis updatedAt: now }) - // Trigger sync syncLogbook(logbookId).catch((err) => console.warn('Background sync failed:', err)) } -// Download the track file exactly as uploaded -export function downloadTrackFile(track: SavedGpsTrack): void { +export function downloadTrackFile(track: SavedTrack): void { const blob = new Blob([track.gpxContent], { type: 'text/plain;charset=utf-8' }) const url = URL.createObjectURL(blob) @@ -130,24 +118,22 @@ export function downloadTrackFile(track: SavedGpsTrack): void { URL.revokeObjectURL(url) } -// Main parser entry point -export function parseTrackFile(text: string, filename: string): { waypoints: GpsWaypoint[]; type: string } { +export function parseTrackFile(text: string, filename: string): { waypoints: TrackWaypoint[]; type: string } { const lowerName = filename.toLowerCase() if (lowerName.endsWith('.kml') || text.includes(' tags const coordsTags = xmlDoc.getElementsByTagName('coordinates') for (let i = 0; i < coordsTags.length; i++) { const text = coordsTags[i].textContent || '' - const coordStrings = text.trim().split(/\s+/) - for (const str of coordStrings) { + for (const str of text.trim().split(/\s+/)) { const parts = str.split(',') if (parts.length >= 2) { const lon = parseFloat(parts[0]) @@ -202,22 +185,18 @@ export function parseKmlFile(kmlText: string): GpsWaypoint[] { } } - // Check for gx:coord extensions (commonly used in Google Earth tracks) const gxCoords = xmlDoc.getElementsByTagName('gx:coord') - if (gxCoords.length > 0) { - for (let i = 0; i < gxCoords.length; i++) { - const text = gxCoords[i].textContent || '' - const parts = text.trim().split(/\s+/) - if (parts.length >= 2) { - const lon = parseFloat(parts[0]) - const lat = parseFloat(parts[1]) - if (!isNaN(lat) && !isNaN(lon)) { - waypoints.push({ - timestamp: Date.now(), - lat: Number(lat.toFixed(6)), - lng: Number(lon.toFixed(6)) - }) - } + for (let i = 0; i < gxCoords.length; i++) { + const parts = (gxCoords[i].textContent || '').trim().split(/\s+/) + if (parts.length >= 2) { + const lon = parseFloat(parts[0]) + const lat = parseFloat(parts[1]) + if (!isNaN(lat) && !isNaN(lon)) { + waypoints.push({ + timestamp: Date.now(), + lat: Number(lat.toFixed(6)), + lng: Number(lon.toFixed(6)) + }) } } } @@ -225,9 +204,8 @@ export function parseKmlFile(kmlText: string): GpsWaypoint[] { return waypoints } -// 3. GeoJSON Parser -export function parseGeoJsonFile(geoJsonText: string): GpsWaypoint[] { - const waypoints: GpsWaypoint[] = [] +function parseGeoJsonFile(geoJsonText: string): TrackWaypoint[] { + const waypoints: TrackWaypoint[] = [] try { const data = JSON.parse(geoJsonText) @@ -235,40 +213,22 @@ export function parseGeoJsonFile(geoJsonText: string): GpsWaypoint[] { if (!geom) return if (geom.type === 'LineString' && Array.isArray(geom.coordinates)) { for (const coord of geom.coordinates) { - const lon = coord[0] - const lat = coord[1] - if (typeof lat === 'number' && typeof lon === 'number') { - waypoints.push({ - timestamp: Date.now(), - lat: Number(lat.toFixed(6)), - lng: Number(lon.toFixed(6)) - }) - } + pushCoord(waypoints, coord) } } else if (geom.type === 'MultiLineString' && Array.isArray(geom.coordinates)) { for (const line of geom.coordinates) { if (Array.isArray(line)) { for (const coord of line) { - const lon = coord[0] - const lat = coord[1] - if (typeof lat === 'number' && typeof lon === 'number') { - waypoints.push({ - timestamp: Date.now(), - lat: Number(lat.toFixed(6)), - lng: Number(lon.toFixed(6)) - }) - } + pushCoord(waypoints, coord) } } } } - }; + } if (data.type === 'FeatureCollection' && Array.isArray(data.features)) { for (const feature of data.features) { - if (feature && feature.geometry) { - processGeometry(feature.geometry) - } + if (feature?.geometry) processGeometry(feature.geometry) } } else if (data.type === 'Feature' && data.geometry) { processGeometry(data.geometry) @@ -282,8 +242,19 @@ export function parseGeoJsonFile(geoJsonText: string): GpsWaypoint[] { return waypoints } -// Generate legacy fallback GPX string -function generateLegacyGpxString(waypoints: GpsWaypoint[], dateStr: string): string { +function pushCoord(waypoints: TrackWaypoint[], coord: number[]) { + const lon = coord[0] + const lat = coord[1] + if (typeof lat === 'number' && typeof lon === 'number') { + waypoints.push({ + timestamp: Date.now(), + lat: Number(lat.toFixed(6)), + lng: Number(lon.toFixed(6)) + }) + } +} + +function buildLegacyGpx(waypoints: TrackWaypoint[], dateStr: string): string { const trkpts = waypoints .map((wp) => { const timeISO = new Date(wp.timestamp).toISOString() @@ -294,7 +265,7 @@ function generateLegacyGpxString(waypoints: GpsWaypoint[], dateStr: string): str .join('\n') return ` - + diff --git a/client/vite.config.ts b/client/vite.config.ts index d8115a2..b1d267b 100644 --- a/client/vite.config.ts +++ b/client/vite.config.ts @@ -38,8 +38,8 @@ export default defineConfig({ registerType: 'autoUpdate', includeAssets: ['favicon.ico', 'logo.png'], manifest: { - name: 'Kapteins Daagbox', - short_name: 'Daagbox', + name: 'Kapteins Daagbok', + short_name: 'Daagbok', description: 'Digital maritime ship logbook with E2E encryption and Passkeys', theme_color: '#1e293b', background_color: '#0f172a', diff --git a/scripts/start-dev-docker.sh b/scripts/start-dev-docker.sh index f5e8106..16d8555 100755 --- a/scripts/start-dev-docker.sh +++ b/scripts/start-dev-docker.sh @@ -5,7 +5,7 @@ COMPOSE_FILE="docker-compose.yml" BACKEND_CONTAINER="daagbox-prod-backend" echo "==================================================" -echo " Kapteins Daagbox Docker Environment Manager " +echo " Kapteins Daagbok Docker Environment Manager " echo "==================================================" echo "Stopping any existing container stack..." docker compose -f $COMPOSE_FILE down diff --git a/scripts/start-dev.sh b/scripts/start-dev.sh index 6cc85ee..092af8f 100755 --- a/scripts/start-dev.sh +++ b/scripts/start-dev.sh @@ -5,7 +5,7 @@ SERVER_PORT=5000 CLIENT_PORT=5173 echo "========================================" -echo " Kapteins Daagbox Dev Environment " +echo " Kapteins Daagbok Dev Environment " echo "========================================" echo "Preparing to (re)start services..." diff --git a/scripts/update-prod.sh b/scripts/update-prod.sh index 3555c3d..79c6508 100755 --- a/scripts/update-prod.sh +++ b/scripts/update-prod.sh @@ -21,7 +21,7 @@ VERSION_FILE="$REPO_ROOT/VERSION" DEFAULT_VERSION="0.1.0.0" echo "==================================================" -echo " Kapteins Daagbox Prod Environment Update " +echo " Kapteins Daagbok Prod Environment Update " echo "==================================================" echo "Target: ${REMOTE_TARGET}:${REMOTE_DIR}" echo "==================================================" diff --git a/server/package.json b/server/package.json index 1af6f13..1a1ca22 100644 --- a/server/package.json +++ b/server/package.json @@ -1,7 +1,7 @@ { "name": "server", "version": "1.0.0", - "description": "Backend API for Kapteins Daagbox", + "description": "Backend API for Kapteins Daagbok", "main": "dist/index.js", "type": "module", "scripts": { diff --git a/server/src/index.ts b/server/src/index.ts index 20c40ba..253cc9d 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -29,7 +29,7 @@ app.get('/api/health', async (req, res) => { status: 'ok', database: 'connected', timestamp: new Date().toISOString(), - service: 'Kapteins Daagbox Backend' + service: 'Kapteins Daagbok Backend' }) } catch (err: any) { res.status(500).json({ @@ -37,7 +37,7 @@ app.get('/api/health', async (req, res) => { database: 'disconnected', error: err.message, timestamp: new Date().toISOString(), - service: 'Kapteins Daagbox Backend' + service: 'Kapteins Daagbok Backend' }) } }) diff --git a/server/src/routes/auth.ts b/server/src/routes/auth.ts index 1bb1e29..feb20f0 100644 --- a/server/src/routes/auth.ts +++ b/server/src/routes/auth.ts @@ -9,7 +9,7 @@ import { prisma } from '../db.js' const router = Router() -const rpName = 'Kapteins Daagbox' +const rpName = 'Kapteins Daagbok' const rpID = process.env.RP_ID || 'localhost' const origin = process.env.ORIGIN || 'http://localhost:5173'