# -*- coding: utf-8 -*-
"""
GEO-SAILING: Impressum, Datenschutzerklaerung und Haftungsausschluss.

Die drei Seiten sind OEFFENTLICH (ohne Key) erreichbar, weil ein Impressum jederzeit und
ohne Anmeldung abrufbar sein muss. Sie werden bei jedem Aufruf aus den Angaben in
daten/rechtliches.json erzeugt. Diese Angaben traegt der Admin auf der Seite "Lizenzen"
(admin.html, Bereich "Rechtliches") ein.

Wichtig: Die Texte sind eine sorgfaeltig gemachte Vorlage, aber keine Rechtsberatung.
Bitte einmal von einer fachkundigen Person pruefen lassen.
"""
import os, json, html, re
from datetime import datetime

# key, Beschriftung, Typ, Pflicht, Hinweis, Platzhalter, Vorgabe
FIELDS = [
    ('Anbieter (Impressum)', [
        ('name', 'Vor- und Nachname', 'text', True, 'So, wie er im Impressum stehen soll.', 'Vorname Nachname', ''),
        ('firma', 'Firma / Verein / Rechtsform', 'text', False, 'Nur ausfüllen, wenn der Dienst unter einer Firma oder einem Verein betrieben wird.', '', ''),
        ('vertretung', 'Vertretungsberechtigte Person', 'text', False, 'Nur bei Firma oder Verein (z. B. Geschäftsführer, Vorstand).', '', ''),
        ('strasse', 'Straße und Hausnummer', 'text', True, 'Ladungsfähige Anschrift. Ein Postfach reicht nicht.', 'Musterstraße 1', ''),
        ('plz_ort', 'PLZ und Ort', 'text', True, '', '12345 Musterstadt', ''),
        ('land', 'Land', 'text', True, '', '', 'Deutschland'),
        ('email', 'E-Mail-Adresse', 'text', True, 'Wird im Impressum und in der Datenschutzerklärung angezeigt.', '', 'niklas.gleinig@googlemail.com'),
        ('telefon', 'Telefon', 'text', False, 'Freiwillig. Eine zweite schnelle Kontaktmöglichkeit neben der E-Mail ist üblich, aber nicht immer Pflicht.', '+49 ...', ''),
        ('register', 'Registereintrag', 'text', False, 'Nur falls vorhanden (z. B. Amtsgericht und HRB-/VR-Nummer).', '', ''),
        ('ust_id', 'Umsatzsteuer-ID', 'text', False, 'Nur falls vorhanden (nach § 27a UStG).', 'DE123456789', ''),
        ('redaktion', 'Verantwortlich für Inhalte (§ 18 Abs. 2 MStV)', 'textarea', False, 'Nur ausfüllen, wenn eine andere Person als der Anbieter verantwortlich ist (Name und Anschrift). Leer lassen = Anbieter.', '', ''),
        ('impressum_zusatz', 'Eigene Ergänzung zum Impressum', 'textarea', False, 'Freier Text, erscheint am Ende des Impressums.', '', ''),
    ]),
    ('Server und Datenschutz', [
        ('hoster', 'Hosting-Anbieter (Name und Anschrift)', 'textarea', True, 'Bei einem gemieteten Server: Firma und Anschrift des Anbieters. Läuft der Server bei dir selbst, einfach „Eigener Server“ eintragen.', 'Hoster GmbH, Straße 1, 12345 Stadt', ''),
        ('serverstandort', 'Standort des Servers (Land)', 'text', True, '', '', 'Deutschland'),
        ('log_dauer', 'Aufbewahrung des Zugriffsprotokolls', 'text', True, 'Wie lange das Zugriffsprotokoll (daten/zugriffe.log) und die Apache-Logs aufbewahrt werden. Das Programm löscht nichts selbst, bitte halte dich an die hier genannte Zeit.', 'z. B. 90 Tage', ''),
        ('aufsicht', 'Zuständige Datenschutz-Aufsichtsbehörde', 'textarea', False, 'Name und Anschrift der Landesdatenschutzbehörde deines Bundeslandes. Leer lassen = allgemeiner Hinweis auf die Aufsichtsbehörde.', '', ''),
        ('dsb', 'Datenschutzbeauftragte/r', 'textarea', False, 'Nur falls benannt (Name und Kontakt).', '', ''),
        ('schlichtung', 'Verbraucherschlichtung (§ 36 VSBG)', 'select', True, 'Pflichtangabe für Unternehmer. Für rein privaten Betrieb gilt sie nicht, die Aussage schadet aber nicht.', '', 'nein'),
        ('datenschutz_zusatz', 'Eigene Ergänzung zur Datenschutzerklärung', 'textarea', False, 'Freier Text, erscheint am Ende der Datenschutzerklärung.', '', ''),
        ('haftung_zusatz', 'Eigene Ergänzung zum Haftungsausschluss', 'textarea', False, 'Freier Text, erscheint am Ende des Haftungsausschlusses.', '', ''),
    ]),
]
SELECTS = {'schlichtung': [('nein', 'Nicht bereit und nicht verpflichtet'), ('ja', 'Bereit, an einem Verfahren teilzunehmen')]}
MAXLEN = {'textarea': 2000, 'text': 200}
KEYS = [f[0] for _, grp in FIELDS for f in grp]
FLAT = {f[0]: f for _, grp in FIELDS for f in grp}
REQUIRED = [f[0] for _, grp in FIELDS for f in grp if f[3]]
DEFAULTS = {f[0]: f[6] for _, grp in FIELDS for f in grp}
PAGES = {'impressum': 'Impressum', 'datenschutz': 'Datenschutzerklärung', 'haftungsausschluss': 'Haftungsausschluss'}


