Compare commits

..

2 Commits

Author SHA1 Message Date
elpatron 66a32e0367 chore: release v0.1.0.7 2026-05-29 18:18:26 +02:00
elpatron 819d84eaee feat: Registrierungs-Disclaimer und Header-Zugang
Neue Accounts sehen vor dem Onboarding Hinweise zu E2E, PWA, Sync und Haftung;
bestehende Nutzer können den Disclaimer jederzeit über einen Header-Button öffnen.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-29 18:18:08 +02:00
10 changed files with 296 additions and 5 deletions
+1 -1
View File
@@ -1 +1 @@
0.1.0.7
0.1.0.8
+94
View File
@@ -334,6 +334,100 @@ html.scheme-dark .themed-select-option.is-selected {
text-align: left;
}
.registration-disclaimer {
max-width: 560px;
max-height: min(90vh, 820px);
display: flex;
flex-direction: column;
}
.registration-disclaimer--modal {
width: min(560px, calc(100vw - 32px));
margin: 0;
}
.registration-disclaimer .auth-header {
position: relative;
}
.registration-disclaimer__close {
position: absolute;
top: 0;
right: 0;
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border: none;
border-radius: 8px;
background: transparent;
color: #94a3b8;
cursor: pointer;
}
.registration-disclaimer__close:hover {
background: rgba(148, 163, 184, 0.12);
color: #e2e8f0;
}
.disclaimer-modal-overlay {
position: fixed;
inset: 0;
z-index: 10050;
display: flex;
align-items: center;
justify-content: center;
padding: 16px;
background: rgba(2, 6, 23, 0.72);
}
.disclaimer-modal-panel {
width: 100%;
max-width: 560px;
max-height: min(90vh, 820px);
}
.registration-disclaimer__intro {
margin: 0 0 16px;
font-size: 14px;
line-height: 1.55;
color: var(--app-text-muted, #94a3b8);
text-align: left;
}
.registration-disclaimer__sections {
flex: 1;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 14px;
margin-bottom: 16px;
padding-right: 4px;
text-align: left;
}
.registration-disclaimer__section h3 {
margin: 0 0 6px;
font-size: 14px;
font-weight: 600;
color: var(--app-text-heading, #f1f5f9);
}
.registration-disclaimer__section p {
margin: 0;
font-size: 13.5px;
line-height: 1.55;
color: var(--app-text-muted, #94a3b8);
}
.registration-disclaimer__copyright {
margin: 0 0 20px;
font-size: 12px;
color: var(--app-text-muted, #64748b);
text-align: center;
}
.phrase-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
+14 -2
View File
@@ -26,7 +26,8 @@ import PwaUpdatePrompt from './components/PwaUpdatePrompt.tsx'
import AppFooter from './components/AppFooter.tsx'
import { db } from './services/db.js'
import { useLiveQuery } from 'dexie-react-hooks'
import { Ship, LogOut, ChevronLeft, Users, FileText, Settings, Wifi, WifiOff } from 'lucide-react'
import { Ship, LogOut, ChevronLeft, Users, FileText, Settings, Wifi, WifiOff, Languages } from 'lucide-react'
import DisclaimerHeaderButton from './components/DisclaimerHeaderButton.tsx'
import { useTranslation } from 'react-i18next'
import {
getStoredDemoFirstEntryId,
@@ -34,7 +35,7 @@ import {
} from './services/demoLogbook.js'
function App() {
const { t } = useTranslation()
const { t, i18n } = useTranslation()
const { registerNavigation, requestStartAfterLogin } = useAppTour()
const [isAuthenticated, setIsAuthenticated] = useState(false)
const [activeLogbookId, setActiveLogbookId] = useState<string | null>(null)
@@ -194,6 +195,11 @@ function App() {
localStorage.removeItem('active_logbook_title')
}
const toggleLanguage = () => {
const nextLang = i18n.language.startsWith('de') ? 'en' : 'de'
i18n.changeLanguage(nextLang)
}
if (isViewerMode) {
return (
<div style={{ display: 'contents' }}>
@@ -275,6 +281,12 @@ function App() {
<span>{online ? 'Online' : t('sync.status_offline')}</span>
</div>
<button className="btn-icon" onClick={toggleLanguage} title="Switch Language">
<Languages size={18} />
</button>
<DisclaimerHeaderButton />
<button className="btn-icon logout" onClick={handleLogout} title={t('dashboard.logout')}>
<LogOut size={18} />
</button>
+26 -2
View File
@@ -12,6 +12,7 @@ import {
forgetUsername
} from '../services/auth.js'
import { KeyRound, ShieldAlert, Languages, HelpCircle, UserRound, X } from 'lucide-react'
import RegistrationDisclaimer from './RegistrationDisclaimer.tsx'
interface AuthOnboardingProps {
onAuthenticated: () => void
@@ -45,6 +46,23 @@ export default function AuthOnboarding({ onAuthenticated }: AuthOnboardingProps)
const [showPinLogin, setShowPinLogin] = useState(false)
const [pinLoginInput, setPinLoginInput] = useState('')
const [isNewRegistration, setIsNewRegistration] = useState(false)
const [showDisclaimer, setShowDisclaimer] = useState(false)
const finishAuth = () => {
if (isNewRegistration) {
setShowDisclaimer(true)
return
}
onAuthenticated()
}
const handleDisclaimerAccept = () => {
setIsNewRegistration(false)
setShowDisclaimer(false)
onAuthenticated()
}
const handleRegister = async (e: React.FormEvent) => {
e.preventDefault()
if (!username.trim()) return
@@ -54,6 +72,7 @@ export default function AuthOnboarding({ onAuthenticated }: AuthOnboardingProps)
try {
const result = await registerUser(username.trim())
if (result.verified) {
setIsNewRegistration(true)
setRecoveryPhrase(result.recoveryPhrase)
}
} catch (err: any) {
@@ -148,7 +167,7 @@ export default function AuthOnboarding({ onAuthenticated }: AuthOnboardingProps)
const activeKey = getActiveMasterKey()
if (activeKey) {
await setLocalPin(pinInput.trim(), pinSetupUsername, activeKey)
onAuthenticated()
finishAuth()
} else {
setError('No active master key found')
}
@@ -198,6 +217,11 @@ export default function AuthOnboarding({ onAuthenticated }: AuthOnboardingProps)
}
}
// Render 0: Registration disclaimer (new accounts only, before app onboarding)
if (showDisclaimer) {
return <RegistrationDisclaimer variant="accept" onDismiss={handleDisclaimerAccept} />
}
// Render 1: Display new registration recovery phrase
if (recoveryPhrase) {
return (
@@ -266,7 +290,7 @@ export default function AuthOnboarding({ onAuthenticated }: AuthOnboardingProps)
<button
type="button"
className="btn secondary"
onClick={onAuthenticated}
onClick={finishAuth}
disabled={loading}
>
{t('auth.skip_pin')}
@@ -0,0 +1,24 @@
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { ScrollText } from 'lucide-react'
import DisclaimerModal from './DisclaimerModal.tsx'
export default function DisclaimerHeaderButton() {
const { t } = useTranslation()
const [open, setOpen] = useState(false)
return (
<>
<button
type="button"
className="btn-icon"
onClick={() => setOpen(true)}
title={t('disclaimer.button_title')}
aria-label={t('disclaimer.button_title')}
>
<ScrollText size={18} />
</button>
<DisclaimerModal open={open} onClose={() => setOpen(false)} />
</>
)
}
+28
View File
@@ -0,0 +1,28 @@
import { useEffect } from 'react'
import RegistrationDisclaimer from './RegistrationDisclaimer.tsx'
interface DisclaimerModalProps {
open: boolean
onClose: () => void
}
export default function DisclaimerModal({ open, onClose }: DisclaimerModalProps) {
useEffect(() => {
if (!open) return
const onKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') onClose()
}
window.addEventListener('keydown', onKeyDown)
return () => window.removeEventListener('keydown', onKeyDown)
}, [open, onClose])
if (!open) return null
return (
<div className="disclaimer-modal-overlay" onClick={onClose}>
<div className="disclaimer-modal-panel" onClick={(event) => event.stopPropagation()}>
<RegistrationDisclaimer variant="view" onDismiss={onClose} />
</div>
</div>
)
}
@@ -7,6 +7,7 @@ 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'
import DisclaimerHeaderButton from './DisclaimerHeaderButton.tsx'
interface LogbookDashboardProps {
onSelectLogbook: (id: string, title: string) => void
@@ -149,6 +150,8 @@ export default function LogbookDashboard({ onSelectLogbook, onLogout }: LogbookD
<Languages size={18} />
</button>
<DisclaimerHeaderButton />
{/* Logout */}
<button className="btn-icon logout" onClick={handleLogout} title={t('dashboard.logout')}>
<LogOut size={18} />
@@ -0,0 +1,66 @@
import { useTranslation } from 'react-i18next'
import { ScrollText, X } from 'lucide-react'
export type DisclaimerVariant = 'accept' | 'view'
interface RegistrationDisclaimerProps {
onDismiss: () => void
variant?: DisclaimerVariant
}
export default function RegistrationDisclaimer({
onDismiss,
variant = 'accept'
}: RegistrationDisclaimerProps) {
const { t } = useTranslation()
const sections = [
{ title: t('disclaimer.e2e_title'), body: t('disclaimer.e2e_body') },
{ title: t('disclaimer.pwa_title'), body: t('disclaimer.pwa_body') },
{ title: t('disclaimer.storage_title'), body: t('disclaimer.storage_body') },
{ title: t('disclaimer.free_title'), body: t('disclaimer.free_body') },
{ title: t('disclaimer.liability_title'), body: t('disclaimer.liability_body') },
{ title: t('disclaimer.warranty_title'), body: t('disclaimer.warranty_body') }
]
return (
<div
className={`auth-card glass registration-disclaimer${variant === 'view' ? ' registration-disclaimer--modal' : ''}`}
role="document"
>
<div className="auth-header">
<ScrollText className="auth-icon accent" size={48} />
<h2>{t('disclaimer.title')}</h2>
{variant === 'view' && (
<button
type="button"
className="registration-disclaimer__close"
onClick={onDismiss}
aria-label={t('disclaimer.close')}
>
<X size={18} />
</button>
)}
</div>
<p className="registration-disclaimer__intro">{t('disclaimer.intro')}</p>
<div className="registration-disclaimer__sections">
{sections.map((section) => (
<section key={section.title} className="registration-disclaimer__section">
<h3>{section.title}</h3>
<p>{section.body}</p>
</section>
))}
</div>
<p className="registration-disclaimer__copyright">{t('disclaimer.copyright')}</p>
<div className="auth-actions">
<button type="button" className="btn primary" onClick={onDismiss}>
{variant === 'accept' ? t('disclaimer.accept') : t('disclaimer.close')}
</button>
</div>
</div>
)
}
+20
View File
@@ -313,6 +313,26 @@
"tour_desc": "Lassen Sie sich erneut durch die wichtigsten Bereiche der App führen.",
"tour_restart": "Tour erneut starten"
},
"disclaimer": {
"title": "Wichtige Hinweise",
"intro": "Bitte lesen Sie die folgenden Hinweise, bevor Sie Kapteins Daagbok nutzen.",
"e2e_title": "Ende-zu-Ende-Verschlüsselung",
"e2e_body": "Ihre Logbuchdaten werden Ende-zu-Ende verschlüsselt. Nur Sie bzw. Personen mit Ihrem Schlüssel können die Inhalte lesen. Auf dem Server werden ausschließlich verschlüsselte Daten gespeichert.",
"pwa_title": "Progressive Web App (PWA)",
"pwa_body": "Kapteins Daagbok läuft als Progressive Web App in Ihrem Browser und kann auf Ihrem Gerät installiert werden ähnlich wie eine native App, ohne App-Store.",
"storage_title": "Lokale Speicherung & Synchronisation",
"storage_body": "Ihre Daten werden lokal auf Ihrem Gerät zwischengespeichert (IndexedDB). Bei aktiver Internetverbindung werden Änderungen mit dem Server synchronisiert. Ohne Verbindung können Sie weiterarbeiten; die Synchronisation erfolgt später.",
"free_title": "Kostenlos & werbefrei",
"free_body": "Kapteins Daagbok ist kostenlos und enthält keine Werbung.",
"liability_title": "Haftungsausschluss",
"liability_body": "Die Nutzung erfolgt auf eigene Verantwortung. Es wird keine Haftung für Schäden übernommen, die aus der Nutzung der App entstehen einschließlich fehlerhafter oder unvollständiger Logbucheinträge, Datenverlust oder technischen Störungen.",
"warranty_title": "Keine Gewährleistung",
"warranty_body": "Es wird keine Gewährleistung für die Funktion, Richtigkeit oder Verfügbarkeit des Dienstes übernommen. Der Betrieb kann jederzeit unterbrochen, eingeschränkt oder eingestellt werden.",
"copyright": "© 2026 KnorrLabs, Markus F.J. Busche",
"accept": "Akzeptieren und fortfahren",
"close": "Schließen",
"button_title": "Hinweise & Haftungsausschluss"
},
"demo": {
"logbook_title": "Demo-Logbuch Ostsee",
"badge": "Demo"
+20
View File
@@ -313,6 +313,26 @@
"tour_desc": "Take a guided walkthrough of the main areas of the app again.",
"tour_restart": "Restart tour"
},
"disclaimer": {
"title": "Important notice",
"intro": "Please read the following information before using Kapteins Daagbok.",
"e2e_title": "End-to-end encryption",
"e2e_body": "Your logbook data is encrypted end-to-end. Only you or people with your key can read the contents. The server stores encrypted data only.",
"pwa_title": "Progressive Web App (PWA)",
"pwa_body": "Kapteins Daagbok runs as a Progressive Web App in your browser and can be installed on your device similar to a native app, without an app store.",
"storage_title": "Local storage & sync",
"storage_body": "Your data is cached locally on your device (IndexedDB). When online, changes are synced to the server. You can keep working offline; sync happens when connectivity returns.",
"free_title": "Free & ad-free",
"free_body": "Kapteins Daagbok is free to use and contains no advertising.",
"liability_title": "Disclaimer of liability",
"liability_body": "Use is at your own risk. No liability is accepted for damages arising from use of the app including incorrect or incomplete log entries, data loss, or technical failures.",
"warranty_title": "No warranty",
"warranty_body": "No warranty is provided for functionality, accuracy, or availability of the service. Operation may be interrupted, limited, or discontinued at any time.",
"copyright": "© 2026 KnorrLabs, Markus F.J. Busche",
"accept": "Accept and continue",
"close": "Close",
"button_title": "Legal notice & disclaimer"
},
"demo": {
"logbook_title": "Baltic Sea Demo Logbook",
"badge": "Demo"