waffensachkunde

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

/ app src shared datenschutz.ts

4,7 KB Rohdatei
app/src/shared/datenschutz.ts — 113 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 vier Formen, die der Erzeuger hervorbringt. Nachgezählt am
52 * Dokument vom 30.08.2026: 93 Absätze, 34 Überschriften, 11 Listen und
53 * 6 Codeblöcke. 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 export interface Datenschutzerklaerung {
69 /** SHA-256 des Veröffentlichungsteils von `docs/datenschutz.md`. */
70 readonly quellpruefsumme: string;
71 /** Fassung der Erklärung, aus ihrer Standtabelle gelesen. */
72 readonly fassung: string;
73 /** Stand der Erklärung, aus ihrer Standtabelle gelesen. */
74 readonly stand: string;
75 readonly bloecke: readonly Datenschutzblock[];
76 }
77
78 export interface Datenschutzangaben {
79 /** Die Erklärung; `null`, wenn die Datei fehlt oder unlesbar ist. */
80 readonly erklaerung: Datenschutzerklaerung | null;
81 /**
82 * Warum sie fehlt – für die Anzeige, nicht fürs Protokoll.
83 *
84 * Ein leerer Bereich ohne Begründung wäre die schlechteste Auskunft: Der
85 * Nutzende hielte die Erklärung für nicht vorhanden statt für nicht
86 * ladbar.
87 */
88 readonly warnung: string | null;
89 }
90
91 /** Ohne Datei: die Anwendung läuft weiter und sagt, was fehlt. */
92 export const DATENSCHUTZ_LEER: Datenschutzangaben = Object.freeze({
93 erklaerung: null,
94 warnung: null,
95 });
96
97 /**
98 * Die Überschriften, die ein Sprungmenü anbietet.
99 *
100 * Nur die zweite Ebene: Die Erklärung hat 13 Abschnitte und 20
101 * Unterabschnitte; ein Verzeichnis über alle 33 wäre selbst wieder eine
102 * Wand aus Text. Die erste Ebene ist der Titel und braucht kein Sprungziel.
103 */
104 export function abschnitte(
105 erklaerung: Datenschutzerklaerung,
106 ): readonly { readonly text: string; readonly kennung: string }[] {
107 return erklaerung.bloecke
108 .filter(
109 (block): block is Extract<Datenschutzblock, { art: 'ueberschrift' }> =>
110 block.art === 'ueberschrift' && block.ebene === 2,
111 )
112 .map((block) => ({ text: block.text, kennung: block.kennung }));
113 }