def path(data_dir): return os.path.join(data_dir, 'rechtliches.json')


def load(data_dir):
    v = dict(DEFAULTS)
    try:
        with open(path(data_dir), encoding='utf-8') as f:
            d = json.load(f)
        for k in KEYS:
            if isinstance(d.get(k), str): v[k] = d[k]
        v['_stand'] = d.get('_stand') if isinstance(d.get('_stand'), str) else ''
        v['_aktiv'] = d.get('_aktiv') is not False
    except Exception:
        v['_stand'] = ''; v['_aktiv'] = True
    return v


def active(data_dir): return load(data_dir)['_aktiv']


def set_active(data_dir, on):
    """Die drei Seiten (und ihre Links) ein- oder ausschalten; die Angaben bleiben erhalten."""
    d = load(data_dir); d['_aktiv'] = bool(on)
    tmp = path(data_dir) + '.tmp'
    with open(tmp, 'w', encoding='utf-8') as f: json.dump(d, f, ensure_ascii=False, indent=1)
    os.replace(tmp, path(data_dir))


def clean(raw):
    """Eingaben pruefen: nur bekannte Felder, nur Text, Laenge begrenzt, Steuerzeichen raus."""
    out = {}
    for k in KEYS:
        t = FLAT[k][2]; x = raw.get(k, '') if isinstance(raw, dict) else ''
        x = x if isinstance(x, str) else ''
        x = re.sub(r'[\x00-\x08\x0b\x0c\x0e-\x1f]', '', x).replace('\r\n', '\n').replace('\r', '\n').strip()
        if t == 'select':
            ok = [o[0] for o in SELECTS[k]]; x = x if x in ok else DEFAULTS[k]
        else:
            x = x[:MAXLEN[t]]
        out[k] = x
    return out


def save(data_dir, vals):
    d = clean(vals); d['_stand'] = datetime.now().strftime('%Y-%m-%d'); d['_aktiv'] = active(data_dir)
    tmp = path(data_dir) + '.tmp'
    with open(tmp, 'w', encoding='utf-8') as f: json.dump(d, f, ensure_ascii=False, indent=1)
    os.replace(tmp, path(data_dir))
    return d


def missing(v):
    return [FLAT[k][1] for k in REQUIRED if not (v.get(k) or '').strip()]


def spec(v):
    """Beschreibung der Felder fuer die Admin-Seite."""
    groups = []
    for title, grp in FIELDS:
        groups.append({'title': title, 'fields': [
            {'key': k, 'label': l, 'type': t, 'required': r, 'hint': h, 'placeholder': p, 'value': v.get(k, ''),
             'options': [{'v': a, 'l': b} for a, b in SELECTS.get(k, [])]}
            for k, l, t, r, h, p, _d in grp]})
    return groups


# ------------------------------------------------------------------ Darstellung
def E(s): return html.escape(s or '', quote=True)
def M(v, k, label=None):
    """Wert ausgeben; fehlt eine Pflichtangabe, steht eine sichtbare Markierung da."""
    x = (v.get(k) or '').strip()
    if x: return E(x).replace('\n', '<br>')
    return '<mark class="miss">[noch ausfüllen: %s]</mark>' % E(label or FLAT[k][1])
def mail(v):
    x = (v.get('email') or '').strip()
    return '<a href="mailto:%s">%s</a>' % (E(x), E(x)) if x else M(v, 'email')
def de_date(s):
    return '%s.%s.%s' % (s[8:10], s[5:7], s[:4]) if re.match(r'^\d{4}-\d{2}-\d{2}$', s or '') else ''
