waffensachkunde

Waffensachkunde – Lernsoftware für die Sachkundeprüfung nach § 7 WaffG. Barrierefrei, offline, EUPL-1.2.

/ app src shared datenschutz.ts

5,3 KB Rohdatei
app/src/shared/datenschutz.ts — 126 Zeilen
1 /**
2 * Die Datenschutzerklärung, wie die Anwendung sie zeigt.
3 *
4 * ## Warum sie überhaupt in der Anwendung steht
5 *
6 * Bis Fassung 0.24.2 war sie **nirgends** erreichbar. Das Wort
7 * „Datenschutzerklärung“ kam in der ganzen Oberfläche kein einziges Mal als
8 * sichtbarer Text vor; `docs/datenschutz.html` wurde nicht mitgeliefert
9 * (`electron-builder.yml` führte kein `docs/`), und der einzige Weg nach
10 * außen führt zur Unterstützungsseite. Zugleich sagt „Über diese Software“
11 * zu, die Anwendung übertrage keine Daten — die weitreichendste Zusage nach
12 * der Offline-Zusage, und ihr Beleg war für den Nutzenden unerreichbar.
13 *
14 * ## Warum ein Blockmodell und kein HTML
15 *
16 * Dieselbe Überlegung wie bei {@link Hilfeblock} in `hilfe.ts`: In den
17 * Zeichenketten steht kein Markup, und nichts davon wird über
18 * `dangerouslySetInnerHTML` gerendert. Der Renderer kennt
19 * `dangerouslySetInnerHTML` an keiner Stelle, und das soll so bleiben — die
20 * Inhaltssicherheitsrichtlinie (`shared/csp.ts`) verbietet ohnehin fremde
21 * Skripte, aber ein Blockmodell braucht die Frage gar nicht erst zu stellen.
22 *
23 * ## Warum erzeugt und nicht geschrieben
24 *
25 * `content/datenschutz.json` entsteht aus `docs/datenschutz.md` über
26 * `tools/datenschutz_anwendung.py`, und zwar über **denselben** Umsetzer,
27 * der auch die veröffentlichte HTML-Fassung erzeugt. Es gibt genau einen
28 * Parser für dieses Dokument.
29 *
30 * Der Grund ist nicht Bequemlichkeit: Die veröffentlichte Seite stand am
31 * 30.08.2026 auf Fassung 1.1, während die Quelldatei bei 1.5 war — vier
32 * Revisionen Abstand in vier Tagen, bei vorhandenem Erzeuger und vorhandener
33 * Prüfliste. Ausgelaufen ist der Schritt, den nur ein Mensch tut. Deshalb
34 * trägt die erzeugte Datei die Prüfsumme ihrer Quelle, und
35 * `tests/datenschutz.test.ts` rechnet sie nach.
36 */
37
38 /** Ein ausgezeichneter Teil innerhalb eines Absatzes oder Listenpunkts. */
39 export type Datenschutzteil =
40 | { readonly art: 'text'; readonly text: string }
41 /** Hervorhebung – im Markdown `**fett**`. */
42 | { readonly art: 'stark'; readonly text: string }
43 /** Datei-, Feld- oder Spaltenname – im Markdown `` `so` ``. */
44 | { readonly art: 'kennzeichnung'; readonly text: string }
45 /** Verweis mit Ziel; die Anwendung zeigt ihn als Text samt Adresse. */
46 | { readonly art: 'verweis'; readonly text: string; readonly ziel: string };
47
48 /**
49 * Ein Baustein der Erklärung.
50 *
51 * Genau die fünf Formen, die der Erzeuger hervorbringt. Nachgezählt am
52 * Dokument vom 01.09.2026: 94 Absätze, 34 Überschriften, 9 Listen,
53 * 6 Codeblöcke und 2 Tabellen. Was hier nicht vorgesehen ist, lässt den Erzeuger abbrechen,
54 * statt es stillschweigend wegzulassen.
55 */
56 export type Datenschutzblock =
57 | {
58 readonly art: 'ueberschrift';
59 readonly ebene: 1 | 2 | 3;
60 readonly text: string;
61 /** Sprungziel, gebildet wie in der HTML-Fassung. */
62 readonly kennung: string;
63 }
64 | { readonly art: 'absatz'; readonly teile: readonly Datenschutzteil[] }
65 | { readonly art: 'liste'; readonly punkte: readonly (readonly Datenschutzteil[])[] }
66 | { readonly art: 'code'; readonly text: string }
67 | {
68 readonly art: 'tabelle';
69 /**
70 * Die Spaltenüberschriften – **leer**, wenn die Tabelle keine hat.
71 *
72 * Die Standtabelle am Ende der Erklärung ist so eine: Sie ist in
73 * Wahrheit eine Aufzählung von Merkmal und Wert. Die Anzeige macht
74 * daraus Zeilenköpfe statt leerer Spaltenköpfe – eine Kopfzelle ohne
75 * Text ist für einen Bildschirmleser eine Ansage ins Nichts.
76 */
77 readonly kopf: readonly string[];
78 readonly zeilen: readonly (readonly (readonly Datenschutzteil[])[])[];
79 };
80
81 export interface Datenschutzerklaerung {
82 /** SHA-256 des Veröffentlichungsteils von `docs/datenschutz.md`. */
83 readonly quellpruefsumme: string;
84 /** Fassung der Erklärung, aus ihrer Standtabelle gelesen. */
85 readonly fassung: string;
86 /** Stand der Erklärung, aus ihrer Standtabelle gelesen. */
87 readonly stand: string;
88 readonly bloecke: readonly Datenschutzblock[];
89 }
90
91 export interface Datenschutzangaben {
92 /** Die Erklärung; `null`, wenn die Datei fehlt oder unlesbar ist. */
93 readonly erklaerung: Datenschutzerklaerung | null;
94 /**
95 * Warum sie fehlt – für die Anzeige, nicht fürs Protokoll.
96 *
97 * Ein leerer Bereich ohne Begründung wäre die schlechteste Auskunft: Der
98 * Nutzende hielte die Erklärung für nicht vorhanden statt für nicht
99 * ladbar.
100 */
101 readonly warnung: string | null;
102 }
103
104 /** Ohne Datei: die Anwendung läuft weiter und sagt, was fehlt. */
105 export const DATENSCHUTZ_LEER: Datenschutzangaben = Object.freeze({
106 erklaerung: null,
107 warnung: null,
108 });
109
110 /**
111 * Die Überschriften, die ein Sprungmenü anbietet.
112 *
113 * Nur die zweite Ebene: Die Erklärung hat 13 Abschnitte und 20
114 * Unterabschnitte; ein Verzeichnis über alle 33 wäre selbst wieder eine
115 * Wand aus Text. Die erste Ebene ist der Titel und braucht kein Sprungziel.
116 */
117 export function abschnitte(
118 erklaerung: Datenschutzerklaerung,
119 ): readonly { readonly text: string; readonly kennung: string }[] {
120 return erklaerung.bloecke
121 .filter(
122 (block): block is Extract<Datenschutzblock, { art: 'ueberschrift' }> =>
123 block.art === 'ueberschrift' && block.ebene === 2,
124 )
125 .map((block) => ({ text: block.text, kennung: block.kennung }));
126 }