waffensachkunde

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

/ app src shared ipc.ts

42,8 KB Rohdatei
app/src/shared/ipc.ts — 1075 Zeilen
1 /**
2 * Typisierter IPC-Vertrag zwischen Main- und Renderer-Prozess.
3 *
4 * Der Renderer erhält NIEMALS direkten Zugriff auf `ipcRenderer`. Stattdessen
5 * legt das Preload-Skript über `contextBridge` genau die hier beschriebene
6 * API frei. Jeder Kanal ist über {@link IpcVertrag} typisiert – Anfrage- und
7 * Antworttyp werden auf beiden Seiten aus derselben Quelle abgeleitet.
8 */
9
10 import { ANZEIGEGROESSE_STANDARD } from './ansicht';
11 import type { HartnaeckigeFrage } from './hartnaeckig';
12 import { SPRECHTEMPO_STANDARD } from './sprechtempo';
13 import { TEXTABSTAND_STANDARD } from './textabstand';
14 import type { Erklaerungstiefe } from './druck/fehlerprotokoll';
15 import type { Schriftgroesse } from './druck/stil';
16 import type { Erklaerungen } from './erklaerungen';
17 import type { Glossar } from './glossar';
18 import type { Verlaufspunkt } from './reifeverlauf';
19 import type { Normtexte } from './normtexte';
20 import type { Themen } from './themen';
21 import type { Katalog } from './katalog';
22 import type { Lernplan } from './lernplan';
23 import type {
24 Antwortprotokoll,
25 FrageStand,
26 Lernuebersicht,
27 Profil,
28 SitzungsFilter,
29 SitzungsFrage,
30 } from './lernstand';
31 import type { Datenschutzangaben } from './datenschutz';
32 import type { Lizenzangaben } from './lizenzen';
33 import type {
34 Einspielergebnis,
35 Pruefergebnis,
36 Sicherungsergebnis,
37 Uebernahmeergebnis,
38 } from './sicherung';
39 import type {
40 OffenerLauf,
41 Pruefungsantwort,
42 Pruefungsauftrag,
43 Pruefungsbogen,
44 Pruefungsergebnis,
45 Pruefungsverlauf,
46 Zwischenstand,
47 } from './pruefung';
48 import type { ThemeAuswahl } from './theme';
49
50 /**
51 * Ergebnis eines Exports.
52 *
53 * Ein Abbruch im Speicherdialog ist kein Fehler, sondern eine Entscheidung
54 * des Nutzers – deshalb `gespeichert: false` statt einer Ausnahme. Nur was
55 * wirklich schiefgeht (kein Schreibrecht, voller Datenträger), wirft.
56 */
57 export interface Druckergebnis {
58 readonly gespeichert: boolean;
59 /** Wohin geschrieben wurde, oder `null` bei Abbruch. */
60 readonly pfad: string | null;
61 /** Größe der geschriebenen Datei in Byte; 0 bei Abbruch. */
62 readonly bytes: number;
63 /**
64 * Kennung, unter der sich die Datei öffnen oder im Ordner zeigen lässt.
65 *
66 * **Eine Kennung und kein Pfad**: Wer einen Pfad mitbringen darf, darf auch
67 * einen anderen mitbringen. Der Hauptprozess merkt sich, was er selbst
68 * geschrieben hat, und öffnet ausschließlich das (`main/dateizugriff.ts`) –
69 * dieselbe Überlegung wie beim Einspielweg der Sicherung.
70 *
71 * Fehlt bei Abbruch und in älteren Fassungen des Anwendungskerns; dann
72 * entfallen die beiden Schaltflächen.
73 */
74 readonly dateiKennung?: string;
75 }
76
77 /** Ergebnis des better-sqlite3-Rauchtests im Main-Prozess. */
78 export interface DatenbankStatus {
79 /** `true`, wenn das native Modul geladen und eine Abfrage ausgeführt wurde. */
80 readonly verfuegbar: boolean;
81 /** Von SQLite gemeldete Version, z. B. "3.50.2". */
82 readonly sqliteVersion: string | null;
83 /** Für Menschen lesbare Statusmeldung (deutsch). */
84 readonly meldung: string;
85 }
86
87 /**
88 * Befund des Katalogstand-Abgleichs beim Öffnen des Lernstands.
89 *
90 * Die Lernstand-Datenbank vermerkt seit Schemafassung 9, gegen welchen
91 * amtlichen Katalogstand sie geführt wird. Weicht der geladene Katalog davon
92 * ab – etwa nach einer neuen BVA-Fassung mit geänderter Nummerierung –,
93 * entsteht dieser Befund, und zwar genau in der Sitzung, die den Wechsel
94 * zuerst sieht: Danach ist der neue Stand vermerkt, und der nächste Start
95 * findet Gleichstand vor. Gelöscht wird dabei nichts; die beiden Zählungen
96 * beziffern nur, was im geladenen Katalog kein Ziel mehr hat.
97 */
98 export interface Katalogwechsel {
99 /** Katalogstand (ISO-Datum), unter dem der Lernstand bisher geführt wurde. */
100 readonly vorher: string;
101 /** Stand des jetzt geladenen Katalogs. */
102 readonly nachher: string;
103 /** `frage_stand`-Zeilen zu Frage-IDs, die es im geladenen Katalog nicht gibt. */
104 readonly verwaisteStaende: number;
105 /** `antwort_log`-Zeilen zu Frage-IDs, die es im geladenen Katalog nicht gibt. */
106 readonly verwaisteAntworten: number;
107 }
108
109 /** Laufzeit-Informationen für die Info-Anzeige im Startbildschirm. */
110 export interface AnwendungsInfo {
111 readonly anwendungsVersion: string;
112 /**
113 * Kurzes Commit-Kürzel dieses Baus, `+` bei ungesicherten Änderungen.
114 *
115 * Leer, wenn beim Bauen kein Git zur Verfügung stand. Die Versionsnummer
116 * allein benennt einen Stand nicht eindeutig – zwischen zwei
117 * Veröffentlichungen entstehen viele Bauten mit derselben Nummer.
118 */
119 readonly baukennung: string;
120 /** Commit-Datum als ISO-Datum, oder leer. */
121 readonly baustand: string;
122 readonly electronVersion: string;
123 readonly chromeVersion: string;
124 readonly nodeVersion: string;
125 readonly plattform: NodeJS.Platform;
126 readonly datenbank: DatenbankStatus;
127 /**
128 * Befund eines Katalogwechsels, oder `null`, wenn nichts zu melden ist –
129 * auch dann, wenn der Lernstand nicht zu öffnen war: Dieser Fehler hat
130 * seine eigene Meldung an anderer Stelle. Optional geführt wie die übrigen
131 * jüngeren Erweiterungen, damit die Attrappen der Oberflächentests mit dem
132 * bisherigen Zuschnitt gültig bleiben.
133 */
134 readonly katalogwechsel?: Katalogwechsel | null;
135 /**
136 * Ablageort des Fehlerprotokolls, oder `null`.
137 *
138 * Steht im Systemzustand, damit die Datei auffindbar ist, wenn jemand einen
139 * Absturz melden will. Sie geht nie von selbst irgendwohin – der Satz
140 * daneben sagt das ausdrücklich.
141 *
142 * Optional geführt wie die übrigen jüngeren Erweiterungen, damit die
143 * Attrappen der Oberflächentests mit dem bisherigen Zuschnitt gültig
144 * bleiben.
145 */
146 readonly protokollPfad?: string | null;
147 }
148
149 /** Dauerhaft gespeicherte Nutzereinstellungen. */
150 export interface Einstellungen {
151 readonly thema: ThemeAuswahl;
152 /** Zuletzt benutztes Lernprofil. */
153 readonly profilId?: number;
154 /**
155 * Wie viele Fragen eine Lernsitzung umfasst.
156 *
157 * Fehlt der Wert, gilt {@link SITZUNGSUMFANG}. Bis 0.26.6 war die Zahl
158 * eine Konstante im Quelltext: Wer täglich nur zehn Minuten hat, bekam
159 * dieselben zwanzig Fragen vorgelegt wie jemand mit einer Stunde – und
160 * ließ die Sitzung entweder halb liegen oder lernte sie unter Zeitdruck
161 * zu Ende. Der Anwendungskern nimmt seit jeher jede Zahl von 1 bis 1000
162 * entgegen; es fehlte allein der Weg dorthin.
163 */
164 readonly sitzungsumfang?: number;
165 /**
166 * Die Frage nach dem abwählbaren Kapitel wurde beim Erststart gestellt.
167 *
168 * Eine Einstellung und keine Spalte am Profil: Es ist eine Frage der
169 * Bedienung, kein Lernstand. Sie steht deshalb auch nicht in der
170 * Sicherung – wer auf einem neuen Rechner anfängt, bekommt sie noch
171 * einmal, und das ist richtig so: Dort ist es wieder ein erster Start.
172 */
173 readonly zuschnittGefragt?: boolean;
174 /**
175 * Antwortoptionen in zufälliger Reihenfolge zeigen.
176 *
177 * Vorgabe ist **aus**. Ein Lernvorteil des Mischens ist nicht belegt – es
178 * existiert keine kontrollierte Studie, die eine gemischte gegen eine feste
179 * Übungsreihenfolge stellt und danach das Behalten misst. Belegt ist nur,
180 * dass Umsortieren die Itemschwierigkeit kaum verändert; damit trägt auch
181 * die Gegenbegründung „macht schwerer, also lernwirksamer" nicht. Dem
182 * unbelegten Nutzen steht ein messbarer Aufwand gegenüber: 83 % der
183 * Auswahlfragen erscheinen dann in anderer Reihenfolge als im amtlichen
184 * Katalog, in dem der Lernende nachschlägt. Die Begründung im Einzelnen
185 * steht in `docs/entscheidung-antwortreihenfolge.md`.
186 *
187 * Wer die Funktion will, schaltet sie unter „Schwierigkeit" ein.
188 */
189 readonly optionenMischen?: boolean;
190 /**
191 * Verschweigen, wie viele Antworten richtig sind.
192 *
193 * Vorgabe ist **aus**, also wie bisher: Die Lernsitzung verrät über Radios
194 * gegenüber Kästchen und über den Hinweistext, ob eine oder mehrere
195 * Antworten richtig sind. Eingeschaltet entfällt dieser Hinweis – so, wie
196 * die Prüfungssimulation es ohnehin hält.
197 *
198 * Das ist der belegbarere der beiden Schwierigkeitsregler: Er entfernt
199 * einen Hinweis, den die Prüfung nicht gibt, statt einen hinzuzufügen,
200 * den sie auch nicht gibt.
201 */
202 readonly antwortzahlVerbergen?: boolean;
203 /**
204 * Bietet in Lernsitzung und Prüfungslauf eine Schaltfläche zum Vorlesen an
205 * (nutzt Systemstimmen).
206 *
207 * Gesprochen wird **nur auf Knopfdruck**. Bis 0.11.1 begann die Ansage der
208 * Rückmeldung von selbst – was weder der Schalter noch das Handbuch zusagte.
209 */
210 readonly vorlesen?: boolean;
211 /**
212 * In der Lernsitzung von selbst vorlesen, ohne Knopfdruck.
213 *
214 * Ab Werk **aus**, und nur wirksam, wenn {@link vorlesen} an ist. Bis 0.11.1
215 * gab es diese Wahl nicht: Die Rückmeldung wurde immer von selbst
216 * angesagt, sobald die Sprachausgabe eingeschaltet war. Gemeldet von einem
217 * Nutzer, und die eigenen Texte gaben ihm recht – Schalter wie Handbuch
218 * sagten nur eine Schaltfläche zu.
219 *
220 * Wer es einschaltet, bekommt beides angesagt: die neue Frage, sobald sie
221 * erscheint, und die Rückmeldung nach dem Bestätigen. Gedacht für Menschen
222 * mit Lese-Rechtschreib-Störung oder ermüdeten Augen, denen ein Knopfdruck
223 * je Frage im Weg steht.
224 *
225 * **Nicht** im Prüfungslauf: Dort bildet die Anwendung eine schriftliche
226 * Prüfung nach, und eine Stimme, die von selbst losredet, gehört nicht dazu.
227 * Die Schaltfläche gibt es dort weiterhin.
228 */
229 readonly vorlesenAutomatisch?: boolean;
230 /**
231 * Bei falscher Antwort die ausführliche Begründung vorlesen.
232 *
233 * Ab Werk **aus**, unabhängig von {@link vorlesenAutomatisch} und nur
234 * wirksam, wenn {@link vorlesen} an ist.
235 *
236 * Der ausführliche Text bleibt sonst überall außen vor – auch die
237 * Schaltfläche liest ihn nicht: Er dauert gesprochen über eine Minute, und
238 * die nächste Frage wartet. Genau deshalb ist er eine eigene Wahl und nicht
239 * Teil der Ansage: Wer eine Frage falsch hatte, hat die Minute gerade übrig;
240 * bei jeder richtigen Antwort wäre sie eine Zumutung.
241 *
242 * Unabhängig, weil die nützlichste Kombination die ist, die man sonst nicht
243 * einstellen könnte: sonst selbst lesen, aber bei einem Fehler die
244 * Begründung hören.
245 */
246 readonly vorlesenErklaerungBeiFehler?: boolean;
247 /**
248 * Sprechgeschwindigkeit als `rate` der Sprachausgabe; 1 ist die
249 * Systemvorgabe.
250 *
251 * Bis 0.22.0 wurde sie nie gesetzt, das Tempo war also nicht einstellbar.
252 * Für Menschen mit Lese-Rechtschreib-Störung – die Zielgruppe, die der
253 * Schalter „Vorlesen“ selbst benennt – ist ein festes Tempo bei
254 * minutenlangen Rechtstexten eine echte Hürde. Stufen in
255 * `renderer/src/lernen/sprachausgabe.ts`.
256 */
257 readonly sprechtempo?: number;
258 /**
259 * Anzeigegröße der ganzen Anwendung in Prozent.
260 *
261 * Gültige Stufen stehen in `shared/ansicht.ts`. Gespeichert wird die
262 * Prozentzahl, nicht Chromiums Zoomfaktor – sie ist die Zahl, die auch
263 * angezeigt und angesagt wird.
264 */
265 readonly anzeigegroesse?: number;
266 /**
267 * Zeilen-, Wort- und Zeichenabstand in Stufen (WCAG 1.4.12).
268 *
269 * Stufen und Werte stehen in `shared/textabstand.ts`. Getrennt von der
270 * Anzeigegröße, weil es etwas anderes ist: Die Größe skaliert alles
271 * gemeinsam, der Abstand gibt dem Text bei gleicher Größe mehr Luft. Wer
272 * Legasthenie hat, braucht oft das Zweite und nicht das Erste.
273 */
274 readonly textabstand?: string;
275 /**
276 * Zeichentasten-Kürzel in der Lernsitzung (WCAG 2.1.4).
277 *
278 * **Dreiwertig, und das mit Absicht.** Fehlt das Feld, hat niemand
279 * entschieden – dann richtet sich die Voreinstellung danach, ob ein
280 * Hilfsmittel läuft: Im Lesemodus von NVDA und JAWS sind die Ziffern für
281 * die Navigation nach Überschriftenebenen belegt. `true` und `false` sind
282 * ausdrückliche Entscheidungen und haben immer Vorrang, auch wenn später
283 * ein Screenreader startet.
284 *
285 * Deshalb steht das Feld **nicht** in {@link EINSTELLUNGEN_STANDARD} und
286 * wird von `einstellungenBereinigen` nicht aufgefüllt: Ein Rückfallwert
287 * wäre hier eine erfundene Entscheidung und löschte den Unterschied
288 * zwischen „aus, weil ein Hilfsmittel läuft“ und „aus, weil ich das so
289 * will“ – genau den Unterschied, den der Hinweistext am Schalter erklärt.
290 *
291 * Bis 0.21.0 lag diese Wahl allein im `localStorage` des Renderers und
292 * überlebte damit weder einen Gerätewechsel noch eine Sicherung.
293 */
294 readonly tastenkuerzel?: boolean;
295 /**
296 * Zeitpunkt der letzten selbst angelegten Sicherung, als ISO-Zeit.
297 *
298 * Fehlt das Feld, ist noch nie eine angelegt worden – und genau das steht
299 * dann auch auf dem Bildschirm. Ein Rückfall auf „heute“ wäre die
300 * gefährlichste Lüge, die hier möglich ist.
301 *
302 * **Eine Einstellung und kein Lernstand**, obwohl es um den Lernstand
303 * geht: Der Vermerk beschreibt, was auf *diesem* Rechner geschehen ist.
304 * Wanderte er in der Sicherung mit, behauptete er auf dem neuen Rechner
305 * eine Sicherung, die dort niemand angelegt hat.
306 */
307 readonly letzteSicherung?: string;
308 /**
309 * Zuletzt benutzte Fenstergröße und -lage.
310 *
311 * Bis 0.22.0 startete das Fenster immer mit 1180 × 820 an der
312 * Systemposition. Wer maximiert arbeitet oder es – wie der Kommentar in
313 * `main/fenster.ts` selbst als Anwendungsfall nennt – schmal neben eine
314 * Bildschirmlupe stellt, richtete das bei jedem Start neu ein. Das trifft
315 * genau die Zielgruppe des Barrierefreiheitsanspruchs.
316 *
317 * Die Lage wird beim Start gegen die vorhandenen Bildschirme geprüft: Ein
318 * Fenster, das auf einem abgesteckten zweiten Monitor lag, wäre sonst
319 * unsichtbar und praktisch unerreichbar.
320 */
321 readonly fenster?: {
322 readonly breite: number;
323 readonly hoehe: number;
324 readonly x?: number;
325 readonly y?: number;
326 readonly maximiert?: boolean;
327 };
328 }
329
330 export const EINSTELLUNGEN_STANDARD: Einstellungen = Object.freeze({
331 thema: 'system',
332 optionenMischen: false,
333 antwortzahlVerbergen: false,
334 vorlesen: false,
335 zuschnittGefragt: false,
336 vorlesenAutomatisch: false,
337 vorlesenErklaerungBeiFehler: false,
338 anzeigegroesse: ANZEIGEGROESSE_STANDARD,
339 textabstand: TEXTABSTAND_STANDARD,
340 sprechtempo: SPRECHTEMPO_STANDARD,
341 });
342
343 /**
344 * Die Einstellungen, die mit einer Sicherung mitreisen.
345 *
346 * **Warum überhaupt.** Bis 0.27.0 enthielt die Sicherung Profile, Antworten,
347 * Merklisten, Termine und Verlauf – aber keine Einstellung. Wer 400 Prozent
348 * Anzeigegröße oder hohen Kontrast braucht, musste auf dem zweiten Rechner
349 * ohne sie anfangen, um sie einzustellen. Gerade das, was Barrierefreiheit
350 * herstellt, blieb zurück.
351 *
352 * **Warum eine Auswahl und nicht alles.** Vier Felder dürfen nicht mitreisen,
353 * und das ist keine Geschmacksfrage:
354 *
355 * - `profilId` zeigt auf eine Profilnummer des **alten** Rechners. Auf dem
356 * neuen gehört sie einem anderen Profil oder gar keinem.
357 * - `letzteSicherung` nennt einen Dateipfad, den es dort nicht gibt.
358 * - `fenster` ist die Fenstergröße einer fremden Bildschirmauflösung.
359 * - `zuschnittGefragt` merkt sich, dass die Erststart-Frage **auf diesem
360 * Rechner** gestellt wurde. Mitgereist übersprünge sie jemand, der sie nie
361 * gesehen hat.
362 *
363 * Der Rest reist mit. Er beschreibt, wie jemand lesen, hören und lernen
364 * will – und das ändert sich nicht mit dem Gerät.
365 */
366 export const EINSTELLUNGEN_REISEN = Object.freeze([
367 'thema',
368 'anzeigegroesse',
369 'textabstand',
370 'sprechtempo',
371 'vorlesen',
372 'vorlesenAutomatisch',
373 'vorlesenErklaerungBeiFehler',
374 'tastenkuerzel',
375 'optionenMischen',
376 'antwortzahlVerbergen',
377 'sitzungsumfang',
378 ] as const satisfies readonly (keyof Einstellungen)[]);
379
380 /** Nur die mitreisenden Felder, als eigenes Stück. */
381 export type ReisendeEinstellungen = Partial<
382 Pick<Einstellungen, (typeof EINSTELLUNGEN_REISEN)[number]>
383 >;
384
385 /**
386 * Die einzige Stelle, an der IPC-Kanäle definiert werden.
387 * `anfrage` = Nutzlast vom Renderer, `antwort` = Rückgabe des Main-Prozesses.
388 *
389 * Kanäle ohne Nutzlast verwenden `undefined` (nicht `void`): der Wert wird
390 * tatsächlich über die Bridge gereicht, ist also ein Wert und kein
391 * Rückgabetyp.
392 */
393 export interface IpcVertrag {
394 'anwendung:info': { anfrage: undefined; antwort: AnwendungsInfo };
395 'einstellungen:lesen': { anfrage: undefined; antwort: Einstellungen };
396 /**
397 * Ändert einzelne Einstellungen. Die Nutzlast ist eine Teilmenge – sie
398 * wird über den gespeicherten Stand gelegt, nicht an seine Stelle
399 * gesetzt. Antwort ist der vollständige neue Stand.
400 */
401 'einstellungen:schreiben': { anfrage: Partial<Einstellungen>; antwort: Einstellungen };
402
403 /** Liefert den vollständigen Fragenkatalog (einmalig beim Start). */
404 'katalog:laden': { anfrage: undefined; antwort: Katalog };
405 /** Bild eines Prüfzeichens als Data-URL – die CSP verbietet Dateizugriffe. */
406 'katalog:bild': { anfrage: { bildId: string }; antwort: string };
407 /**
408 * Erklärungen zu den Fragen – eigener redaktioneller Inhalt.
409 *
410 * Wird wie der Katalog einmalig beim Start geladen. Der Bestand ist
411 * kleiner als der Katalog und wächst mit ihm; ein Nachladen je Frage
412 * wäre viel Verkehr für wenig Nutzen.
413 */
414 'erklaerungen:laden': { anfrage: undefined; antwort: Erklaerungen };
415 /**
416 * Glossar der Fachbegriffe – erfüllt WCAG 3.1.3 und 3.1.4.
417 *
418 * Wie die Erklärungen einmalig geladen: Die Einträge ändern sich zur
419 * Laufzeit nicht, und die Begriffssuche braucht ohnehin alle auf einmal.
420 */
421 'glossar:laden': { anfrage: undefined; antwort: Glossar };
422
423 /**
424 * Die mitgelieferten Normtexte.
425 *
426 * Wie Erklärungen und Glossar einmalig geladen. Der Umfang ist bekannt und
427 * fest: nur die Normen, die irgendwo zitiert werden – rund 500 KiB. Ein
428 * Kanal je Fundstelle wäre bei 2123 Zitaten mehr Verkehr als die ganze
429 * Datei und brächte dem Lesenden nichts, weil er ohnehin blättert.
430 */
431 'gesetz:normtexte': { anfrage: undefined; antwort: Normtexte };
432
433 /**
434 * Die redaktionelle Feingliederung der Kapitel II bis IV.
435 *
436 * Wie Erklärungen und Glossar einmalig geladen: 29 Gruppen über 230 Fragen,
437 * und sie ändern sich zur Laufzeit nicht.
438 */
439 'themen:laden': { anfrage: undefined; antwort: Themen };
440
441 'profil:liste': { anfrage: undefined; antwort: Profil[] };
442 'profil:anlegen': { anfrage: { name: string }; antwort: Profil };
443 'profil:aktualisieren': {
444 anfrage: {
445 id: number;
446 name?: string;
447 pruefungstermin?: string | null;
448 /** Kapitel, die dieses Profil dauerhaft nicht lernt – Kennungen wie „IV“. */
449 kapitelAusschluss?: readonly string[];
450 };
451 antwort: Profil;
452 };
453 /**
454 * Löscht ein Profil samt Lernstand, Antworten und Prüfungsverlauf.
455 *
456 * Antwort ist die verbleibende Liste – die Oberfläche muss danach ohnehin
457 * ein anderes Profil wählen und braucht die Auswahl sofort.
458 */
459 'profil:loeschen': { anfrage: { id: number }; antwort: Profil[] };
460
461 /** Stellt die Fragen einer Lernsitzung nach Filter zusammen. */
462 'lernen:sitzung': {
463 anfrage: { profilId: number; filter: SitzungsFilter };
464 antwort: SitzungsFrage[];
465 };
466 /** Protokolliert eine Antwort und liefert den neuen Stand der Frage. */
467 'lernen:antworten': {
468 anfrage: { profilId: number; protokoll: Antwortprotokoll };
469 antwort: FrageStand;
470 };
471 /** Merkliste umschalten. */
472 'lernen:merken': {
473 anfrage: { profilId: number; frageId: string; gemerkt: boolean };
474 antwort: FrageStand;
475 };
476 /**
477 * Der gespeicherte Stand einer einzelnen Frage; `null`, wenn sie noch nie
478 * beantwortet wurde.
479 *
480 * **Wozu.** `FrageStand` transportierte `versuche`, `richtige`,
481 * `zuletztBeantwortet` und `faelligAb` schon immer über die Brücke – die
482 * Oberfläche benutzte davon bis 0.22.0 ausschließlich `gemerkt`. Der
483 * Lernende konnte nirgends beantworten, wie oft er diese Frage schon hatte
484 * und wie oft davon richtig; „Warum kommt die schon wieder?“ blieb ohne
485 * Antwort, obwohl die Antwort in der Datenbank stand.
486 *
487 * Ein eigener Kanal und nicht die Rückgabe von `lernen:antworten`: Der
488 * Steckbrief soll die Historie **vor** der heutigen Antwort zeigen, und bei
489 * offenen Fragen wird die Buchung ohnehin bis zum Weiterblättern
490 * zurückgehalten (`useSitzung`, `bewertungAendern`).
491 */
492 'lernen:fragestand': {
493 anfrage: { profilId: number; frageId: string };
494 antwort: FrageStand;
495 };
496 /**
497 * Die hartnäckigen Fragen dieses Profils – wiederholt danebengegangen.
498 *
499 * Rein deskriptiv: gezählte Fehlschläge aus dem Protokoll, keine
500 * Kennzahl. Einzelheiten in `shared/hartnaeckig.ts`.
501 */
502 'lernen:hartnaeckige': {
503 anfrage: { profilId: number };
504 antwort: HartnaeckigeFrage[];
505 };
506 'lernen:uebersicht': { anfrage: { profilId: number }; antwort: Lernuebersicht };
507 /**
508 * Der Reifegrad der letzten Tage, aus dem Antwortprotokoll nachgerechnet.
509 *
510 * Eigener Kanal statt eines Feldes an `lernen:uebersicht`: Die Rechnung
511 * läuft über das ganze Protokoll und wird nur dort gebraucht, wo der
512 * Verlauf auch angezeigt wird. Die Übersicht holt die Oberfläche nach
513 * **jeder** Antwort.
514 */
515 'lernen:verlauf': {
516 anfrage: { profilId: number; tage?: number };
517 antwort: Verlaufspunkt[];
518 };
519 /**
520 * Lernplan: Prognose, Tagespensum und Machbarkeit zum Prüfungstermin.
521 *
522 * Eigener Kanal statt einer Erweiterung von `lernen:uebersicht`: Der Plan
523 * rechnet über den gesamten Katalog und wird nur dort gebraucht, wo er
524 * auch angezeigt wird.
525 */
526 'lernen:plan': { anfrage: { profilId: number }; antwort: Lernplan };
527 /** Lernstand des Profils vollständig zurücksetzen (ohne Begrenzung). */
528 'lernen:zuruecksetzen': {
529 anfrage: { profilId: number; kapitel?: string | null };
530 antwort: Lernuebersicht;
531 };
532
533 /**
534 * Stellt einen Prüfungsbogen nach dem gewählten Profil zusammen.
535 *
536 * Die Antwort ist ein {@link Pruefungsbogen} und keine reine Fragenliste:
537 * Weicht das Ziehen von den Vorgaben ab, gehört das an den Bildschirm und
538 * nicht nur ins Protokoll des Hauptprozesses. Die Sätze in `warnungen`
539 * kommen fertig formuliert von dort.
540 */
541 'pruefung:starten': {
542 anfrage: { profilId: number; auftrag: Pruefungsauftrag };
543 antwort: Pruefungsbogen;
544 };
545 /** Wertet einen Simulationslauf aus und speichert ihn im Verlauf. */
546 'pruefung:auswerten': {
547 anfrage: {
548 profilId: number;
549 auftrag: Pruefungsauftrag;
550 antworten: Pruefungsantwort[];
551 dauerMs: number;
552 zeitAbgelaufen: boolean;
553 };
554 antwort: Pruefungsergebnis;
555 };
556 'pruefung:verlauf': { anfrage: { profilId: number }; antwort: Pruefungsverlauf[] };
557
558 /**
559 * Sichert den Zwischenstand eines laufenden Bogens.
560 *
561 * Bewusst OHNE den Bogen selbst: Der Kern hat ihn beim Starten abgelegt.
562 * Wer den Bogen mitbringen dürfte, könnte ihn auch erfinden – dann wäre
563 * die Prüfung der eingereichten Antwortliste wertlos. Gesichert wird nur,
564 * was dem Renderer gehört.
565 *
566 * Die Antwort sagt, ob es noch eine Zeile zu sichern gab. `false` heißt
567 * nicht Fehler, sondern „dieser Lauf ist vorbei" – etwa weil die Abgabe
568 * schneller war als ein entprellter Nachzügler.
569 */
570 'pruefung:sichern': {
571 anfrage: { profilId: number; stand: Zwischenstand };
572 antwort: boolean;
573 };
574
575 /** Der unterbrochene Lauf eines Profils, oder `null`. */
576 'pruefung:offen': { anfrage: { profilId: number }; antwort: OffenerLauf | null };
577
578 /** Verwirft den unterbrochenen Lauf – nur auf ausdrücklichen Wunsch. */
579 'pruefung:verwerfen': { anfrage: { profilId: number }; antwort: undefined };
580
581 /**
582 * Meldet, ob gerade eine Prüfung bearbeitet wird.
583 *
584 * Der Hauptprozess fragt beim Schließen des Fensters nach, wenn eine läuft.
585 * Bewusst ein Abgleich und keine Kantenmeldung: Der Renderer schickt seinen
586 * Zustand auch unverändert, damit ein Merker kein Neuladen überlebt.
587 */
588 'pruefung:laeuft': { anfrage: { laeuft: boolean }; antwort: undefined };
589
590 /**
591 * Erzeugt den Lernbericht als PDF und fragt, wohin er gespeichert werden soll.
592 *
593 * Der Anwendungskern holt die Zahlen selbst – Übersicht, Lernplan und
594 * Prüfungsverlauf liegen dort ohnehin. Der Renderer schickt nur, für wen
595 * und wie groß; so kann keine Oberfläche Zahlen in ein Dokument bringen,
596 * die der Kern nicht bestätigt hat.
597 */
598 'druck:lernbericht': {
599 anfrage: { profilId: number; schriftgroesse: Schriftgroesse };
600 antwort: Druckergebnis;
601 };
602
603 /**
604 * Setzt die Anzeigegröße und merkt sie sich.
605 *
606 * Der Zoom gehört in den Hauptprozess: Er wirkt auf das Fenster, nicht auf
607 * das Dokument, und muss beim nächsten Start wieder anliegen, bevor der
608 * erste Bildpunkt gezeichnet wird.
609 */
610 'ansicht:groesse': { anfrage: { prozent: number }; antwort: number };
611
612 /**
613 * Das Fehlerprotokoll als PDF – die zuletzt falsch beantworteten Fragen.
614 *
615 * `tiefe` steuert, ob nur die Kurzerklärung oder die vollständige
616 * Begründung mitgedruckt wird. Der Unterschied im Umfang ist erheblich —
617 * die Zahlen dazu stehen in `shared/druck/umfang.ts` und sonst nirgends.
618 */
619 'druck:fehlerprotokoll': {
620 anfrage: { profilId: number; schriftgroesse: Schriftgroesse; tiefe: Erklaerungstiefe };
621 antwort: Druckergebnis;
622 };
623
624 /**
625 * Die Fragenliste als PDF – ein Bogen zum Bearbeiten auf Papier.
626 *
627 * `bereiche` sind Kapitel- oder Abschnittskennungen; eine leere Liste
628 * bedeutet den ganzen Katalog. **Ohne Lernprofil**: Die Liste hängt am
629 * Katalog und nicht am Lernstand, es gibt also nichts zu personalisieren.
630 */
631 'druck:fragenliste': {
632 anfrage: {
633 bereiche: readonly string[];
634 schriftgroesse: Schriftgroesse;
635 mitLoesungen: boolean;
636 };
637 antwort: Druckergebnis;
638 };
639
640 /**
641 * Ein einzelnes Profil aus der geprüften Datei dazunehmen.
642 *
643 * `profilIndex` zeigt in `Pruefergebnis.ausDatei.jeProfil` – nicht auf eine
644 * Profilnummer. Die Nummern der fremden Datei kennt nur der Anwendungskern,
645 * und er behält sie für sich.
646 */
647 'sicherung:uebernehmen': {
648 anfrage: { vorgang: string; profilIndex: number };
649 antwort: Uebernahmeergebnis;
650 };
651
652 /** Lizenzangaben für den Bereich „Über diese Software“. */
653 'lizenzen:lesen': { anfrage: undefined; antwort: Lizenzangaben };
654
655 /**
656 * Die mitgelieferte Datenschutzerklärung.
657 *
658 * Derselbe Weg wie bei den Lizenztexten: Der Hauptprozess liest die Datei,
659 * der Renderer bekommt fertige Blöcke. Kein Dateizugriff im Renderer.
660 */
661 'datenschutz:lesen': { anfrage: undefined; antwort: Datenschutzangaben };
662
663 /**
664 * Meldet, ob gerade ein Hilfsmittel läuft (Screenreader, Bildschirmlupe …).
665 *
666 * Grundlage ist `app.accessibilitySupportEnabled` von Electron. Der Wert
667 * steuert die Voreinstellung der Zeichenkürzel: Diese kollidieren im
668 * Lesemodus von NVDA und JAWS mit der Schnellnavigation – dort sind die
669 * Ziffern für Überschriftenebenen belegt.
670 */
671 'system:hilfsmittel': { anfrage: undefined; antwort: boolean };
672 /**
673 * Legt Text in die Zwischenablage.
674 *
675 * Es gibt diesen Kanal, weil der Renderer es selbst **nicht darf**:
676 * `sicherheit.ts` lehnt mit `setPermissionCheckHandler(() => false)`
677 * sämtliche Berechtigungen ab, und Blink fragt für
678 * `navigator.clipboard.writeText()` die Berechtigung
679 * `clipboard-sanitized-write` ab. Nachgemessen im gebauten Fenster:
680 * `isSecureContext` ist `true`, `navigator.clipboard.writeText` existiert –
681 * und der Aufruf scheitert mit `NotAllowedError: Write permission denied`.
682 *
683 * Den Berechtigungswächter dafür zu öffnen wäre der schlechtere Handel: Das
684 * gäbe die Zwischenablage jedem Renderer-Code frei statt einem Kanal mit
685 * fester Nutzlast.
686 */
687 'system:kopieren': { anfrage: { text: string }; antwort: boolean };
688
689 /**
690 * Öffnet die Unterstützungsseite im Standardbrowser des Systems.
691 *
692 * Der einzige Kanal dieser Anwendung, der nach außen führt – und bewusst
693 * kein allgemeines „öffne diese Adresse“. Der Hauptprozess vergleicht die
694 * Nutzlast mit `UNTERSTUETZUNG_URL` aus `shared/unterstuetzung.ts` und
695 * öffnet ausschließlich diese eine Adresse; jede andere wird abgewiesen.
696 * Wer eine Adresse mitbringen darf, darf sonst auch eine andere mitbringen
697 * – dieselbe Überlegung wie bei `sicherung:einspielen`.
698 *
699 * Dass die Adresse trotzdem in der Nutzlast steht und nicht weggelassen
700 * wird, ist Absicht: So lässt sich prüfen, dass die Oberfläche genau das
701 * anfordert, was sie anzeigt.
702 *
703 * Antwort ist `false`, wenn keine Adresse eingetragen ist, die Nutzlast
704 * nicht passt oder das Betriebssystem den Browser nicht öffnen konnte. Die
705 * Oberfläche zeigt dann die Adresse zum Abschreiben – sie behauptet nicht,
706 * es sei etwas geschehen.
707 */
708 'system:unterstuetzung': { anfrage: { url: string }; antwort: boolean };
709
710 /**
711 * Öffnet eine soeben geschriebene Datei oder zeigt sie im Dateimanager.
712 *
713 * Die Nutzlast ist eine **Kennung**, nie ein Pfad: Wer einen Pfad
714 * mitbringen darf, darf auch einen anderen mitbringen. Der Hauptprozess
715 * merkt sich, was er in dieser Sitzung selbst geschrieben hat, und öffnet
716 * ausschließlich das (`main/dateizugriff.ts`).
717 *
718 * Antwortet `false`, wenn die Kennung unbekannt ist oder das
719 * Betriebssystem die Datei nicht öffnen konnte – etwa weil sie inzwischen
720 * verschoben wurde. Die Oberfläche sagt dann, dass es nicht geklappt hat,
721 * statt so zu tun, als sei etwas geschehen.
722 */
723 'system:datei-zeigen': {
724 anfrage: { kennung: string; wunsch: 'oeffnen' | 'ordner' };
725 antwort: boolean;
726 };
727
728 /**
729 * Schreibt den ganzen Lernstand in eine Datei.
730 *
731 * Der Nutzer wählt den Ort in einem Systemdialog. Übertragen wird nichts –
732 * die Datei geht dorthin, wohin er sie legt, und sonst nirgendwohin.
733 */
734 'sicherung:anlegen': { anfrage: undefined; antwort: Sicherungsergebnis };
735 /**
736 * Öffnet den Ordner der selbsttätigen Sicherheitskopien.
737 *
738 * **Ohne Argument, mit Absicht.** Welcher Ordner das ist, entscheidet der
739 * Hauptprozess; aus der Oberfläche kommt kein Pfad und keine Kennung. Ein
740 * Kanal, der einen Pfad entgegennähme, wäre ein Weg, beliebige Ordner des
741 * Rechners zu öffnen – und der Renderer hat in diesem Projekt keinen
742 * Node-Zugriff, gerade damit es solche Wege nicht gibt.
743 *
744 * Antwortet `false`, wenn es den Ordner noch nicht gibt. Er entsteht mit
745 * der ersten Kopie; bis dahin gibt es nichts zu zeigen, und die Oberfläche
746 * sagt das, statt so zu tun, als sei etwas geschehen.
747 */
748 'sicherung:ordner-zeigen': { anfrage: undefined; antwort: boolean };
749
750 /**
751 * Prüft eine gewählte Datei und beziffert sie, **ohne etwas zu verändern**.
752 *
753 * Getrennt vom Einspielen, weil zwischen „das steht darin“ und „ja, ersetze
754 * meinen Lernstand“ eine Entscheidung des Nutzers liegt. Bis
755 * {@link IpcVertrag['sicherung:einspielen']} gerufen wird, ist nichts
756 * angefasst.
757 */
758 'sicherung:pruefen': { anfrage: undefined; antwort: Pruefergebnis };
759
760 /**
761 * Ersetzt den Lernstand durch die zuvor geprüfte Datei.
762 *
763 * Die Nutzlast ist ausschliesslich die Vorgangskennung aus
764 * `sicherung:pruefen`; einen Pfad nimmt dieser Kanal nicht entgegen. Wer
765 * einen Pfad mitbringen darf, darf auch einen anderen mitbringen.
766 */
767 'sicherung:einspielen': { anfrage: { vorgang: string }; antwort: Einspielergebnis };
768 }
769
770 /**
771 * Kanal, über den der Main-Prozess von sich aus meldet, dass sich der
772 * Hilfsmittel-Zustand geändert hat. Nötig, weil ein Screenreader auch
773 * mitten in der Sitzung gestartet werden kann – gerade dann, wenn jemand
774 * merkt, dass er ihn braucht.
775 */
776 export const KANAL_HILFSMITTEL_GEAENDERT = 'system:hilfsmittel-geaendert' as const;
777
778 /** Kanal, über den der Hauptprozess eine geänderte Anzeigegröße meldet. */
779 export const KANAL_ANZEIGEGROESSE_GEAENDERT = 'ansicht:groesse-geaendert' as const;
780
781 /**
782 * Befehle, die aus der Menüleiste kommen und in der Oberfläche wirken.
783 *
784 * Die Menüleiste liegt im Hauptprozess, die Ansicht im Renderer – ohne einen
785 * solchen Kanal wäre ein Menüeintrag, der etwas anzeigt, gar nicht baubar.
786 * Absichtlich eine geschlossene Liste und keine freie Zeichenkette: Was hier
787 * nicht steht, kommt am anderen Ende nicht an.
788 */
789 export const MENUEBEFEHLE = [
790 'hilfe',
791 /*
792 Die Ziele des Menüs „Gehe zu“, seit Fassung 0.26.0.
793
794 Bis dahin führte das Anwendungsmenü zu keiner einzigen der zehn
795 Ansichten. Es ist die einzige Fläche des Fensters, die nicht wegrollt —
796 und der Startbildschirm ist bei 1265 Bildpunkten Breite 8530 hoch.
797 Wer daran vorbeigerollt war, hatte keinen Weg mehr in eine andere
798 Ansicht als zurück an den Anfang.
799
800 Die Namen sind die Ansichtsarten aus `renderer/src/lernen/typen.ts`, mit
801 „gehe-zu-“ davor: Ein Befehl ist keine Ansicht, und beide Listen sollen
802 sich unabhängig ändern dürfen.
803 */
804 'gehe-zu-start',
805 'gehe-zu-kapitelwahl',
806 'gehe-zu-pruefungswahl',
807 'gehe-zu-suche',
808 'gehe-zu-glossar',
809 'gehe-zu-gesetze',
810 'gehe-zu-ueber',
811 ] as const;
812
813 export type Menuebefehl = (typeof MENUEBEFEHLE)[number];
814
815 export function istMenuebefehl(wert: unknown): wert is Menuebefehl {
816 return typeof wert === 'string' && (MENUEBEFEHLE as readonly string[]).includes(wert);
817 }
818
819 /** Kanal, über den der Hauptprozess einen Menübefehl an die Ansicht meldet. */
820 export const KANAL_MENUE_BEFEHL = 'menue:befehl' as const;
821
822 /**
823 * Kanal, über den der Hauptprozess meldet, wie lange der Rechner geschlafen
824 * hat. Die Nutzlast ist die Schlafdauer in Millisekunden.
825 *
826 * **Warum das nicht der Renderer selbst messen kann.** Naheliegend wäre, im
827 * Sekundentakt zu prüfen, ob zwischen zwei Takten mehr als eine Sekunde
828 * vergangen ist. Chromium drosselt Zeitgeber in verdeckten Fenstern aber auf
829 * einen Takt pro Minute – ein bloß **minimiertes** Fenster sähe damit aus wie
830 * ein schlafender Rechner. Die Prüfungsuhr ließe sich durch Minimieren
831 * anhalten, und das wäre ein Schummelweg statt eines Nachteilsausgleichs.
832 * `powerMonitor` meldet ausschließlich echten Energiesparmodus und ist von
833 * der Drosselung unberührt.
834 */
835 export const KANAL_ENERGIESPAREN_ENDE = 'system:energiesparen-ende' as const;
836
837 export type IpcKanal = keyof IpcVertrag;
838 export type IpcAnfrage<K extends IpcKanal> = IpcVertrag[K]['anfrage'];
839 export type IpcAntwort<K extends IpcKanal> = IpcVertrag[K]['antwort'];
840
841 /**
842 * Allowlist aller erlaubten Kanäle. Main und Preload prüfen dagegen, damit
843 * keine unbeabsichtigten Kanäle über die Bridge erreichbar werden.
844 */
845 export const IPC_KANAELE = [
846 'anwendung:info',
847 'einstellungen:lesen',
848 'einstellungen:schreiben',
849 'katalog:laden',
850 'katalog:bild',
851 'erklaerungen:laden',
852 'glossar:laden',
853 'gesetz:normtexte',
854 'themen:laden',
855 'profil:liste',
856 'profil:anlegen',
857 'profil:aktualisieren',
858 'profil:loeschen',
859 'lernen:sitzung',
860 'lernen:antworten',
861 'lernen:merken',
862 'lernen:fragestand',
863 'lernen:hartnaeckige',
864 'lernen:uebersicht',
865 'lernen:verlauf',
866 'lernen:plan',
867 'lernen:zuruecksetzen',
868 'pruefung:starten',
869 'pruefung:auswerten',
870 'pruefung:verlauf',
871 'pruefung:sichern',
872 'pruefung:offen',
873 'pruefung:verwerfen',
874 'pruefung:laeuft',
875 'druck:lernbericht',
876 'druck:fehlerprotokoll',
877 'druck:fragenliste',
878 'ansicht:groesse',
879 'lizenzen:lesen',
880 'datenschutz:lesen',
881 'system:hilfsmittel',
882 'system:kopieren',
883 'system:unterstuetzung',
884 'system:datei-zeigen',
885 'sicherung:ordner-zeigen',
886 'sicherung:anlegen',
887 'sicherung:pruefen',
888 'sicherung:einspielen',
889 'sicherung:uebernehmen',
890 ] as const satisfies readonly IpcKanal[];
891
892 export function istIpcKanal(wert: unknown): wert is IpcKanal {
893 return typeof wert === 'string' && (IPC_KANAELE as readonly string[]).includes(wert);
894 }
895
896 /** Name des Objekts, das im Renderer unter `window` liegt. */
897 export const BRIDGE_NAME = 'lernApp' as const;
898
899 /** Die im Renderer sichtbare, vollständig typisierte API. */
900 export interface LernAppBridge {
901 readonly anwendungsInfoLesen: () => Promise<AnwendungsInfo>;
902 readonly einstellungenLesen: () => Promise<Einstellungen>;
903 readonly einstellungenSchreiben: (aenderung: Partial<Einstellungen>) => Promise<Einstellungen>;
904
905 readonly katalogLaden: () => Promise<Katalog>;
906 readonly katalogBild: (bildId: string) => Promise<string>;
907 readonly erklaerungenLaden: () => Promise<Erklaerungen>;
908 readonly glossarLaden: () => Promise<Glossar>;
909 readonly normtexteLaden: () => Promise<Normtexte>;
910 readonly themenLaden: () => Promise<Themen>;
911
912 readonly profilListe: () => Promise<Profil[]>;
913 readonly profilAnlegen: (name: string) => Promise<Profil>;
914 readonly profilAktualisieren: (
915 id: number,
916 aenderung: {
917 name?: string;
918 pruefungstermin?: string | null;
919 /** Kapitel, die dieses Profil dauerhaft nicht lernt. */
920 kapitelAusschluss?: readonly string[];
921 },
922 ) => Promise<Profil>;
923 /** Löscht ein Profil; liefert die verbleibenden zurück. */
924 readonly profilLoeschen: (id: number) => Promise<Profil[]>;
925
926 readonly lernSitzung: (profilId: number, filter: SitzungsFilter) => Promise<SitzungsFrage[]>;
927 readonly lernAntworten: (profilId: number, protokoll: Antwortprotokoll) => Promise<FrageStand>;
928 readonly lernMerken: (profilId: number, frageId: string, gemerkt: boolean) => Promise<FrageStand>;
929 /**
930 * Der gespeicherte Stand einer einzelnen Frage – für den Steckbrief.
931 *
932 * Optional geführt wie die übrigen jüngeren Kanäle: Fehlt er, entfällt der
933 * Steckbrief, und die Sitzung läuft wie bisher.
934 */
935 readonly lernFragestand?: (profilId: number, frageId: string) => Promise<FrageStand>;
936 /** Die hartnäckigen Fragen – wiederholt danebengegangen. */
937 readonly lernHartnaeckige?: (profilId: number) => Promise<HartnaeckigeFrage[]>;
938 readonly lernUebersicht: (profilId: number) => Promise<Lernuebersicht>;
939 /** Der nachgerechnete Reifegrad-Verlauf; wahlfrei, weil erst ab 0.27.0. */
940 readonly lernVerlauf?: (profilId: number, tage?: number) => Promise<Verlaufspunkt[]>;
941 readonly lernPlan: (profilId: number) => Promise<Lernplan>;
942 readonly lernZuruecksetzen: (
943 profilId: number,
944 kapitel?: string | null,
945 ) => Promise<Lernuebersicht>;
946
947 /** Liefert den Bogen samt der Abweichungen, die beim Ziehen nötig waren. */
948 readonly pruefungStarten: (
949 profilId: number,
950 auftrag: Pruefungsauftrag,
951 ) => Promise<Pruefungsbogen>;
952 readonly pruefungAuswerten: (
953 profilId: number,
954 auftrag: Pruefungsauftrag,
955 antworten: Pruefungsantwort[],
956 dauerMs: number,
957 zeitAbgelaufen: boolean,
958 ) => Promise<Pruefungsergebnis>;
959 readonly pruefungVerlauf: (profilId: number) => Promise<Pruefungsverlauf[]>;
960 /** Sichert den Zwischenstand; `false` heißt „dieser Lauf ist vorbei". */
961 readonly pruefungSichern: (profilId: number, stand: Zwischenstand) => Promise<boolean>;
962 /** Der unterbrochene Lauf eines Profils, oder `null`. */
963 readonly pruefungOffen: (profilId: number) => Promise<OffenerLauf | null>;
964 readonly pruefungVerwerfen: (profilId: number) => Promise<void>;
965 /** Meldet dem Hauptprozess, ob gerade geprüft wird. */
966 readonly pruefungLaeuft: (laeuft: boolean) => Promise<void>;
967
968 /**
969 * Setzt die Anzeigegröße in Prozent; liefert die tatsächlich gesetzte
970 * Stufe zurück (der Kern begrenzt auf gültige Werte).
971 */
972 readonly anzeigegroesseSetzen: (prozent: number) => Promise<number>;
973 /**
974 * Meldet, wenn die Anzeigegröße anderswo geändert wurde – über das Menü
975 * oder Strg+Plus. Ohne diese Meldung zeigte die Einstellung eine Zahl an,
976 * die nicht mehr stimmt.
977 */
978 readonly anzeigegroesseBeobachten: (melden: (prozent: number) => void) => () => void;
979
980 /**
981 * Nimmt ein Profil aus der geprüften Datei dazu, ohne etwas zu ersetzen.
982 *
983 * `profilIndex` zeigt in die Liste, die `sicherungPruefen` geliefert hat.
984 */
985 readonly sicherungUebernehmen: (
986 vorgang: string,
987 profilIndex: number,
988 ) => Promise<Uebernahmeergebnis>;
989
990 /** Erzeugt das Fehlerprotokoll als PDF und fragt nach dem Speicherort. */
991 readonly fehlerprotokollDrucken: (
992 profilId: number,
993 schriftgroesse: Schriftgroesse,
994 tiefe: Erklaerungstiefe,
995 ) => Promise<Druckergebnis>;
996
997 /** Erzeugt die Fragenliste als PDF und fragt nach dem Speicherort. */
998 readonly fragenlisteDrucken: (
999 bereiche: readonly string[],
1000 schriftgroesse: Schriftgroesse,
1001 mitLoesungen: boolean,
1002 ) => Promise<Druckergebnis>;
1003
1004 /** Erzeugt den Lernbericht als PDF und fragt nach dem Speicherort. */
1005 readonly lernberichtDrucken: (
1006 profilId: number,
1007 schriftgroesse: Schriftgroesse,
1008 ) => Promise<Druckergebnis>;
1009
1010 readonly lizenzenLesen: () => Promise<Lizenzangaben>;
1011
1012 readonly datenschutzLesen: () => Promise<Datenschutzangaben>;
1013
1014 readonly hilfsmittelAktiv: () => Promise<boolean>;
1015 /**
1016 * Legt Text in die Zwischenablage. Siehe Kanal `system:kopieren`.
1017 *
1018 * Optional geführt wie die übrigen jüngeren Kanäle: Die Oberfläche muss
1019 * ohne ihn auskommen, und die Attrappen der Oberflächentests tragen das
1020 * volle Interface. Fehlt er, entfällt der Kopierknopf – der Meldetext bleibt
1021 * sichtbar und markierbar.
1022 */
1023 readonly zwischenablageSchreiben?: (text: string) => Promise<boolean>;
1024
1025 /**
1026 * Öffnet die Unterstützungsseite im Standardbrowser. Siehe Kanal
1027 * `system:unterstuetzung`.
1028 *
1029 * Optional geführt wie die übrigen jüngeren Kanäle. Fehlt er, lässt die
1030 * Ansicht „Über diese Software“ das Angebot **vollständig** weg – ein Knopf,
1031 * der nichts öffnen kann, wäre eine Zusage ohne Deckung.
1032 */
1033 readonly unterstuetzungOeffnen?: (url: string) => Promise<boolean>;
1034
1035 /**
1036 * Öffnet eine soeben geschriebene Datei oder zeigt sie im Ordner.
1037 *
1038 * Optional geführt wie die übrigen jüngeren Kanäle: Fehlt er, entfallen
1039 * die beiden Schaltflächen nach einem Export – gespeichert ist die Datei
1040 * dann trotzdem, und ihr Pfad steht auf dem Bildschirm.
1041 */
1042 readonly dateiZeigen?: (kennung: string, wunsch: 'oeffnen' | 'ordner') => Promise<boolean>;
1043 /**
1044 * Öffnet den Ordner der selbsttätigen Sicherheitskopien.
1045 *
1046 * Ohne Argument: Welcher Ordner das ist, weiß allein der Hauptprozess.
1047 */
1048 readonly sicherungsordnerZeigen?: () => Promise<boolean>;
1049
1050 /* Optional geführt wie die übrigen jüngeren Kanäle: Die Oberfläche muss
1051 ohne sie auskommen, und die Attrappen der Tests tragen das volle
1052 Interface. Fehlen sie, entfällt die Karte zur Sicherung. */
1053 readonly sicherungAnlegen?: () => Promise<Sicherungsergebnis>;
1054 readonly sicherungPruefen?: () => Promise<Pruefergebnis>;
1055 readonly sicherungEinspielen?: (vorgang: string) => Promise<Einspielergebnis>;
1056 /**
1057 * Meldet Änderungen des Hilfsmittel-Zustands. Liefert eine Funktion zum
1058 * Abmelden zurück, damit React beim Abbau aufräumen kann.
1059 */
1060 readonly hilfsmittelBeobachten: (melden: (aktiv: boolean) => void) => () => void;
1061
1062 /**
1063 * Meldet Befehle aus der Menüleiste. Liefert eine Funktion zum Abmelden
1064 * zurück, damit React beim Abbau aufräumen kann.
1065 */
1066 readonly menuebefehlBeobachten: (melden: (befehl: Menuebefehl) => void) => () => void;
1067
1068 /**
1069 * Meldet nach dem Aufwachen, wie lange der Rechner geschlafen hat.
1070 *
1071 * Optional geführt wie die übrigen jüngeren Kanäle: Fehlt er, läuft die
1072 * Simulation wie bisher – nur ohne Gutschrift.
1073 */
1074 readonly energiesparenBeobachten?: (melden: (schlafMs: number) => void) => () => void;
1075 }