def anbieter(v):
    zeilen = []
    if (v.get('firma') or '').strip(): zeilen.append(E(v['firma'].strip()))
    zeilen.append(M(v, 'name'))
    zeilen.append(M(v, 'strasse')); zeilen.append(M(v, 'plz_ort'))
    land = (v.get('land') or '').strip()
    if land and land.lower() not in ('deutschland', 'germany'): zeilen.append(E(land))
    return '<br>'.join(zeilen)
def selbst(v): return (v.get('hoster') or '').strip().lower().startswith('eigen')

CSS = '''
:root{--bg:#e9f0ef;--surface:#f9fcfb;--ink:#12252b;--muted:#55696f;--line:#cbd9d9;--accent:#0a6573;--bad:#a8201a;--mk:#fff1c2;color-scheme:light}
@media (prefers-color-scheme:dark){:root:not([data-theme="light"]){--bg:#0b171b;--surface:#112228;--ink:#e3eeee;--muted:#94aaae;--line:#23393f;--accent:#4fc0cf;--bad:#f58a82;--mk:#4a3d10;color-scheme:dark}}
:root[data-theme="dark"]{--bg:#0b171b;--surface:#112228;--ink:#e3eeee;--muted:#94aaae;--line:#23393f;--accent:#4fc0cf;--bad:#f58a82;--mk:#4a3d10;color-scheme:dark}
:root[data-theme="black"]{--bg:#000;--surface:#0a1316;--ink:#e3eeee;--muted:#94aaae;--line:#23393f;--accent:#4fc0cf;--bad:#f58a82;--mk:#4a3d10;color-scheme:dark}
*{box-sizing:border-box}html{-webkit-text-size-adjust:100%}
body{margin:0;background:var(--bg);color:var(--ink);font:16px/1.65 "IBM Plex Sans",system-ui,-apple-system,"Segoe UI",sans-serif;padding:max(14px,env(safe-area-inset-top,0px)) max(16px,env(safe-area-inset-right,0px)) max(28px,env(safe-area-inset-bottom,0px)) max(16px,env(safe-area-inset-left,0px))}
.wrap{max-width:780px;margin:0 auto}
nav{display:flex;flex-wrap:wrap;gap:8px 16px;align-items:center;margin:4px 0 18px;font-size:14px}
nav a{color:var(--accent);text-decoration:none;font-weight:600}nav a:hover{text-decoration:underline}
nav a[aria-current]{color:var(--ink);text-decoration:underline}
main{background:var(--surface);border:1px solid var(--line);border-radius:12px;padding:22px clamp(16px,4vw,32px) 28px}
h1{font:700 clamp(26px,5vw,34px)/1.2 "Bricolage Grotesque",system-ui,sans-serif;margin:0 0 4px;letter-spacing:-.01em}
h2{font:700 19px/1.3 "Bricolage Grotesque",system-ui,sans-serif;margin:28px 0 6px}
h3{font:600 16px/1.4 inherit;margin:18px 0 4px}
p,ul{margin:8px 0}ul{padding-left:22px}li{margin:4px 0}
.stand{color:var(--muted);font-size:14px;margin:0 0 6px}
a{color:var(--accent);overflow-wrap:anywhere}
mark.miss{background:var(--mk);color:var(--ink);border-radius:4px;padding:0 4px;font-weight:600}
.note{border-left:4px solid var(--accent);background:var(--bg);padding:10px 14px;border-radius:0 8px 8px 0;margin:14px 0}
table{border-collapse:collapse;width:100%;font-size:14.5px;margin:10px 0}th,td{border:1px solid var(--line);padding:7px 10px;text-align:left;vertical-align:top}th{background:var(--bg)}
.tw{overflow-x:auto}
@media print{nav{display:none}body{background:#fff;color:#000}main{border:0;padding:0}}
'''
THEME_JS = '''(function(){try{var t=localStorage.getItem('ostsee-theme');if(t==='light'||t==='dark'||t==='black')document.documentElement.setAttribute('data-theme',t)}catch(e){}})();'''

def shell(slug, body):
    titel = PAGES[slug]
    links = ''.join('<a href="%s.html"%s>%s</a>' % (s, ' aria-current="page"' if s == slug else '', n) for s, n in PAGES.items())
    return ('<!doctype html>\n<html lang="de"><head><meta charset="utf-8">'
            '<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">'
            '<meta name="robots" content="noindex"><title>%s · GEO-SAILING</title>'
            '<link rel="icon" href="icon-192.png"><style>%s</style><script>%s</script></head><body><div class="wrap">'
            '<nav aria-label="Rechtliches"><a href="index.html">← GEO-SAILING</a>%s</nav><main>%s</main></div></body></html>'
            % (E(titel), CSS, THEME_JS, links, body))


