/** * Die Datenschutzerklärung, wie die Anwendung sie zeigt. * * ## Warum sie überhaupt in der Anwendung steht * * Bis Fassung 0.24.2 war sie **nirgends** erreichbar. Das Wort * „Datenschutzerklärung“ kam in der ganzen Oberfläche kein einziges Mal als * sichtbarer Text vor; `docs/datenschutz.html` wurde nicht mitgeliefert * (`electron-builder.yml` führte kein `docs/`), und der einzige Weg nach * außen führt zur Unterstützungsseite. Zugleich sagt „Über diese Software“ * zu, die Anwendung übertrage keine Daten — die weitreichendste Zusage nach * der Offline-Zusage, und ihr Beleg war für den Nutzenden unerreichbar. * * ## Warum ein Blockmodell und kein HTML * * Dieselbe Überlegung wie bei {@link Hilfeblock} in `hilfe.ts`: In den * Zeichenketten steht kein Markup, und nichts davon wird über * `dangerouslySetInnerHTML` gerendert. Der Renderer kennt * `dangerouslySetInnerHTML` an keiner Stelle, und das soll so bleiben — die * Inhaltssicherheitsrichtlinie (`shared/csp.ts`) verbietet ohnehin fremde * Skripte, aber ein Blockmodell braucht die Frage gar nicht erst zu stellen. * * ## Warum erzeugt und nicht geschrieben * * `content/datenschutz.json` entsteht aus `docs/datenschutz.md` über * `tools/datenschutz_anwendung.py`, und zwar über **denselben** Umsetzer, * der auch die veröffentlichte HTML-Fassung erzeugt. Es gibt genau einen * Parser für dieses Dokument. * * Der Grund ist nicht Bequemlichkeit: Die veröffentlichte Seite stand am * 30.08.2026 auf Fassung 1.1, während die Quelldatei bei 1.5 war — vier * Revisionen Abstand in vier Tagen, bei vorhandenem Erzeuger und vorhandener * Prüfliste. Ausgelaufen ist der Schritt, den nur ein Mensch tut. Deshalb * trägt die erzeugte Datei die Prüfsumme ihrer Quelle, und * `tests/datenschutz.test.ts` rechnet sie nach. */ /** Ein ausgezeichneter Teil innerhalb eines Absatzes oder Listenpunkts. */ export type Datenschutzteil = | { readonly art: 'text'; readonly text: string } /** Hervorhebung – im Markdown `**fett**`. */ | { readonly art: 'stark'; readonly text: string } /** Datei-, Feld- oder Spaltenname – im Markdown `` `so` ``. */ | { readonly art: 'kennzeichnung'; readonly text: string } /** Verweis mit Ziel; die Anwendung zeigt ihn als Text samt Adresse. */ | { readonly art: 'verweis'; readonly text: string; readonly ziel: string }; /** * Ein Baustein der Erklärung. * * Genau die fünf Formen, die der Erzeuger hervorbringt. Nachgezählt am * Dokument vom 01.09.2026: 94 Absätze, 34 Überschriften, 9 Listen, * 6 Codeblöcke und 2 Tabellen. Was hier nicht vorgesehen ist, lässt den Erzeuger abbrechen, * statt es stillschweigend wegzulassen. */ export type Datenschutzblock = | { readonly art: 'ueberschrift'; readonly ebene: 1 | 2 | 3; readonly text: string; /** Sprungziel, gebildet wie in der HTML-Fassung. */ readonly kennung: string; } | { readonly art: 'absatz'; readonly teile: readonly Datenschutzteil[] } | { readonly art: 'liste'; readonly punkte: readonly (readonly Datenschutzteil[])[] } | { readonly art: 'code'; readonly text: string } | { readonly art: 'tabelle'; /** * Die Spaltenüberschriften – **leer**, wenn die Tabelle keine hat. * * Die Standtabelle am Ende der Erklärung ist so eine: Sie ist in * Wahrheit eine Aufzählung von Merkmal und Wert. Die Anzeige macht * daraus Zeilenköpfe statt leerer Spaltenköpfe – eine Kopfzelle ohne * Text ist für einen Bildschirmleser eine Ansage ins Nichts. */ readonly kopf: readonly string[]; readonly zeilen: readonly (readonly (readonly Datenschutzteil[])[])[]; }; export interface Datenschutzerklaerung { /** SHA-256 des Veröffentlichungsteils von `docs/datenschutz.md`. */ readonly quellpruefsumme: string; /** Fassung der Erklärung, aus ihrer Standtabelle gelesen. */ readonly fassung: string; /** Stand der Erklärung, aus ihrer Standtabelle gelesen. */ readonly stand: string; readonly bloecke: readonly Datenschutzblock[]; } export interface Datenschutzangaben { /** Die Erklärung; `null`, wenn die Datei fehlt oder unlesbar ist. */ readonly erklaerung: Datenschutzerklaerung | null; /** * Warum sie fehlt – für die Anzeige, nicht fürs Protokoll. * * Ein leerer Bereich ohne Begründung wäre die schlechteste Auskunft: Der * Nutzende hielte die Erklärung für nicht vorhanden statt für nicht * ladbar. */ readonly warnung: string | null; } /** Ohne Datei: die Anwendung läuft weiter und sagt, was fehlt. */ export const DATENSCHUTZ_LEER: Datenschutzangaben = Object.freeze({ erklaerung: null, warnung: null, }); /** * Die Überschriften, die ein Sprungmenü anbietet. * * Nur die zweite Ebene: Die Erklärung hat 13 Abschnitte und 20 * Unterabschnitte; ein Verzeichnis über alle 33 wäre selbst wieder eine * Wand aus Text. Die erste Ebene ist der Titel und braucht kein Sprungziel. */ export function abschnitte( erklaerung: Datenschutzerklaerung, ): readonly { readonly text: string; readonly kennung: string }[] { return erklaerung.bloecke .filter( (block): block is Extract => block.art === 'ueberschrift' && block.ebene === 2, ) .map((block) => ({ text: block.text, kennung: block.kennung })); }