waffensachkunde

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

/ app src shared ipc.ts

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