def impressum(v, geo=True):
    b = ['<h1>Impressum</h1>']
    b.append('<h2>Angaben nach § 5 DDG</h2><p>%s</p>' % anbieter(v))
    b.append('<h2>Kontakt</h2><p>E-Mail: %s' % mail(v))
    if (v.get('telefon') or '').strip(): b.append('<br>Telefon: %s' % M(v, 'telefon'))
    b.append('</p>')
    if (v.get('vertretung') or '').strip(): b.append('<h2>Vertretungsberechtigt</h2><p>%s</p>' % M(v, 'vertretung'))
    if (v.get('register') or '').strip(): b.append('<h2>Registereintrag</h2><p>%s</p>' % M(v, 'register'))
    if (v.get('ust_id') or '').strip(): b.append('<h2>Umsatzsteuer-ID</h2><p>Umsatzsteuer-Identifikationsnummer nach § 27a Umsatzsteuergesetz:<br>%s</p>' % M(v, 'ust_id'))
    if (v.get('redaktion') or '').strip():
        b.append('<h2>Verantwortlich für den Inhalt nach § 18 Abs. 2 MStV</h2><p>%s</p>' % M(v, 'redaktion'))
    else:
        b.append('<h2>Verantwortlich für den Inhalt nach § 18 Abs. 2 MStV</h2><p>%s</p>' % anbieter(v))
    b.append('<h2>Hinweis zum Angebot</h2><p>GEO-SAILING ist ein Hilfsmittel zur Törn-Vorbereitung und zum Führen eines Logbuchs. '
             'Der Zugang ist nur mit persönlichem Key möglich. Es gelten der <a href="haftungsausschluss.html">Haftungsausschluss</a> '
             'und die <a href="datenschutz.html">Datenschutzerklärung</a>.</p>')
    if v.get('schlichtung') == 'ja':
        b.append('<h2>Verbraucherschlichtung</h2><p>Wir sind bereit, an einem Streitbeilegungsverfahren vor einer Verbraucherschlichtungsstelle teilzunehmen.</p>')
    else:
        b.append('<h2>Verbraucherschlichtung</h2><p>Wir sind nicht bereit und nicht verpflichtet, an Streitbeilegungsverfahren vor einer Verbraucherschlichtungsstelle teilzunehmen.</p>')
    if (v.get('impressum_zusatz') or '').strip(): b.append('<h2>Weitere Angaben</h2><p>%s</p>' % M(v, 'impressum_zusatz'))
    return shell('impressum', ''.join(b))


