waffensachkunde

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

/ app src main ipc.ts

19,8 KB Rohdatei
app/src/main/ipc.ts — 540 Zeilen
1 /**
2 * Registrierung der IPC-Handler – ausschließlich über den typisierten Vertrag
3 * aus `src/shared/ipc.ts`.
4 *
5 * Die Typisierung beschreibt, was der Renderer schicken SOLL. Was er
6 * tatsächlich schickt, ist an dieser Grenze unbekannt: die Nutzlasten werden
7 * deshalb in den Fachmodulen (`katalog.ts`, `lernstand.ts`) zur Laufzeit
8 * geprüft, bevor irgendetwas damit geschieht.
9 */
10
11 import { join } from 'node:path';
12
13 import { app, BrowserWindow, clipboard, dialog, ipcMain, type IpcMainInvokeEvent } from 'electron';
14
15 import {
16 IPC_KANAELE,
17 KANAL_HILFSMITTEL_GEAENDERT,
18 type AnwendungsInfo,
19 type IpcAnfrage,
20 type IpcAntwort,
21 type IpcKanal,
22 type Katalogwechsel,
23 } from '../shared/ipc';
24 import { anzeigegroesseSetzen } from './anzeige';
25 import { dateiZeigen } from './dateizugriff';
26 import { datenbankStatus } from './datenbank';
27 import { einstellungenLesen, einstellungenSchreiben } from './einstellungen';
28 import { erklaerungenLaden } from './erklaerungen';
29 import { glossarLaden } from './glossar';
30 import { normtexteLaden } from './normtexte';
31 import { themenLaden } from './themen';
32 import { katalogBild, katalogLaden } from './katalog';
33 import { fehlerprotokollExportieren, fragenlisteExportieren } from './fragendruck-export';
34 import { lernberichtExportieren } from './lernbericht-export';
35 import {
36 datenbankKonstruktor,
37 istBeschaedigt,
38 lernstandBeiseitelegen,
39 lernstandInstanz,
40 lernstandOffen,
41 lernstandSchliessen,
42 type Lernstand,
43 } from './lernstand';
44 import { protokollPfad } from './protokoll';
45 import { selbstsicherungAnlegen } from './selbstsicherung';
46 import { datenschutzLesen } from './datenschutz';
47 import { lizenzenLesen } from './lizenzen';
48 import { pruefungInstanz, pruefungZuruecksetzen, type Pruefung } from './pruefung';
49 import {
50 sicherungAnlegen,
51 sicherungEinspielen,
52 sicherungUebernehmen,
53 sicherungPruefen,
54 type Umgebung as Sicherungsumgebung,
55 } from './sicherung-dialoge';
56 import { laufMelden } from './laufwaechter';
57 import { istErlaubteNavigation, unterstuetzungOeffnen } from './sicherheit';
58
59 /** Dateiname des Lernstands im `userData`-Verzeichnis. */
60 const LERNSTAND_DATEI = 'lernstand.db';
61
62 /**
63 * Anwendungsversion, zur Bauzeit aus der package.json eingesetzt
64 * (siehe `electron.vite.config.ts`).
65 *
66 * `app.getVersion()` taugt dafür nicht durchgängig: Wird der gebaute
67 * Einstiegspunkt direkt gestartet – in der Entwicklung und in den
68 * Oberflächentests – findet Electron keine package.json und liefert
69 * ersatzweise seine eigene Version zurück.
70 */
71 declare const __ANWENDUNGS_VERSION__: string | undefined;
72
73 function anwendungsVersion(): string {
74 return typeof __ANWENDUNGS_VERSION__ === 'string' ? __ANWENDUNGS_VERSION__ : app.getVersion();
75 }
76
77 /**
78 * Baukennung und Commit-Datum, ebenfalls zur Bauzeit eingesetzt.
79 *
80 * Ohne Git beim Bauen bleiben beide leer; die Oberfläche lässt die Zeile
81 * dann weg, statt „unbekannt“ anzuzeigen.
82 */
83 declare const __BAUKENNUNG__: string | undefined;
84 declare const __BAUSTAND__: string | undefined;
85
86 function zeichenkette(wert: string | undefined): string {
87 return typeof wert === 'string' ? wert : '';
88 }
89
90 /**
91 * Stammt die Anfrage aus unserer eigenen Oberfläche?
92 *
93 * Zweite Verteidigungslinie hinter der Navigationssperre in `sicherheit.ts`.
94 * Käme eine fremde Seite doch einmal in den Renderer, hätte sie über das
95 * Preload-Skript die volle Bridge in der Hand – hier endet das trotzdem.
96 *
97 * Ein Rahmen ohne URL wird durchgelassen: Das kommt vor, wenn der Rahmen
98 * gerade abgebaut wird, und wäre kein Angriff, sondern ein Wettlauf.
99 */
100 function vonEigenerOberflaeche(ereignis: IpcMainInvokeEvent): boolean {
101 const herkunft = ereignis.senderFrame?.url;
102 return herkunft === undefined || herkunft === '' || istErlaubteNavigation(herkunft);
103 }
104
105 /**
106 * Typsichere Hülle um `ipcMain.handle`: Kanal, Anfrage- und Antworttyp werden
107 * aus {@link IpcVertrag} abgeleitet, sodass Tippfehler zu Compilerfehlern
108 * werden statt zu stillen `undefined`-Antworten.
109 */
110 function behandeln<K extends IpcKanal>(
111 kanal: K,
112 /* Auch ein Versprechen ist erlaubt: `ipcMain.handle` wartet es ab. Gebraucht
113 wird das vom Export, der einen Dialog zeigt und danach schreibt. */
114 behandler: (
115 ereignis: IpcMainInvokeEvent,
116 anfrage: IpcAnfrage<K>,
117 ) => IpcAntwort<K> | Promise<IpcAntwort<K>>,
118 ): void {
119 ipcMain.handle(kanal, (ereignis, anfrage: unknown) => {
120 if (!vonEigenerOberflaeche(ereignis)) {
121 console.warn(`[sicherheit] IPC-Anfrage fremder Herkunft abgewiesen: ${kanal}`);
122 throw new Error('Diese Anfrage stammt nicht aus der Oberfläche dieser Anwendung.');
123 }
124 return behandler(ereignis, anfrage as IpcAnfrage<K>);
125 });
126 }
127
128 /**
129 * Holt ein Feld aus einer IPC-Nutzlast. Trotz Typisierung wird hier nichts
130 * vorausgesetzt – die eigentliche Prüfung der Werte macht das Fachmodul.
131 */
132 function feld(anfrage: unknown, name: string): unknown {
133 if (typeof anfrage !== 'object' || anfrage === null || Array.isArray(anfrage)) {
134 throw new Error(`Ungültige Anfrage: Es wurde ein Objekt mit dem Feld „${name}“ erwartet.`);
135 }
136 return (anfrage as Record<string, unknown>)[name];
137 }
138
139 /**
140 * Befund des Katalogstand-Abgleichs. Der Abgleich selbst läuft beim Öffnen
141 * des Lernstands; hier wird er nur abgeholt. Scheitert das Öffnen – etwa bei
142 * einem Lernstand aus einer neueren Programmversion oder ohne Katalog –,
143 * antwortet die Systemauskunft trotzdem: Diese Fehler haben ihre eigenen
144 * Meldungen an anderer Stelle.
145 */
146 function katalogwechselLesen(): Katalogwechsel | null {
147 try {
148 return lernstand().katalogwechsel;
149 } catch {
150 return null;
151 }
152 }
153
154 function anwendungsInfo(): AnwendungsInfo {
155 return {
156 anwendungsVersion: anwendungsVersion(),
157 baukennung: zeichenkette(__BAUKENNUNG__),
158 baustand: zeichenkette(__BAUSTAND__),
159 electronVersion: process.versions.electron,
160 chromeVersion: process.versions.chrome,
161 nodeVersion: process.versions.node,
162 plattform: process.platform,
163 datenbank: datenbankStatus(),
164 katalogwechsel: katalogwechselLesen(),
165 protokollPfad: protokollPfad(),
166 };
167 }
168
169 /**
170 * Lernstand beim ersten Bedarf öffnen. Der Katalog wird dabei mitgeladen –
171 * ohne ihn lassen sich weder Fragen zusammenstellen noch Statistiken bilden.
172 *
173 * **Der eine Fall, in dem es hier etwas anzubieten gibt.** Ist die Datei
174 * beschädigt, endete das bis 0.22.0 in einer Sackgasse: Der Renderer meldete
175 * „Ihr Lernprofil konnte nicht geladen werden … Antworten werden dabei nicht
176 * gespeichert“, und dabei blieb es bei jedem Start aufs Neue. Wer nicht von
177 * sich aus auf die Idee kam, im `userData`-Verzeichnis nachzusehen, konnte
178 * die Anwendung nie wieder benutzen.
179 *
180 * Deshalb fragt der Hauptprozess hier nach. Der Dialog ist bewusst
181 * synchron und nativ: Er muss vorliegen, bevor dieser Aufruf zurückkehrt,
182 * und er wird auch dann noch vorgelesen, wenn die Oberfläche nichts anzeigen
183 * kann. Nichts wird von selbst getan – die beschädigte Datei bleibt liegen,
184 * bis jemand ausdrücklich zustimmt, und auch dann wird sie nur umbenannt.
185 */
186 function lernstand(): Lernstand {
187 const pfad = join(app.getPath('userData'), LERNSTAND_DATEI);
188 try {
189 return lernstandInstanz(pfad, katalogLaden());
190 } catch (fehler: unknown) {
191 if (!istBeschaedigt(fehler)) {
192 throw fehler;
193 }
194 return beschaedigtenLernstandKlaeren(pfad, fehler);
195 }
196 }
197
198 /**
199 * Bietet nach einer beschädigten Datei den Neuanfang an – einmal je Start.
200 *
201 * Der Merker verhindert eine Dialogschleife: Jeder IPC-Kanal, der einen
202 * Lernstand braucht, liefe sonst in dieselbe Rückfrage, und der Startbildschirm
203 * fragt mehrere davon gleich nacheinander ab.
204 */
205 let beschaedigungGeklaert = false;
206
207 function beschaedigtenLernstandKlaeren(pfad: string, fehler: unknown): Lernstand {
208 if (beschaedigungGeklaert) {
209 throw fehler;
210 }
211 beschaedigungGeklaert = true;
212
213 const wahl = dialog.showMessageBoxSync({
214 type: 'error',
215 buttons: ['Beiseitelegen und neu anfangen', 'Nichts tun'],
216 defaultId: 1,
217 cancelId: 1,
218 noLink: true,
219 title: 'Waffensachkunde – Lernsoftware',
220 message: 'Die Datei mit Ihrem Lernstand ist beschädigt.',
221 detail: [
222 'Sie lässt sich nicht mehr öffnen. Gelöscht wird nichts: Auf Wunsch wird die beschädigte Datei umbenannt und beiseitegelegt, und die Anwendung beginnt mit einem leeren Lernstand.',
223 'Haben Sie eine Sicherung, können Sie sie danach unter „Lernstand sichern und übertragen“ einspielen.',
224 `Die beschädigte Datei bleibt in jedem Fall erhalten unter:\n${pfad}`,
225 ].join('\n\n'),
226 });
227
228 if (wahl !== 0) {
229 throw fehler;
230 }
231
232 const beiseite = lernstandBeiseitelegen(pfad);
233 console.warn(`[lernstand] Beschädigte Datei beiseitegelegt: ${beiseite}`);
234 return lernstandInstanz(pfad, katalogLaden());
235 }
236
237 /**
238 * Die Prüfungssimulation arbeitet auf demselben Lernstand: ein Lauf soll in
239 * Statistik und Wiedervorlage einfließen, nicht daneben stehen.
240 */
241 function pruefung(): Pruefung {
242 return pruefungInstanz(lernstand(), katalogLaden());
243 }
244
245 export function ipcRegistrieren(): void {
246 behandeln('anwendung:info', () => anwendungsInfo());
247 behandeln('einstellungen:lesen', () => einstellungenLesen());
248 behandeln('einstellungen:schreiben', (_ereignis, einstellungen) =>
249 einstellungenSchreiben(einstellungen),
250 );
251
252 /* Electron meldet über `accessibilitySupportEnabled`, ob eine assistive
253 Technologie erkannt wurde. Die Oberfläche richtet danach die Voreinstellung
254 der Zeichenkürzel aus – sie kollidieren im Lesemodus von NVDA und JAWS mit
255 deren Schnellnavigation. */
256 behandeln('system:hilfsmittel', () => app.accessibilitySupportEnabled);
257
258 /*
259 Die Zwischenablage liegt im Hauptprozess, weil der Renderer sie nicht
260 bekommt: `sicherheit.ts` lehnt sämtliche Berechtigungen ab, und Blink
261 fragt für `navigator.clipboard.writeText()` eine ab. Nachgemessen im
262 gebauten Fenster – `isSecureContext` true, Aufruf trotzdem
263 `NotAllowedError: Write permission denied`.
264
265 Die Obergrenze ist kein Misstrauen gegen die eigene Oberfläche, sondern
266 dieselbe Sorgfalt wie bei jeder anderen Nutzlast: Was über die Brücke
267 kommt, wird geprüft, bevor es wirkt. Der Meldeblock ist rund 500 Zeichen
268 lang; 20 000 lassen jede vorstellbare Erweiterung zu und schließen aus,
269 dass ein Fehler anderswo die Zwischenablage des Nutzers mit Unrat füllt.
270 */
271 behandeln('system:kopieren', (_ereignis, anfrage) => {
272 const text = feld(anfrage, 'text');
273 if (typeof text !== 'string' || text.length === 0 || text.length > 20_000) {
274 return false;
275 }
276 clipboard.writeText(text);
277 return true;
278 });
279
280 /*
281 Der einzige Kanal, der nach außen führt. Er nimmt eine Adresse entgegen,
282 damit prüfbar bleibt, dass die Oberfläche genau die anfordert, die sie
283 anzeigt – geöffnet wird aber ausschließlich die eine eingetragene, siehe
284 `sicherheit.ts`. Ohne Eintrag öffnet er nichts, und die Oberfläche zeigt
285 das Angebot dann gar nicht erst an.
286 */
287 behandeln('system:unterstuetzung', (_ereignis, anfrage) =>
288 unterstuetzungOeffnen(feld(anfrage, 'url')),
289 );
290
291 /*
292 Öffnet ausschließlich, was dieser Prozess in dieser Sitzung selbst
293 geschrieben hat. Der Renderer schickt eine Kennung, keinen Pfad – wer
294 einen Pfad mitbringen darf, darf auch einen anderen mitbringen.
295 */
296 behandeln('system:datei-zeigen', async (_ereignis, anfrage) => {
297 const wunsch = feld(anfrage, 'wunsch');
298 return dateiZeigen(feld(anfrage, 'kennung'), wunsch === 'ordner' ? 'ordner' : 'oeffnen');
299 });
300
301 behandeln('ansicht:groesse', (_ereignis, anfrage) =>
302 anzeigegroesseSetzen(feld(anfrage, 'prozent')),
303 );
304
305 behandeln('katalog:laden', () => katalogLaden());
306 behandeln('katalog:bild', (_ereignis, anfrage) => katalogBild(feld(anfrage, 'bildId')));
307 behandeln('erklaerungen:laden', () => erklaerungenLaden());
308 behandeln('glossar:laden', () => glossarLaden());
309 behandeln('gesetz:normtexte', () => normtexteLaden());
310 behandeln('themen:laden', () => themenLaden());
311
312 behandeln('profil:liste', () => lernstand().profile());
313 behandeln('profil:anlegen', (_ereignis, anfrage) =>
314 lernstand().profilAnlegen(feld(anfrage, 'name')),
315 );
316 behandeln('profil:aktualisieren', (_ereignis, anfrage) =>
317 lernstand().profilAktualisieren(anfrage),
318 );
319 behandeln('profil:loeschen', (_ereignis, anfrage) =>
320 lernstand().profilLoeschen(feld(anfrage, 'id')),
321 );
322
323 behandeln('lernen:sitzung', (_ereignis, anfrage) =>
324 lernstand().sitzung(feld(anfrage, 'profilId'), feld(anfrage, 'filter')),
325 );
326 behandeln('lernen:antworten', (_ereignis, anfrage) =>
327 lernstand().antworten(feld(anfrage, 'profilId'), feld(anfrage, 'protokoll')),
328 );
329 behandeln('lernen:merken', (_ereignis, anfrage) =>
330 lernstand().merken(
331 feld(anfrage, 'profilId'),
332 feld(anfrage, 'frageId'),
333 feld(anfrage, 'gemerkt'),
334 ),
335 );
336 behandeln('lernen:fragestand', (_ereignis, anfrage) =>
337 lernstand().frageStand(feld(anfrage, 'profilId'), feld(anfrage, 'frageId')),
338 );
339 behandeln('lernen:hartnaeckige', (_ereignis, anfrage) =>
340 lernstand().hartnaeckige(feld(anfrage, 'profilId')),
341 );
342 behandeln('lernen:uebersicht', (_ereignis, anfrage) =>
343 lernstand().uebersicht(feld(anfrage, 'profilId')),
344 );
345 behandeln('lernen:plan', (_ereignis, anfrage) => lernstand().lernplan(feld(anfrage, 'profilId')));
346 behandeln('lernen:zuruecksetzen', (_ereignis, anfrage) =>
347 lernstand().zuruecksetzen(feld(anfrage, 'profilId'), feld(anfrage, 'kapitel')),
348 );
349
350 /*
351 Sicherung des Lernstands. Drei Kanäle, weil zwischen „das steht in der
352 Datei" und „ja, ersetze meinen Lernstand" eine Entscheidung des Nutzers
353 liegt: Der Prüfschritt ist folgenlos, erst `einspielen` fasst etwas an.
354
355 Die Umgebung wird bei jedem Aufruf frisch gebaut – `lernstand()` muss beim
356 Einspielen NACH dem Ersetzen eine neue Instanz liefern, und ein
357 festgehaltener Verweis zeigte auf die geschlossene alte.
358 */
359 const sicherungsumgebung = (): Sicherungsumgebung => ({
360 lernstand,
361 katalog: katalogLaden,
362 Datenbank: datenbankKonstruktor(),
363 jetzt: () => new Date(),
364 });
365
366 behandeln('sicherung:anlegen', async (ereignis) =>
367 sicherungAnlegen(BrowserWindow.fromWebContents(ereignis.sender), sicherungsumgebung()),
368 );
369
370 behandeln('sicherung:pruefen', async (ereignis) =>
371 sicherungPruefen(BrowserWindow.fromWebContents(ereignis.sender), sicherungsumgebung()),
372 );
373
374 behandeln('sicherung:einspielen', (ereignis, anfrage) =>
375 sicherungEinspielen(
376 feld(anfrage, 'vorgang'),
377 BrowserWindow.fromWebContents(ereignis.sender),
378 sicherungsumgebung(),
379 ),
380 );
381
382 /* Ohne `async`, wie das Einspielen: Der Hauptprozess ist einfädig, und
383 solange nichts abgewartet wird, kann sich kein zweiter Kanal
384 dazwischenschieben und in derselben Datenbank schreiben. */
385 behandeln('sicherung:uebernehmen', (ereignis, anfrage) =>
386 sicherungUebernehmen(
387 feld(anfrage, 'vorgang'),
388 feld(anfrage, 'profilIndex'),
389 BrowserWindow.fromWebContents(ereignis.sender),
390 sicherungsumgebung(),
391 ),
392 );
393
394 /* Der Speicherdialog hängt am Fenster, aus dem der Auftrag kam – modal ist
395 für Screenreader die klarere Lage als ein freischwebender Dialog. */
396 behandeln('druck:lernbericht', async (ereignis, anfrage) =>
397 lernberichtExportieren(
398 {
399 lernstand: lernstand(),
400 pruefung: pruefung(),
401 katalog: katalogLaden(),
402 elternfenster: BrowserWindow.fromWebContents(ereignis.sender),
403 jetzt: () => new Date(),
404 },
405 feld(anfrage, 'profilId'),
406 feld(anfrage, 'schriftgroesse'),
407 ),
408 );
409
410 behandeln('druck:fehlerprotokoll', async (ereignis, anfrage) =>
411 fehlerprotokollExportieren(
412 {
413 lernstand: lernstand(),
414 katalog: katalogLaden(),
415 elternfenster: BrowserWindow.fromWebContents(ereignis.sender),
416 jetzt: () => new Date(),
417 },
418 feld(anfrage, 'profilId'),
419 feld(anfrage, 'schriftgroesse'),
420 feld(anfrage, 'tiefe'),
421 ),
422 );
423
424 behandeln('druck:fragenliste', async (ereignis, anfrage) =>
425 fragenlisteExportieren(
426 {
427 lernstand: lernstand(),
428 katalog: katalogLaden(),
429 elternfenster: BrowserWindow.fromWebContents(ereignis.sender),
430 jetzt: () => new Date(),
431 },
432 feld(anfrage, 'bereiche'),
433 feld(anfrage, 'schriftgroesse'),
434 feld(anfrage, 'mitLoesungen'),
435 ),
436 );
437
438 behandeln('pruefung:starten', (_ereignis, anfrage) =>
439 pruefung().starten(feld(anfrage, 'profilId'), feld(anfrage, 'auftrag')),
440 );
441 behandeln('pruefung:auswerten', (_ereignis, anfrage) =>
442 pruefung().auswerten(
443 feld(anfrage, 'profilId'),
444 feld(anfrage, 'auftrag'),
445 feld(anfrage, 'antworten'),
446 feld(anfrage, 'dauerMs'),
447 feld(anfrage, 'zeitAbgelaufen'),
448 ),
449 );
450 behandeln('pruefung:sichern', (_ereignis, anfrage) =>
451 pruefung().sichern(feld(anfrage, 'profilId'), feld(anfrage, 'stand')),
452 );
453 behandeln('pruefung:offen', (_ereignis, anfrage) =>
454 pruefung().offenerBogen(feld(anfrage, 'profilId')),
455 );
456 behandeln('pruefung:verwerfen', (_ereignis, anfrage) => {
457 pruefung().verwerfen(feld(anfrage, 'profilId'));
458 return undefined;
459 });
460 behandeln('pruefung:laeuft', (ereignis, anfrage) => {
461 /* An den sendenden `webContents` gebunden, nicht an ein Modul-Merkmal:
462 Sonst spräche ein zweites Fenster für das erste mit. */
463 const laeuft = feld(anfrage, 'laeuft');
464 laufMelden(ereignis.sender, laeuft === true);
465 return undefined;
466 });
467 behandeln('pruefung:verlauf', (_ereignis, anfrage) =>
468 pruefung().verlauf(feld(anfrage, 'profilId')),
469 );
470
471 behandeln('lizenzen:lesen', () => lizenzenLesen());
472 behandeln('datenschutz:lesen', () => datenschutzLesen());
473 }
474
475 /**
476 * Meldet Änderungen des Hilfsmittel-Zustands an alle Fenster.
477 *
478 * Ein Screenreader kann auch mitten in der Sitzung gestartet werden – gerade
479 * dann, wenn jemand merkt, dass er ihn braucht. Ohne diese Meldung bliebe die
480 * Oberfläche auf dem Stand des Programmstarts.
481 */
482 function hilfsmittelMelden(_ereignis: unknown, aktiv: boolean): void {
483 for (const fenster of BrowserWindow.getAllWindows()) {
484 if (!fenster.isDestroyed()) {
485 fenster.webContents.send(KANAL_HILFSMITTEL_GEAENDERT, aktiv);
486 }
487 }
488 }
489
490 export function hilfsmittelBeobachten(): void {
491 /* Benannt und nicht anonym, damit `ipcAbmelden` ihn wieder lösen kann.
492 Anonym ließ er sich nicht entfernen: `ipcAbmelden` heißt „Gegenstück für
493 Tests und sauberes Herunterfahren“ und hielt das bis Fassung 0.24.1 für
494 diesen einen Zuhörer nicht ein. In der Auslieferung folgenlos – der
495 Aufruf geschieht genau einmal –, aber ein Gegenstück, das nicht alles
496 zurücknimmt, ist beim nächsten Mal die Falle. */
497 app.on('accessibility-support-changed', hilfsmittelMelden);
498 }
499
500 /**
501 * Legt beim Beenden eine Sicherheitskopie an, wenn eine fällig ist.
502 *
503 * **Vor** dem Schließen: `VACUUM INTO` braucht die offene Verbindung. Und
504 * nur, wenn der Lernstand in dieser Sitzung überhaupt geöffnet wurde – wer
505 * das Programm startet und gleich wieder beendet, hat nichts zu sichern.
506 *
507 * Ein Fehlschlag hält das Beenden nicht auf. Eine Anwendung, die sich wegen
508 * einer misslungenen Nebensache nicht schließen lässt, wäre der schlechtere
509 * Handel; die Kopie ist eine Zugabe, kein Versprechen.
510 */
511 function selbstsicherungBeimBeenden(): void {
512 const offen = lernstandOffen();
513 if (offen === null) {
514 return;
515 }
516
517 try {
518 const pfad = selbstsicherungAnlegen(
519 offen.datenbank,
520 app.getPath('userData'),
521 datenbankKonstruktor(),
522 );
523 if (pfad !== null) {
524 console.info(`[sicherung] Selbsttätige Sicherheitskopie: ${pfad}`);
525 }
526 } catch (fehler: unknown) {
527 console.warn('[sicherung] Selbsttätige Sicherheitskopie misslang:', fehler);
528 }
529 }
530
531 /** Gegenstück für Tests und sauberes Herunterfahren. */
532 export function ipcAbmelden(): void {
533 for (const kanal of IPC_KANAELE) {
534 ipcMain.removeHandler(kanal);
535 }
536 app.off('accessibility-support-changed', hilfsmittelMelden);
537 pruefungZuruecksetzen();
538 selbstsicherungBeimBeenden();
539 lernstandSchliessen();
540 }