Ergänzung Datenschutzerklärung und Impressum, Fristenformular in Einstellungen

This commit is contained in:
2026-08-10 06:44:11 +02:00
parent 8ffbda875e
commit 777e3e5447
25 changed files with 1310 additions and 139 deletions

View File

@@ -1465,3 +1465,94 @@ button.archive-head .chev { color: var(--muted); }
.reg-toggle .checkline { margin-top: 0; }
@media print { ul.suggestions, .card.archive { display: none !important; } }
/* ===================================================================
Fußzeile mit Impressum und Datenschutz
=================================================================== */
.app-footer {
max-width: 30rem;
margin: 0 auto;
padding: 1.5rem 1rem calc(1.5rem + env(safe-area-inset-bottom));
text-align: center;
font-size: .82rem;
color: var(--muted);
}
.app-footer a { color: var(--muted); }
.app-footer span { margin: 0 .4rem; }
/* Über dem Sucher und über Dialogen darf sie nicht durchscheinen. */
body.scanning .app-footer,
body.modal-open .app-footer { display: none; }
/* ===================================================================
Rechtliche Seiten
=================================================================== */
.legal-text { line-height: 1.6; }
.legal-text h1 { margin-bottom: .2rem; }
.legal-text h2 {
margin: 2rem 0 .5rem;
font-size: 1.05rem;
padding-bottom: .3rem;
border-bottom: 1px solid var(--line);
}
.legal-text h3 { margin: 1.4rem 0 .4rem; font-size: .95rem; }
.legal-text p { margin: .6rem 0; }
.legal-text ul { margin: .6rem 0; padding-left: 1.2rem; }
.legal-text li { margin: .25rem 0; }
.legal-text table {
width: 100%;
border-collapse: collapse;
margin: .8rem 0;
font-size: .9rem;
}
.legal-text th,
.legal-text td {
text-align: left;
vertical-align: top;
padding: .45rem .5rem .45rem 0;
border-bottom: 1px solid var(--line);
}
.legal-text th { font-size: .8rem; text-transform: uppercase; color: var(--muted); }
/* Hinweis, dass es sich um einen Entwurf handelt - soll nach dem
Ausfüllen entfernt werden. */
.legal-text .draft {
background: var(--warn-bg);
border: 1px solid var(--warn-line);
border-radius: var(--radius);
padding: .8rem 1rem;
margin: 0 0 1.5rem;
font-size: .9rem;
}
/* Erläuterungen zur Rechtslage - gehören nicht in den fertigen Text. */
.legal-text .hint {
font-size: .85rem;
color: var(--muted);
border-left: 2px solid var(--line);
padding-left: .8rem;
margin: .5rem 0 1rem;
}
.legal-text code {
background: rgba(127, 127, 127, .18);
padding: .1em .35em;
border-radius: 3px;
font-size: .9em;
}
@media print {
/* Im Ausdruck haben Rechtslinks nichts zu suchen - er soll die
Einkaufsliste zeigen, nicht die Fußzeile der Anwendung. */
.app-footer { display: none !important; }
}

316
web/html/datenschutz.html Normal file
View File