def haftung(v, geo=True):
    b = ['<h1>Haftungsausschluss</h1>']
    if de_date(v.get('_stand')): b.append('<p class="stand">Stand: %s</p>' % de_date(v['_stand']))
    b.append('<div class="note"><b>Wichtig für die Navigation:</b> GEO-SAILING ist ein Hilfsmittel zur Vorbereitung und Begleitung eines Törns. '
             'Es ersetzt weder amtliche Seekarten, Seehandbücher, Gezeiten- und Strömungsangaben, Nachrichten für Seefahrer noch den amtlichen '
             'Seewetterbericht, ein Funkgerät, eine gute Seemannschaft oder die Ausbildung der Crew. Die Verantwortung für Schiff, Crew und Kurs '
             'liegt jederzeit bei der Schiffsführung.</div>')
    b.append('<h2>1. Keine Gewähr für Inhalte</h2><p>Die Inhalte (Checklisten, Hinweise zu Sprechfunk, Manövern und Einweisung, Törn-Briefing, Karten, '
             'Wetter- und Warnungsdarstellung, Berechnungen, Logbuch) wurden mit Sorgfalt erstellt. Für Richtigkeit, Vollständigkeit, Aktualität '
             'und Verfügbarkeit wird jedoch keine Gewähr übernommen. Die Inhalte sind allgemeine Hinweise und keine Beratung im Einzelfall, '
             'insbesondere keine nautische, rechtliche, medizinische oder versicherungsrechtliche Beratung.</p>')
    b.append('<h2>2. Wetter, Warnungen, Radar und Karten</h2><p>Wetter- und Seegangsdaten, Warnungen, Radarbilder und Kartenmaterial stammen von '
             'Dritten (u. a. Open-Meteo, Deutscher Wetterdienst, SMHI, OpenStreetMap, OpenSeaMap, OpenTopoMap) und werden automatisch '
             'übernommen. Sie können fehlerhaft, verzögert, lückenhaft oder zeitweise nicht verfügbar sein; Prognosen sind naturgemäß unsicher. '
             'Karten enthalten keine amtlich geprüften Tiefen-, Tonnen- oder Gefahrenangaben und sind nicht für die Navigation zugelassen. '
             'Vor dem Auslaufen und während der Fahrt sind die amtlichen Quellen (z. B. Seewetterbericht, NAVTEX, BSH, Seekarten) maßgeblich. '
             'Bei Ausfall des Netzes oder im Offline-Modus können angezeigte Daten veraltet sein.</p>')
    b.append('<h2>3. Positionsdaten und Logbuch</h2><p>Positionen aus der Ortung des Geräts können ungenau sein oder ausfallen. Das Logbuch '
             'ist eine persönliche Aufzeichnung und ersetzt nicht ein gesetzlich oder versicherungsrechtlich vorgeschriebenes Schiffstagebuch. '
             'Für den Verlust oder die Veränderung von Einträgen (z. B. durch Gerätefehler, gelöschte Browserdaten, Synchronisationsfehler oder '
             'Ablauf bzw. Sperrung eines Keys) wird nicht gehaftet. Bitte sichere wichtige Daten zusätzlich, z. B. über die Export-Funktion.</p>')
    b.append('<h2>4. Haftung</h2><p>Der Anbieter haftet unbeschränkt bei Vorsatz und grober Fahrlässigkeit, bei Verletzung von Leben, Körper oder '
             'Gesundheit sowie nach dem Produkthaftungsgesetz und soweit eine Garantie übernommen wurde. Bei leicht fahrlässiger Verletzung '
             'wesentlicher Vertragspflichten (Pflichten, deren Erfüllung die ordnungsgemäße Durchführung erst ermöglicht und auf deren Einhaltung '
             'man regelmäßig vertrauen darf) ist die Haftung auf den vorhersehbaren, typischen Schaden begrenzt. Im Übrigen ist die Haftung '
             'ausgeschlossen, soweit gesetzlich zulässig. Das gilt auch für Schäden an Schiff, Ausrüstung oder Gesundheit, die aus der Nutzung '
             'oder Nichtnutzung der Inhalte entstehen. Zwingende gesetzliche Haftung bleibt unberührt.</p>')
    b.append('<h2>5. Verfügbarkeit</h2><p>Der Dienst wird ohne Anspruch auf ständige Verfügbarkeit angeboten. Wartung, Störungen, Änderungen, '
             'Sperrung oder Ablauf eines Keys können den Zugang zeitweise oder dauerhaft einschränken. Ein Anspruch auf Fortführung besteht nicht, '
             'soweit nichts anderes vereinbart ist.</p>')
    b.append('<h2>6. Externe Links und Dienste</h2><p>Die Seiten enthalten Links zu externen Webseiten und nutzen externe Dienste. Auf deren Inhalte '
             'hat der Anbieter keinen Einfluss und übernimmt dafür keine Gewähr. Für die Inhalte verlinkter Seiten ist stets der jeweilige '
             'Betreiber verantwortlich. Zum Zeitpunkt der Verlinkung waren keine Rechtsverstöße erkennbar; bei Bekanntwerden werden solche '
             'Links umgehend entfernt.</p>')
    b.append('<h2>7. Urheberrecht</h2><p>Die Inhalte dieser Seiten unterliegen dem deutschen Urheberrecht. Vervielfältigung, Bearbeitung und '
             'Weitergabe außerhalb der Grenzen des Urheberrechts bedürfen der Zustimmung des Anbieters. Keys und Zugänge sind persönlich und '
             'dürfen nicht weitergegeben werden. Karten- und Wetterdaten Dritter unterliegen den Lizenzen ihrer Anbieter '
             '(z. B. © OpenStreetMap-Mitwirkende, © OpenTopoMap, OpenSeaMap, Open-Meteo CC BY 4.0, Deutscher Wetterdienst).</p>')
    b.append('<h2>8. Hinweise auf Rechtsverletzungen</h2><p>Sollten dir Inhalte auffallen, die gegen Recht verstoßen, schreibe bitte an %s. '
             'Berechtigte Hinweise werden zeitnah geprüft und die Inhalte bei Bedarf geändert oder entfernt.</p>' % mail(v))
    if (v.get('haftung_zusatz') or '').strip(): b.append('<h2>9. Weitere Hinweise</h2><p>%s</p>' % M(v, 'haftung_zusatz'))
    b.append('<p style="margin-top:22px">Siehe auch <a href="impressum.html">Impressum</a> und <a href="datenschutz.html">Datenschutzerklärung</a>.</p>')
    return shell('haftungsausschluss', ''.join(b))


def datenschutz(v, geo=True):
    b = ['<h1>Datenschutzerklärung</h1>']
    if de_date(v.get('_stand')): b.append('<p class="stand">Stand: %s</p>' % de_date(v['_stand']))
    b.append('<p>Hier steht, welche personenbezogenen Daten bei der Nutzung von GEO-SAILING verarbeitet werden, wofür und wie lange. '
             'GEO-SAILING ist kein öffentliches Angebot: Die Inhalte sind nur mit einem persönlichen Key abrufbar. Es gibt keine Werbung, '
             'kein Tracking, keine Analyse- oder Statistik-Dienste und keine automatisierte Entscheidungsfindung.</p>')
    b.append('<h2>1. Verantwortlicher</h2><p>%s<br>E-Mail: %s</p>' % (anbieter(v), mail(v)))
    if (v.get('dsb') or '').strip(): b.append('<p><b>Datenschutzbeauftragte/r:</b><br>%s</p>' % M(v, 'dsb'))
    b.append('<p>Weitere Angaben stehen im <a href="impressum.html">Impressum</a>.</p>')

    b.append('<h2>2. Hosting und Serverprotokoll</h2>')
    if selbst(v):
        b.append('<p>Die Seiten werden auf einem eigenen Server des Verantwortlichen in %s betrieben.</p>' % M(v, 'serverstandort'))
    else:
        b.append('<p>Die Seiten werden bei folgendem Hosting-Anbieter betrieben (Serverstandort: %s):<br>%s<br>Mit dem Anbieter besteht, soweit erforderlich, '
                 'ein Vertrag zur Auftragsverarbeitung nach Art. 28 DSGVO.</p>' % (M(v, 'serverstandort'), M(v, 'hoster')))
    b.append('<p>Bei jedem Aufruf verarbeitet der Server technisch notwendige Daten: IP-Adresse, Datum und Uhrzeit, aufgerufene Seite oder '
             'Schnittstelle, Antwortstatus und Browser-Kennung (User-Agent). Das Programm führt außerdem ein Zugriffsprotokoll mit Zeit, '
             'IP-Adresse, Ereignis (z. B. Anmeldung, Seitenaufruf, abgelehnter Key) und Namen des Keys. Zweck sind Betrieb, Fehlersuche, '
             'Schutz vor Missbrauch und Erkennung von weitergegebenen Keys. Rechtsgrundlage ist Art. 6 Abs. 1 lit. f DSGVO (berechtigtes '
             'Interesse an einem sicheren und kontrollierten Betrieb). Die Protokolle werden %s aufbewahrt und danach gelöscht.</p>' % M(v, 'log_dauer'))

    b.append('<h2>3. Key, Gerätebindung und Anmeldung</h2><p>Der Zugang erfolgt über einen persönlichen Key. Gespeichert werden:</p><ul>'
             '<li>ein Hash des Keys (der Key selbst ist nicht auslesbar), der vergebene Name des Keys, Erstellungs- und Ablaufdatum, Sperr-Status und die erlaubte Geräteanzahl,</li>'
             '<li>eine zufällige Gerätekennung als Hash, Zeitpunkt der ersten und letzten Nutzung und die zuletzt verwendete IP-Adresse,</li>'
             '<li>ein Verlauf der IP-Adressen, mit denen der Key benutzt wurde (erstes und letztes Auftreten, Anzahl der Aufrufe),</li>'
             '<li>kurzzeitig die IP-Adresse bei Fehlversuchen (Sperre nach 10 Fehlversuchen in 10 Minuten).</li></ul>'
             '<p>Zweck ist die Zugangskontrolle, die Bindung des Keys an Geräte und der Schutz vor Missbrauch. Rechtsgrundlage ist Art. 6 Abs. 1 '
             'lit. b DSGVO (Bereitstellung des Zugangs) und lit. f DSGVO (Schutz vor unberechtigter Nutzung). Ohne diese Daten ist die Nutzung '
             'nicht möglich. Die Daten werden gelöscht, wenn der Key gelöscht wird, auf Wunsch auch früher.</p>')
    if geo:
        b.append('<p><b>Standortabfrage zur IP-Adresse:</b> Zur Beurteilung auffälliger Zugriffe kann der Verantwortliche im Admin-Bereich '
                 'die grobe Herkunft einer IP-Adresse (Ort, Land, Netzbetreiber) abfragen. Dafür wird die IP-Adresse an den Dienst ipwho.is '
                 'übermittelt; das Ergebnis wird auf dem Server zwischengespeichert. Rechtsgrundlage ist Art. 6 Abs. 1 lit. f DSGVO.</p>')

    b.append('<h2>4. Cookie und lokale Speicherung auf deinem Gerät</h2><p>Für die Anmeldung wird ein technisch notwendiges Cookie '
             '(<code>gg_dev</code>, bis zu 400 Tage, nur für diese Seite) und ein Eintrag im Browser-Speicher (<code>ostsee-dev</code>) mit der '
             'Gerätekennung gesetzt. Zusätzlich speichert die App lokal auf deinem Gerät: Darstellung (Hell/Dunkel), eigene Eingaben und Häkchen '
             '(z. B. Checklisten, Törn-Vorbereitung, Sprechfunk- und Einweisungs-Stände), eine Kopie des Logbuchs, bei aktiviertem Offline-Modus '
             'bzw. automatischer Sicherung die Seiten selbst, Bibliotheken, Schriften, bereits gesehene Kartenkacheln und die zuletzt geladenen '
             'Wetter- und Warndaten (Service Worker, Cache). Diese Speicherungen sind erforderlich, damit die von dir gewünschte Funktion '
             'bereitgestellt wird (§ 25 Abs. 2 TDDDG; Rechtsgrundlage der Verarbeitung Art. 6 Abs. 1 lit. b bzw. f DSGVO). Es werden keine '
             'Cookies zu Werbe- oder Analysezwecken gesetzt. Mit „Abmelden“ bzw. dem Löschen der Browserdaten entfernst du diese Daten wieder. '
             'Sobald das Gerät online ist, prüft die App im Hintergrund, ob der Key noch gültig ist (Offline-Frist).</p>')

    b.append('<h2>5. Logbuch, Ortung und geteilte Törns</h2><p>Das Logbuch speichert deine Einträge (Zeit, Position, Wetter, Wind, Kurs, Fahrt, Notizen, '
             'Törn-Angaben wie Boots-, Start- und Zielname) auf dem Server und ordnet sie deinem Key zu, damit sie auf mehreren Geräten und nach '
             'einem Gerätewechsel verfügbar sind. Die Ortung deines Geräts erfolgt nur nach deiner Freigabe im Browser und nur, solange du '
             'sie nutzt; die Position wird lokal ausgelesen und nur gespeichert, wenn du einen Eintrag speicherst. Rechtsgrundlage ist '
             'Art. 6 Abs. 1 lit. b DSGVO.</p><p>Teilst du einen Törn per Link oder QR-Code, können Personen, die mit eigenem Key beitreten, die '
             'Einträge dieses Törns sehen und eigene hinzufügen; angezeigt wird dabei der Name des jeweiligen Keys. Gib den Link nur an '
             'Personen weiter, die das sehen dürfen. Einträge löschst du selbst im Logbuch; mit dem Löschen des Keys werden auch alle '
             'zugehörigen Logbuch-Daten gelöscht. Auf Wunsch geschieht das jederzeit.</p>')

    b.append('<h2>6. Dienste Dritter beim Aufruf</h2><p>Beim Nutzen der Seiten lädt dein Browser Inhalte direkt von den folgenden Anbietern. '
             'Dabei wird dort zwangsläufig deine IP-Adresse sowie technische Angaben deines Browsers verarbeitet. Je nach Funktion kommen '
             'Koordinaten bzw. der angezeigte Kartenausschnitt dazu. Rechtsgrundlage ist Art. 6 Abs. 1 lit. f DSGVO (funktionsfähige, '
             'gewünschte Darstellung des Angebots; die Funktionen sind ohne diese Dienste nicht nutzbar).</p>'
             '<div class="tw"><table><tr><th>Dienst</th><th>Zweck</th><th>Übermittelt</th></tr>'
             '<tr><td>Google Fonts (Google Ireland Ltd., Dublin; Google LLC, USA)</td><td>Schriftarten</td><td>IP-Adresse, Browserdaten</td></tr>'
             '<tr><td>cdnjs (Cloudflare, Inc., USA)</td><td>Kartenbibliothek Leaflet</td><td>IP-Adresse, Browserdaten</td></tr>'
             '<tr><td>OpenTopoMap, OpenSeaMap, OpenStreetMap (Tile-Server), EOX Sentinel-2 cloudless</td><td>Kartenkacheln und Seezeichen</td><td>IP-Adresse, Kartenausschnitt und Zoomstufe</td></tr>'
             '<tr><td>Open-Meteo</td><td>Wetter-, Wellen- und Klimadaten</td><td>IP-Adresse, Koordinaten des gewählten Gebiets bzw. Standorts</td></tr>'
             '<tr><td>Deutscher Wetterdienst (DWD), Bright Sky, SMHI</td><td>Warnungen, Regenradar</td><td>IP-Adresse, ggf. Koordinaten</td></tr>'
             '<tr><td>Google Übersetzer bzw. MyMemory</td><td>Übersetzung fremdsprachiger Warntexte, nur wenn die Funktion genutzt wird</td><td>IP-Adresse, der zu übersetzende Warntext</td></tr>'
             '</table></div><p>Zum Teil sitzen die Anbieter in den USA oder anderen Drittländern. Die Übermittlung stützt sich, soweit '
             'einschlägig, auf den Angemessenheitsbeschluss der EU-Kommission (EU-US Data Privacy Framework) oder auf Standardvertragsklauseln. '
             'Im Offline-Modus werden Schriften, Bibliothek, Kartenkacheln und Wetterdaten, die schon einmal geladen wurden, vom Gerät '
             'verwendet, dann findet dafür kein Abruf statt.</p>')
    b.append('<p><b>Externe Links:</b> Die Seite enthält Links zu externen Angeboten (z. B. Wetterdienste, Behörden und die Spendenseite PayPal). '
             'Erst wenn du einen Link anklickst, werden Daten an den jeweiligen Anbieter übertragen. Für diese Seiten gelten deren eigene Datenschutzhinweise.</p>')

    b.append('<h2>7. Empfänger</h2><p>Daten werden nur weitergegeben, soweit das für den Betrieb nötig ist (Hosting-Anbieter, oben genannte '
             'Dienste) oder du es selbst veranlasst (geteilte Törns). Eine Weitergabe zu Werbezwecken oder Datenverkauf findet nicht statt.</p>')
    b.append('<h2>8. Speicherdauer</h2><p>Key-, Geräte- und IP-Daten bleiben gespeichert, solange der Key besteht, und werden mit dem Löschen des '
             'Keys entfernt. Das Zugriffsprotokoll wird %s aufbewahrt. Logbuch-Daten bleiben bis zum Löschen durch dich oder bis zum '
             'Löschen des Keys bestehen. Lokal gespeicherte Daten liegen auf deinem Gerät, bis du sie löschst. Gesetzliche '
             'Aufbewahrungspflichten bleiben unberührt.</p>' % M(v, 'log_dauer'))

    b.append('<h2>9. Deine Rechte</h2><p>Du hast nach der DSGVO das Recht auf Auskunft (Art. 15), Berichtigung (Art. 16), Löschung (Art. 17), '
             'Einschränkung der Verarbeitung (Art. 18), Datenübertragbarkeit (Art. 20) und Widerspruch gegen Verarbeitungen auf Grundlage von '
             'Art. 6 Abs. 1 lit. f DSGVO (Art. 21). Wende dich dazu an %s. Da Keys nur als Hash gespeichert sind, brauche ich zur Zuordnung '
             'deiner Daten Angaben wie den Namen des Keys und den ungefähren Nutzungszeitraum.</p>' % mail(v))
    auf = (v.get('aufsicht') or '').strip()
    b.append('<h2>10. Beschwerderecht</h2><p>Du kannst dich bei einer Datenschutz-Aufsichtsbehörde beschweren, wenn du der Ansicht bist, dass '
             'die Verarbeitung deiner Daten gegen das Datenschutzrecht verstößt.%s</p>' %
             ((' Zuständig für den Verantwortlichen ist:<br>' + E(auf).replace('\n', '<br>')) if auf else
              ' Das kann die Behörde an deinem Wohnort, an deinem Arbeitsplatz oder am Sitz des Verantwortlichen sein.'))
    b.append('<h2>11. Sicherheit</h2><p>Die Verbindung ist per TLS (HTTPS) verschlüsselt. Keys werden nur als Hash gespeichert. Trotzdem kann '
             'kein System absolute Sicherheit garantieren.</p>')
    b.append('<h2>12. Änderungen</h2><p>Wenn sich der Dienst oder die Rechtslage ändert, wird diese Erklärung angepasst. Es gilt die jeweils hier abrufbare Fassung.</p>')
    if (v.get('datenschutz_zusatz') or '').strip(): b.append('<h2>13. Weitere Hinweise</h2><p>%s</p>' % M(v, 'datenschutz_zusatz'))
    b.append('<p style="margin-top:22px">Siehe auch <a href="impressum.html">Impressum</a> und <a href="haftungsausschluss.html">Haftungsausschluss</a>.</p>')
    return shell('datenschutz', ''.join(b))


RENDER = {'impressum': impressum, 'datenschutz': datenschutz, 'haftungsausschluss': haftung}


def render(slug, data_dir, geo=True):
    return RENDER[slug](load(data_dir), geo)


# Fusszeile mit Links, wird in jede geschuetzte Seite eingefuegt (und steht auch auf dem Login-Bild)
FOOTER = ('<footer class="gg-legal" style="max-width:1080px;margin:28px auto 0;padding:14px 16px calc(14px + env(safe-area-inset-bottom,0px));'
          'text-align:center;font:13px/1.6 system-ui,-apple-system,sans-serif;color:var(--muted,#55696f)">'
          '<a href="impressum.html" style="color:inherit;margin:0 8px">Impressum</a>·'
          '<a href="datenschutz.html" style="color:inherit;margin:0 8px">Datenschutz</a>·'
          '<a href="haftungsausschluss.html" style="color:inherit;margin:0 8px">Haftungsausschluss</a></footer>'
          '<style>@media print{.gg-legal{display:none!important}}</style>')