@@ -0,0 +1,316 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light dark">
<title>Datenschutzerklärung</title>
<link rel="stylesheet" href="/app.css">
<link rel="icon" href="/icons/favicon.ico" sizes="any">
</head>
<body class="legal">
<main id="app">
<header class="app-bar">
<a class="bar-back" href="/"> Zurück zur App</a>
</header>
<article class="card legal-text">
<p class="draft">
<strong>Entwurf.</strong> Dieser Text beschreibt die Verarbeitungen, die
diese Anwendung technisch vornimmt vollständig und nach bestem Wissen,
aber ohne Rechtsberatung. Vor der Veröffentlichung sollte er von jemandem
geprüft werden, der dazu befugt ist. Alle mit <code>[…]</code> markierten
Stellen müssen ausgefüllt werden.
</p>
<h1>Datenschutzerklärung</h1>
<p class="lead">Stand: [Datum]</p>
<h2>1. Verantwortlicher</h2>
<p>
Verantwortlich für die Verarbeitung personenbezogener Daten im Sinne von
Art. 4 Nr. 7 DSGVO ist:
</p>
<p>
[Vorname Nachname]<br>
[Straße und Hausnummer]<br>
[PLZ Ort]<br>
E-Mail: [E-Mail-Adresse]<br>
Telefon: [Telefonnummer]
</p>
<p>
[Falls benannt:] Datenschutzbeauftragte/r: [Name, Kontakt]. Ein
Datenschutzbeauftragter ist nur unter den Voraussetzungen des Art. 37 DSGVO
und § 38 BDSG zu benennen; für eine privat oder in kleinem Rahmen betriebene
Anwendung ist das regelmäßig nicht der Fall.
</p>
<h2>2. Grundsatz</h2>
<p>
Diese Anwendung ist auf Datensparsamkeit ausgelegt. Es findet keine
Reichweitenmessung, kein Tracking, keine Profilbildung und keine Werbung
statt. Es werden keine Inhalte von fremden Servern nachgeladen weder
Schriftarten noch Skripte, Karten oder Symbole. Alle Darstellungsmittel
liegen auf demselben Server wie die Anwendung; für die Schrift wird die
auf dem Gerät bereits vorhandene Systemschrift verwendet.
</p>
<h2>3. Welche Daten verarbeitet werden</h2>
<h3>3.1 Benutzerkonto</h3>
<table>
<tr><th>Angabe</th><th>Pflicht</th><th>Zweck</th></tr>
<tr><td>E-Mail-Adresse</td><td>ja</td><td>Anmeldung, Bestätigung, Einladungen, Passwort zurücksetzen</td></tr>
<tr><td>Passwort</td><td>ja</td><td>Wird ausschließlich als Streuwert (Argon2id) gespeichert, nie im Klartext</td></tr>
<tr><td>Anzeigename</td><td>nein</td><td>Darstellung gegenüber anderen Mitgliedern; ein Pseudonym genügt</td></tr>
<tr><td>Zeitpunkt der Registrierung und der letzten Nutzung</td><td>automatisch</td><td>Bestätigung des Kontos, automatische Bereinigung ungenutzter Konten</td></tr>
</table>
<p>
<strong>Rechtsgrundlage:</strong> Art. 6 Abs. 1 lit. b DSGVO die
Verarbeitung ist zur Bereitstellung der Anwendung erforderlich.
</p>
<h3>3.2 Inhalte, die Sie selbst eingeben</h3>
<p>
Einkaufslisten, Artikelbezeichnungen, Mengen, Eigenschaften, Notizen,
Märkte, Warengruppen und Preise. Bei geteilten Listen ist für die übrigen
Mitglieder erkennbar, wer einen Eintrag angelegt hat.
</p>
<p>
<strong>Bitte beachten:</strong> Welche Angaben in einer Liste stehen,
bestimmen ausschließlich Sie. Tragen Sie dort Hinweise auf Gesundheit,
religiöse Überzeugungen oder Ähnliches ein etwa Medikamente oder
Speisevorschriften , verarbeiten Sie besondere Kategorien
personenbezogener Daten im Sinne des Art. 9 DSGVO. Der Betreiber hat darauf
keinen Einfluss und rät davon ab, solche Angaben aufzunehmen.
</p>
<p><strong>Rechtsgrundlage:</strong> Art. 6 Abs. 1 lit. b DSGVO.</p>
<h3>3.3 Einladungen</h3>
<p>
Wenn Sie eine Liste mit einer anderen Person teilen, geben Sie deren
E-Mail-Adresse an. Diese wird gespeichert, um die Einladung zuzustellen und
ihren Stand nachvollziehbar zu machen (versendet, angenommen, widerrufen).
Nicht angenommene Einladungen laufen nach [14] Tagen ab; der Datensatz
wird [30] Tage <em>nach Ablauf</em> gelöscht (zusammen also [44] Tage).
</p>
<p>
<strong>Rechtsgrundlage:</strong> Art. 6 Abs. 1 lit. f DSGVO berechtigtes
Interesse an der gemeinsamen Nutzung. Die eingeladene Person wird mit der
Einladung über die Verarbeitung informiert.
</p>
<h3>3.4 Sitzungen und Cookies</h3>
<p>Es werden ausschließlich zwei technisch notwendige Cookies gesetzt:</p>
<table>
<tr><th>Name</th><th>Zweck</th><th>Dauer</th></tr>
<tr><td><code>ea_session</code></td><td>Erkennung der angemeldeten Sitzung; <code>HttpOnly</code>, für Skripte unlesbar</td><td>[30] Tage</td></tr>
<tr><td><code>ea_csrf</code></td><td>Schutz vor seitenübergreifend untergeschobenen Anfragen</td><td>[30] Tage</td></tr>
</table>
<p>
Beide sind zur Erbringung des ausdrücklich gewünschten Dienstes unbedingt
erforderlich; eine Einwilligung ist nach § 25 Abs. 2 Nr. 2 TDDDG nicht
erforderlich. Es werden keine Cookies zu Analyse- oder Werbezwecken gesetzt.
</p>
<h3>3.5 Speicherung auf Ihrem Gerät</h3>
<p>
Damit die Anwendung ohne Verbindung nutzbar bleibt, legt sie den zuletzt
bekannten Stand Ihrer Listen sowie noch nicht übertragene Änderungen im
lokalen Speicher des Browsers (IndexedDB) ab. Diese Daten verlassen Ihr
Gerät nur, um sie mit dem Server abzugleichen, und lassen sich über die
Browsereinstellungen jederzeit löschen.
</p>
<h3>3.6 Protokolle und Missbrauchsabwehr</h3>
<p>
Der Webserver protokolliert jeden Abruf mit <strong>gekürzter</strong>
IP-Adresse (letzter Block auf Null gesetzt), Zeitpunkt, angefordertem Pfad,
Statuscode und Browserkennung. Aufrufe von Einladungs-, Freigabe- und
Passwortlinks werden gar nicht protokolliert, da deren Adresse den
Zugangsschlüssel enthält. Aufbewahrung: [7] Tage.
</p>
<p>
Zur Abwehr von Anmeldeversuchen in großer Zahl werden Zähler geführt. Als
Schlüssel dient ein Streuwert aus IP- beziehungsweise E-Mail-Adresse und
einem zufälligen, installationsweiten Zusatz; die Adressen selbst werden
dafür nicht gespeichert. Die Zähler werden spätestens nach 24 Stunden
gelöscht.
</p>
<p><strong>Rechtsgrundlage:</strong> Art. 6 Abs. 1 lit. f DSGVO berechtigtes
Interesse am sicheren Betrieb.</p>
<h3>3.7 Öffentlich geteilte Listen</h3>
<p>
Sie können eine Liste über einen Link freigeben. Wer den Link hat, kann die
Liste ansehen und sofern beim Erzeugen erlaubt Artikel abhaken. In
dieser Ansicht wird <strong>nicht</strong> angezeigt, wer welchen Eintrag
angelegt hat. Der Link hat stets ein Ablaufdatum und ist jederzeit
widerrufbar. Erfasst wird lediglich, wie oft und wann zuletzt er geöffnet
wurde ohne IP-Adresse und ohne Zeitreihe.
</p>
<h3>3.8 Push-Benachrichtigungen</h3>
<p>
Nur wenn Sie sie ausdrücklich einschalten. Dabei wird eine vom Push-Dienst
Ihres Browserherstellers vergebene Adresse gespeichert, zusammen mit dem
Schlüsselmaterial zur Verschlüsselung und einer groben Gerätebezeichnung
(z. B. „Chrome auf Android"). Der Inhalt der Nachricht ist
Ende-zu-Ende-verschlüsselt und kann vom Push-Dienst nicht gelesen werden.
Übermittelt wird lediglich der Name der geänderten Liste und wer sie
geändert hat keine Artikelbezeichnungen.
</p>
<p>
<strong>Empfänger:</strong> Je nach Browser <em>Google LLC</em> (Chrome,
Android), <em>Apple Inc.</em> (Safari) oder die <em>Mozilla Foundation</em>
(Firefox). Dabei können Daten in die USA übermittelt werden; die Anbieter
stützen sich auf den EU-US Data Privacy Framework beziehungsweise auf
Standardvertragsklauseln nach Art. 46 DSGVO.
</p>
<p>
<strong>Rechtsgrundlage:</strong> Art. 6 Abs. 1 lit. a DSGVO Einwilligung.
Sie können sie jederzeit unter „Einstellungen → Benachrichtigungen"
widerrufen; die Anmeldung wird dann gelöscht.
</p>
<h2>4. Externe Dienste</h2>
<h3>4.1 Hosting</h3>
<p>
Die Anwendung wird betrieben bei:
</p>
<p>
[Name des Anbieters]<br>
[Anschrift]<br>
[Land]
</p>
<p>
Der Anbieter verarbeitet die Daten ausschließlich weisungsgebunden. Es
besteht ein Vertrag zur Auftragsverarbeitung nach Art. 28 DSGVO.
[Falls der Serverstandort abweicht, hier angeben.]
</p>
<h3>4.2 E-Mail-Versand</h3>
<p>
Für Bestätigungen, Einladungen und Hinweise wird ein SMTP-Server genutzt:
[Name und Anschrift des Anbieters bei einem selbst betriebenen Server:
„Der Versand erfolgt über einen eigenen Server des Verantwortlichen; ein
Dritter ist nicht beteiligt."]. Übermittelt werden Empfängeradresse,
Betreff und Inhalt der Nachricht.
</p>
<h3>4.3 Produktdatenbank (Strichcodes)</h3>
<p>
Beim Scannen eines Strichcodes wird die Ziffernfolge bei
<em>Open Food Facts</em> nachgeschlagen, um Bezeichnung und Packungsgröße
vorzuschlagen. Betreiber: Open Food Facts, 21 rue des Iles, 94100
Saint-Maur-des-Fossés, Frankreich.
</p>
<p>
Die Abfrage erfolgt <strong>durch den Server dieser Anwendung</strong>,
nicht durch Ihr Gerät. Open Food Facts erfährt dadurch weder Ihre
IP-Adresse noch, wer den Code gescannt hat. Übermittelt wird ausschließlich
die Ziffernfolge. Ergebnisse werden zwischengespeichert, sodass derselbe
Code höchstens einmal je Gültigkeitszeitraum abgefragt wird.
[Falls abgeschaltet (<code>PRODUCT_LOOKUP=off</code>), diesen Abschnitt
streichen.]
</p>
<h3>4.4 Keine weiteren Dienste</h3>
<p>
Es werden keine Analysedienste, keine Werbenetzwerke, keine sozialen
Netzwerke, keine Kartendienste und keine externen Schriftarten eingebunden.
</p>
<h2>5. Empfänger innerhalb der Anwendung</h2>
<p>
Wenn Sie eine Liste teilen, sehen die eingeladenen Personen deren Inhalt,
Ihren Anzeigenamen beziehungsweise Ihre E-Mail-Adresse sowie welche
Einträge von Ihnen stammen. Der Eigentümer einer Liste sieht die
E-Mail-Adressen aller Mitglieder. Personen mit Administratorrechten haben
zur Verwaltung Zugriff auf die Liste aller Konten samt E-Mail-Adressen und
Nutzungszeitpunkten; Listeninhalte sehen sie nur, soweit sie selbst
Mitglied sind.
</p>
<h2>6. Speicherdauer</h2>
<table>
<tr><th>Daten</th><th>Dauer</th></tr>
<tr><td>Konto und Inhalte</td><td>Bis zur Löschung des Kontos</td></tr>
<tr><td>Konten ohne Anmeldung</td><td>Deaktivierung nach [12] Monaten, Löschung [12] Monate danach</td></tr>
<tr><td>Listen ohne Nutzung</td><td>Archivierung nach [12] Monaten, Löschung [12] Monate danach; der Eigentümer wird 7 Tage und 1 Tag vorher benachrichtigt</td></tr>
<tr><td>Gelöschte Einträge und Listen</td><td>Endgültige Entfernung nach [30] Tagen die Frist stellt sicher, dass auch länger offline gebliebene Geräte vom Löschen erfahren</td></tr>
<tr><td>Sitzungen</td><td>[30] Tage, bei Abmeldung sofort</td></tr>
<tr><td>Bestätigungs- und Einladungslinks</td><td>[1] Stunde (Passwort) bis [14] Tage (Einladung, Willkommensgruß); Adresswechsel [48] Stunden; Reste [30] Tage nach Ablauf entfernt</td></tr>
<tr><td>Zähler der Missbrauchsabwehr</td><td>24 Stunden</td></tr>
<tr><td>Zugriffsprotokolle</td><td>[7] Tage</td></tr>
<tr><td>Erfasste Preise</td><td>Unbefristet sie enthalten keinen Personenbezug (nur Markt, Artikel, Betrag, Zeitpunkt)</td></tr>
<tr><td>Zwischenspeicher der Produktdatenbank</td><td>[180] Tage, danach erneute Abfrage</td></tr>
</table>
<p>
Beim Löschen eines Kontos gehen Listen, die mit anderen geteilt sind, an
das dienstälteste andere Mitglied über; Listen ohne weitere Mitglieder
werden gelöscht. Bei Einträgen anderer Personen wird der Hinweis auf die
Urheberschaft entfernt, der Eintrag selbst bleibt bestehen.
</p>
<h2>7. Ihre Rechte</h2>
<p>Sie haben nach der DSGVO das Recht auf:</p>
<ul>
<li>Auskunft über die zu Ihnen gespeicherten Daten (Art. 15)</li>
<li>Berichtigung unrichtiger Daten (Art. 16)</li>
<li>Löschung (Art. 17)</li>
<li>Einschränkung der Verarbeitung (Art. 18)</li>
<li>Datenübertragbarkeit (Art. 20)</li>
<li>Widerspruch gegen Verarbeitungen auf Grundlage berechtigter Interessen (Art. 21)</li>
<li>Widerruf erteilter Einwilligungen mit Wirkung für die Zukunft (Art. 7 Abs. 3)</li>
</ul>
<p>
Wenden Sie sich dafür an die oben genannte Adresse. Anzeigename und
Passwort können Sie selbst unter „Einstellungen" ändern; für die Änderung
der E-Mail-Adresse und die Löschung des Kontos wenden Sie sich an die
Administration.
</p>
<p>
Unabhängig davon steht Ihnen ein Beschwerderecht bei einer
Datenschutz-Aufsichtsbehörde zu (Art. 77 DSGVO), etwa bei der für
[Bundesland] zuständigen Behörde: [Name und Anschrift der Behörde].
</p>
<h2>8. Erforderlichkeit der Angaben</h2>
<p>
Die Angabe der E-Mail-Adresse und eines Passworts ist zur Nutzung
erforderlich; ohne sie kann kein Konto geführt werden. Alle übrigen
Angaben sind freiwillig. Eine automatisierte Entscheidungsfindung
einschließlich Profilbildung nach Art. 22 DSGVO findet nicht statt.
</p>
<h2>9. Überprüfbarkeit der Fristen</h2>
<p>
Sämtliche in dieser Erklärung genannten Fristen sind in der Anwendung
hinterlegt und lassen sich vom Betreiber unter „Einstellungen →
Benutzerverwaltung → Fristen" einsehen und ändern. Ein Aufräumlauf setzt sie
einmal täglich um. Weichen die hier genannten Zahlen von den dort
eingestellten ab, gelten die eingestellten bitte melden Sie eine solche
Abweichung, damit dieser Text nachgezogen wird.
</p>
<p class="hint">
Hinweis für den Betreiber: Nach jeder Änderung an den Fristen sind die mit
<code>[…]</code> markierten Zahlen in dieser Erklärung anzupassen. Die
Aufstellung unter „Fristen" zeigt die geltenden Werte.
</p>
<h2>10. Änderungen</h2>
<p>
Diese Erklärung wird angepasst, wenn sich die Anwendung ändert. Maßgeblich
ist die hier veröffentlichte Fassung; das Datum steht oben.
</p>
<p class="switch"><a href="/impressum.html">Impressum</a> · <a href="/">Zurück zur App</a></p>
</article>
</main>
</body>
</html>

130
web/html/impressum.html Normal file
View File

@@ -0,0 +1,130 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light dark">
<title>Impressum</title>
<link rel="stylesheet" href="/app.css">
<link rel="icon" href="/icons/favicon.ico" sizes="any">
</head>
<body class="legal">
<main id="app">
<header class="app-bar">
<a class="bar-back" href="/"> Zurück zur App</a>
</header>
<article class="card legal-text">
<p class="draft">
<strong>Entwurf.</strong> Alle mit <code>[…]</code> markierten Stellen
müssen ausgefüllt werden; nicht zutreffende Abschnitte sind zu streichen.
Dies ist keine Rechtsberatung lassen Sie den Text vor der
Veröffentlichung von jemandem prüfen, der dazu befugt ist.
</p>
<h1>Impressum</h1>
<h2>Angaben gemäß § 5 DDG</h2>
<p>
[Vorname Nachname]<br>
[ggf. Firmierung, z. B. „Muster GmbH"]<br>
[Straße und Hausnummer]<br>
[PLZ Ort]<br>
[Land, falls nicht Deutschland]
</p>
<h2>Kontakt</h2>
<p>
Telefon: [Telefonnummer]<br>
E-Mail: [E-Mail-Adresse]
</p>
<p class="hint">
Eine E-Mail-Adresse und eine zweite Möglichkeit zur unmittelbaren
Kommunikation üblicherweise eine Telefonnummer sind nach § 5 Abs. 1
Nr. 2 DDG verpflichtend. Ein bloßes Kontaktformular genügt nicht.
</p>
<h2>Vertretungsberechtigt</h2>
<p>
[Nur bei juristischen Personen: Name der vertretungsberechtigten Person,
z. B. Geschäftsführer. Bei Einzelpersonen streichen.]
</p>
<h2>Registereintrag</h2>
<p>
[Nur falls vorhanden:]<br>
Registergericht: [Amtsgericht]<br>
Registernummer: [HRB …]
</p>
<h2>Umsatzsteuer-Identifikationsnummer</h2>
<p>
[Nur falls vorhanden:] Umsatzsteuer-Identifikationsnummer gemäß § 27 a
Umsatzsteuergesetz: [DE…]
</p>
<h2>Aufsichtsbehörde</h2>
<p>
[Nur bei zulassungspflichtigen Tätigkeiten:] [Name und Anschrift der
zuständigen Aufsichtsbehörde]
</p>
<h2>Verantwortlich für den Inhalt</h2>
<p>
[Vorname Nachname]<br>
[Anschrift wie oben]
</p>
<p class="hint">
Die frühere Angabe „nach § 55 Abs. 2 RStV" ist überholt; der
Rundfunkstaatsvertrag wurde 2020 durch den Medienstaatsvertrag abgelöst.
Eine gesonderte Angabe nach § 18 Abs. 2 MStV ist nur bei journalistisch
redaktionell gestalteten Angeboten erforderlich für eine Einkaufsliste
regelmäßig nicht. Bei Zweifeln kann die Angabe stehen bleiben; sie schadet
nicht.
</p>
<h2>Streitbeilegung</h2>
<p>
[Nur bei Angeboten gegenüber Verbrauchern im geschäftlichen Verkehr:]
Wir sind weder bereit noch verpflichtet, an Streitbeilegungsverfahren vor
einer Verbraucherschlichtungsstelle teilzunehmen.
</p>
<p class="hint">
Die Plattform der EU-Kommission zur Online-Streitbeilegung wurde zum
20. Juli 2025 eingestellt; ein Hinweis darauf ist nicht mehr erforderlich
und sollte aus älteren Impressen entfernt werden. Wird die Anwendung rein
privat und ohne Entgelt betrieben, entfällt dieser Abschnitt ganz.
</p>
<h2>Haftung für Inhalte</h2>
<p>
Die Inhalte der Einkaufslisten werden ausschließlich von den Nutzerinnen
und Nutzern erstellt. Der Betreiber überwacht sie nicht und ist nach
§ 7 Abs. 2 DDG nicht verpflichtet, übermittelte oder gespeicherte fremde
Informationen zu überwachen. Verpflichtungen zur Entfernung oder Sperrung
bei Kenntnis einer konkreten Rechtsverletzung bleiben unberührt; ab
Kenntnis wird der entsprechende Inhalt umgehend entfernt.
</p>
<h2>Haftung für Links</h2>
<p>
Diese Anwendung verweist nicht auf fremde Seiten. Sollten Nutzerinnen und
Nutzer Verweise in Listeneinträge aufnehmen, hat der Betreiber darauf keinen
Einfluss und übernimmt für deren Inhalt keine Haftung.
</p>
<h2>Urheberrecht</h2>
<p>
Der Quelltext dieser Anwendung steht unter [Lizenz angeben, z. B. „MIT-Lizenz"
oder: „Alle Rechte vorbehalten"]. Produktdaten zu Strichcodes stammen von
Open Food Facts und stehen unter der Open Database License (ODbL).
</p>
<p class="switch"><a href="/datenschutz.html">Datenschutzerklärung</a> · <a href="/">Zurück zur App</a></p>
</article>
</main>
</body>
</html>

View File

@@ -28,6 +28,15 @@
<p class="error">Diese App benötigt JavaScript.</p>
</noscript>
<!-- Außerhalb von #app: Die Ansichten tauschen nur den Inhalt von #app
aus, die Fußzeile bleibt dadurch überall stehen - auch auf der
Anmeldeseite und in der öffentlichen Listenansicht. -->
<footer class="app-footer">
<a href="/impressum.html">Impressum</a>
<span aria-hidden="true">·</span>
<a href="/datenschutz.html">Datenschutz</a>
</footer>
<script type="module" src="/js/app.js"></script>
</body>
</html>

View File

@@ -296,57 +296,66 @@ export async function adminView(root, { back }) {
// Automatik
// -------------------------------------------------------------------
function automationCard() {
const deactivate = el("input.months", {
type: "number", min: "0", max: "600",
value: String(config.auto_deactivate_months),
});
const remove = el("input.months", {
type: "number", min: "0", max: "600",
value: String(config.auto_delete_months),
});
const archive = el("input.months", {
type: "number", min: "0", max: "600",
value: String(config.auto_archive_months),
});
const archiveDelete = el("input.months", {
type: "number", min: "0", max: "600",
value: String(config.archive_delete_months),
});
/** Fristen aus dem Server: Beschriftung, Einheit und Grenzen kommen
* von dort, damit nicht das eine steht und das andere gilt. */
function durationsCard() {
const fields = new Map();
const group = (title, keys, lead) => {
const rows = config.durations.filter((d) => keys.includes(d.key));
if (!rows.length) return null;
return el("div.duration-group", {},
el("h3.device-heading", {}, title),
lead ? el("p.sub", {}, lead) : null,
rows.map((entry) => {
const input = el("input.months", {
type: "number",
min: String(entry.minimum),
max: String(entry.maximum),
value: String(entry.value),
title: `${entry.minimum} bis ${entry.maximum} ${entry.unit}`,
});
fields.set(entry.key, input);
return el("div.duration-row", {},
el("div.duration-text", {},
el("span.duration-label", {}, `${entry.label}${entry.unit}`),
el("span.duration-note", {},
entry.note,
entry.zero_means ? ` · 0 = ${entry.zero_means}` : "",
entry.value !== entry.default
? ` · Vorgabe ${entry.default}` : "")),
input);
}));
};
return el("section.card", {},
el("h2", {}, "Automatische Bereinigung"),
el("h2", {}, "Fristen"),
el("p.lead", {},
"Läuft einmal täglich. ",
el("strong", {}, "0 bedeutet abgeschaltet"),
", nicht „sofort“. Administratorkonten sind ausgenommen."),
"Diese Werte gelten zur Laufzeit und werden auch für die " +
"Datenschutzerklärung gebraucht. Der Aufräumlauf setzt sie " +
"einmal täglich um."),
el("label", {}, "Deaktivieren nach … Monaten ohne Anmeldung"),
deactivate,
el("label", {}, "Löschen nach … Monaten Deaktivierung"),
remove,
el("h3.device-heading", {}, "Listen"),
el("label", {}, "Archivieren nach … Monaten ohne Öffnen"),
archive,
el("p.sub", {},
"Gilt je Person: Die Liste verschwindet nur aus der Übersicht " +
"derjenigen, die sie nicht geöffnet hat."),
el("label", {}, "Löschen nach … Monaten im Archiv"),
archiveDelete,
el("p.sub", {},
"Maßgeblich ist allein das Archivdatum des Eigentümers. Er wird " +
"7 Tage und 1 Tag vorher per E-Mail gewarnt."),
group("Konten",
["auto_deactivate_months", "auto_delete_months"]),
group("Listen",
["auto_archive_months", "archive_delete_months"]),
group("Aufbewahrung",
["cleanup_deleted_days", "cleanup_ops_days", "product_cache_days"]),
group("Sitzungen und Links",
["session_days", "invite_days", "welcome_days", "verify_hours",
"reset_hours", "email_change_hours"]),
el("button.primary", {
type: "button",
onclick: () => guarded(async () => {
await put("/api/admin/settings", {
auto_deactivate_months: Number(deactivate.value) || 0,
auto_delete_months: Number(remove.value) || 0,
auto_archive_months: Number(archive.value) || 0,
archive_delete_months: Number(archiveDelete.value) || 0,
});
say("Einstellungen gespeichert.");
const durations = {};
for (const [key, input] of fields) {
durations[key] = Number(input.value) || 0;
}
await put("/api/admin/settings", { durations });
say("Fristen gespeichert.");
}),
}, "Speichern"),
@@ -354,7 +363,19 @@ export async function adminView(root, { back }) {
el("button", {
type: "button",
onclick: () => guarded(() => post("/api/admin/cleanup")),
}, "Jetzt aufräumen"))
}, "Jetzt aufräumen")),
// Was hier nicht einstellbar ist, soll trotzdem sichtbar sein -
// sonst wirkt die Übersicht vollständig, ohne es zu sein.
Object.keys(config.fixed || {}).length
? el("div", {},
el("h3.device-heading", {}, "Fest eingestellt"),
el("ul.fixed-list", {},
Object.entries(config.fixed).map(([label, value]) =>
el("li", {},
el("span.name", {}, label),
el("span.sub", {}, value)))))
: null
);
}
@@ -375,7 +396,7 @@ export async function adminView(root, { back }) {
registrationToggle(),
el("ul.admin-users", {}, users.map(userRow))),
createCard(),
automationCard()
durationsCard()
);
}

View File

@@ -5,12 +5,14 @@
// Bei jeder Änderung an den Dateien unten hochzählen. Der Wert entscheidet,
// wann alte Zwischenspeicher verworfen werden.
const VERSION = "v25";
const VERSION = "v27";
const SHELL_CACHE = `einkaufsapp-shell-${VERSION}`;
const SHELL = [
"/",
"/app.css",
"/impressum.html",
"/datenschutz.html",
"/js/app.js",
"/js/api.js",
"/js/dom.js",