# Waffensachkunde – Lernsoftware (Anwendungsgerüst) Barrierefreie Desktop-Lernsoftware zur Vorbereitung auf die Waffensachkundeprüfung. Läuft vollständig offline; es werden keine Daten übertragen. Dieses Verzeichnis enthält das **Anwendungsgerüst**; die Lerninhalte kommen aus einem anderen Arbeitsstrang (`content/`, `data-pipeline/`). Das Datenbankschema gehört dagegen hierher: Grundschema und alle Migrationsschritte stehen in `src/main/schema.ts` (siehe den Baum unten). Bis Fassung 0.27.2 nahm dieser Satz es mit aus — wer es suchte, suchte unter `content/` und `data-pipeline/`, wo keine einzige Schemadefinition liegt. --- ## Technischer Stack | Baustein | Version | Zweck | | ----------------- | -------------- | ---------------------------------------- | | Electron | 43.4.1 | Desktop-Laufzeit | | React | 19.2.8 | Oberfläche im Renderer-Prozess | | Vite | 7.3.6 | Bundler | | electron-vite | 5.0.0 | Bindeglied Vite ↔ Electron | | TypeScript | 6.0.3 | `strict` plus zusätzliche Verschärfungen | | better-sqlite3 | 13.0.3 | Lokale Datenbank des Lernstands | | electron-builder | 26.15.3 | Paketierung (NSIS, DMG) | | Vitest | 4.1.11 | Unit-Tests | | Playwright | 1.62.1 | E2E-Tests gegen die echte Anwendung | | axe-core | 4.13.0 | Automatisierte Barrierefreiheitsprüfung | | ESLint / Prettier | 9.39.5 / 3.9.6 | Statische Analyse und Formatierung | --- ## Verzeichnisstruktur ``` app/ ├── src/ │ ├── main/ Main-Prozess (Node, voller Systemzugriff) – 32 Dateien │ │ ├── index.ts Einstiegspunkt, Lebenszyklus der Anwendung │ │ ├── fenster.ts Hauptfenster inkl. gehärteter webPreferences │ │ ├── sicherheit.ts CSP-Header, Berechtigungssperren, Navigationsschutz │ │ ├── menue.ts Deutschsprachiges Anwendungsmenü │ │ ├── ipc.ts Registrierung der typisierten IPC-Handler │ │ ├── einstellungen.ts Nutzereinstellungen (JSON in userData) │ │ ├── datenbank.ts Rauchtest des nativen SQLite-Moduls (In-Memory) │ │ ├── schema.ts Grundschema und die neun Migrationsschritte │ │ ├── lernstand.ts Der Anwendungskern: Profile, Antworten, Sitzung, Plan │ │ ├── pruefung.ts Prüfungssimulation: Bogen, Auswertung, Verlauf │ │ ├── katalog.ts, Laden und Prüfen der Inhalte aus content/ │ │ │ erklaerungen.ts, │ │ │ glossar.ts, │ │ │ normtexte.ts, │ │ │ themen.ts │ │ ├── sicherung.ts, Sichern und Einspielen des Lernstands │ │ │ sicherung-dialoge.ts, │ │ │ selbstsicherung.ts, │ │ │ profil-uebernehmen.ts │ │ └── druck.ts, PDF-Ausgabe: Lernbericht, Fehlerprotokoll, Fragenliste │ │ dokument-export.ts, │ │ fragendruck-export.ts, │ │ lernbericht-export.ts │ │ │ ├── preload/ Die EINZIGE Brücke zwischen Main und Renderer │ │ ├── index.ts contextBridge-Freigabe der typisierten API │ │ └── index.d.ts Deklaration von `window.lernApp` │ │ │ ├── renderer/ React-Oberfläche (Sandbox, kein Node-Zugriff) │ │ ├── index.html lang="de", CSP-Platzhalter, Titel │ │ └── src/ │ │ ├── main.tsx React-Einstiegspunkt │ │ ├── App.tsx Ansichtswahl und die Tore davor │ │ ├── bridge.ts Typisierter Zugriff auf window.lernApp │ │ ├── components/ Bausteine, nach Bereich gegliedert: │ │ │ lernen/ (40), pruefung/ (13), profil/ (3), │ │ │ suche/ (2), ueber/ (4), hilfe/ (1) │ │ ├── hooks/ 29 Haken – Katalog, Lernstand, Sitzung, │ │ │ Prüfung, Thema, Textabstand, Tastenkürzel │ │ ├── lernen/ Bewertung, Sitzungsaufbau, Sprachausgabe │ │ ├── pruefung/ Auftrag, Bogen, Bewertung, Zeit │ │ └── styles/ tokens.css (3 Themes) und sechs Bereichsdateien │ │ │ └── shared/ Von allen drei Prozessen genutzte Typen und Logik │ ├── ipc.ts IPC-Vertrag (44 Kanäle, Anfrage-/Antworttypen) │ ├── fsrs.ts Das Wiedervorlageverfahren │ ├── lernstand.ts, Verträge des Kerns │ │ lernplan.ts, reife.ts │ ├── katalog.ts, Verträge der Inhalte │ │ erklaerungen.ts, glossar.ts, normtexte.ts, themen.ts │ ├── druck/ Die Dokumentbausteine, ohne Electron │ ├── theme.ts Theme-Typen und Auflösungslogik │ └── csp.ts Content-Security-Policy (eine Quelle der Wahrheit) │ ├── tests/ Unit-Tests (Vitest, jsdom) – 79 Testdateien │ ├── setup.ts Test-Setup, matchMedia-Stub │ ├── lernstand.test.ts Der Kern: Profile, Wiedervorlage, Übersicht, Plan │ ├── pruefung.test.ts Bogen, Auswertung, unterbrochener Lauf │ ├── renderer-*.test.tsx Die Oberfläche je Bereich, mit axe-Lauf │ ├── dokumentation.test.ts Fassungsnummern, Changelog, tote Verweise │ └── … Druck, Sicherung, Suche, Glossar, Normtexte, Symbol │ ├── e2e/ E2E-Tests (Playwright gegen echtes Electron) – 23 Prüfdateien │ ├── electron-hilfe.ts Start der gebauten App mit frischem Profil │ ├── axe-hilfe.ts axe-core-Injektion für Electron │ ├── konsolenwache.ts Macht Konsolenfehler der Anwendung rot │ ├── gepackt.spec.ts Rauchtest gegen das gebaute Installationspaket │ └── barrierefreiheit*.spec.ts axe je Ansicht und Farbschema, Landmarken, Fokus │ ├── build-resources/ Programmsymbol und die Grafiken des Store-Pakets ├── tools/ Symbol bauen, Paketstand messen, Gate-Stempel ├── electron.vite.config.ts Build für main / preload / renderer ├── electron-builder.yml Paketierung Windows (NSIS, AppX) und macOS (DMG) ├── eslint.config.mjs ESLint Flat Config ├── playwright.config.ts E2E-Konfiguration ├── vitest.config.ts Unit-Test-Konfiguration samt Abdeckungsschwellen └── tsconfig.*.json Getrennte Projekte für node / web / tests / e2e ``` Nach `npm run build` entsteht zusätzlich `out/` (`out/main`, `out/preload`, `out/renderer`), nach `npm run dist:*` das Verzeichnis `release/`. Beides ist nicht versioniert. --- ## npm-Skripte | Skript | Wirkung | | ---------------------- | -------------------------------------------------------------------------------- | | `npm run dev` | Startet Vite-Dev-Server und Electron mit Hot Reload. | | `npm run build` | Typprüfung, danach Produktionsbundles nach `out/`. | | `npm start` | Zeigt einen bereits gebauten Stand (`electron-vite preview`). | | `npm test` | Unit-Tests einmalig (Vitest). | | `npm run test:watch` | Unit-Tests im Beobachtungsmodus. | | `npm run test:e2e` | Baut die App und führt danach die Playwright-Tests aus. | | `npm run lint` | ESLint über das gesamte Projekt. | | `npm run lint:fix` | ESLint mit automatischer Korrektur. | | `npm run typecheck` | TypeScript für alle vier Teilprojekte (node, web, tests, e2e). | | `npm run format` | Prettier schreibt alle Dateien. | | `npm run format:check` | Prettier prüft ohne zu schreiben (für CI). | | `npm run dist:win` | Windows-Installer (NSIS) nach `release/`. | | `npm run dist:mac` | macOS-Abbild (DMG) nach `release/` – muss auf macOS laufen. | | `npm run rebuild` | Baut native Module für Electron neu. Normalerweise **nicht** nötig, siehe unten. | --- ## Sicherheitsmodell Diese Schalter sind nicht verhandelbar und werden von `tests/sicherheit.test.ts` (Quellprüfung) und `e2e/anwendung.spec.ts` (Verhaltensprüfung) abgesichert: - `contextIsolation: true` – Renderer und Preload laufen in getrennten JS-Kontexten. - `nodeIntegration: false` – im Renderer gibt es kein `require`, `process`, `Buffer`. - `sandbox: true` – der Renderer-Prozess läuft in der Chromium-Sandbox. - `webviewTag: false`, `nodeIntegrationInWorker/-InSubFrames: false`, `webSecurity: true`. Weitere Härtung in `src/main/sicherheit.ts`: alle Berechtigungsanfragen werden abgelehnt, Navigation nach außen wird blockiert, neue Fenster werden nicht in Electron geöffnet, sondern an den Systembrowser übergeben. **Genau ein Kanal führt nach außen**, und er wird immer von Hand angestoßen: `system:unterstuetzung` öffnet die Unterstützungsseite im Standardbrowser (`shell.openExternal`). Der Renderer schickt die Adresse mit, damit prüfbar bleibt, dass die Oberfläche genau die anfordert, die sie anzeigt – geöffnet wird aber ausschließlich die eine Adresse aus `src/shared/unterstuetzung.ts`. Ist dort nichts eingetragen, öffnet dieser Weg nichts, und die Oberfläche zeigt das Angebot gar nicht erst an. Die Anwendung lädt nach wie vor von sich aus nichts: kein `fetch`, kein `net.request`, kein `https.get`. **IPC ausschließlich über `contextBridge`.** Kanäle werden an genau einer Stelle definiert (`src/shared/ipc.ts`, Interface `IpcVertrag`); Anfrage- und Antworttyp werden auf beiden Seiten daraus abgeleitet. Einen neuen Kanal ergänzt man so: 1. Eintrag in `IpcVertrag` und in `IPC_KANAELE` (Allowlist). 2. Handler in `src/main/ipc.ts` über die Hilfsfunktion `behandeln()`. 3. Methode in `LernAppBridge` und Umsetzung in `src/preload/index.ts`. **Content-Security-Policy** steht in `src/shared/csp.ts` und wird zweifach ausgeliefert: als ``-Element im HTML und als HTTP-Kopf aus dem Main-Prozess. Beide greifen auch bei `file://` im gepackten Build, und beide gelten gleichzeitig – ausgewertet wird die Schnittmenge. Im Produktionsbuild gilt `default-src 'none'` ohne jedes `unsafe-*`. Die ``-Fassung lässt `frame-ancestors` bewusst weg: Diese Direktive wirkt ausschließlich im HTTP-Kopf, im ``-Element verwirft Chromium sie mit einem Konsolenfehler bei jedem Start. Dass sie über den Kopf ankommt, misst `e2e/anwendung.spec.ts` im echten Fenster. --- ## Barrierefreiheit Barrierefreiheit ist **Definition of Done**, nicht Nacharbeit. ### Verbindliche Regeln | Bereich | Regel | | ------------ | ----------------------------------------------------------------------- | | Zielniveau | WCAG 2.2 Stufe AA vollständig; AAA als Zielwert, wo erreichbar. | | Textkontrast | Minimum 4,5:1, Zielwert 7:1. | | UI-Kontrast | Ränder, Bedienelemente und Fokusring mindestens 3:1. | | Sprache | `lang="de"` am ``; alle sichtbaren Texte auf Deutsch. | | Tastatur | Jede Funktion ohne Maus bedienbar; sichtbarer `:focus-visible`-Ring. | | Skip-Link | Erster Tabstopp springt zum Hauptinhalt. | | Struktur | Genau eine `

`, keine übersprungenen Ebenen, Landmarken mit Namen. | | Farbe allein | Zustände nie nur über Farbe (Radiogruppe nutzt zusätzlich einen Glyph). | | Klickflächen | Mindestens 44 × 44 px (`--klickflaeche-min`). | | Bewegung | `prefers-reduced-motion: reduce` schaltet alle Übergänge praktisch ab. | | Schriften | Nur systemseitig vorhandene Schriften – keine Webfont-Downloads. | | Widgets | ARIA-Muster strikt nach den ARIA Authoring Practices umsetzen. | ### Themes Drei Themes, definiert als CSS Custom Properties in `src/renderer/src/styles/tokens.css`: **Hell**, **Dunkel** und **Hoher Kontrast**. - Ohne manuelle Auswahl folgt die Oberfläche dem Betriebssystem (`prefers-color-scheme`, `prefers-contrast`). - `forced-colors: active` (Windows-Kontrastdesign) überstimmt alles; die Farbwahl geht dann vollständig an das Betriebssystem (CSS-Systemfarben). - Die manuelle Auswahl (``) wird in `userData` gespeichert und beim nächsten Start wiederhergestellt. Die Auflösungslogik liegt bewusst als reine Funktion in `src/shared/theme.ts` (`themeAufloesen`) und ist damit direkt testbar. Gestaltungsrichtung: ruhig und kontrastreich – zurückhaltende Farben, große klare Typografie (Basisgröße 18 px), viel Weißraum, eine dezente Akzentfarbe (gedämpftes Petrol), ausgelegt auf lange Lernsitzungen. ### Wie geprüft wird Drei Ebenen, die sich ergänzen: 1. **Statisch – `npm run lint`** `eslint-plugin-jsx-a11y` läuft im **strikten** Regelsatz über alle `.tsx`-Dateien. Verstöße sind Fehler, keine Warnungen. 2. **Unit – `npm test`** `tests/ThemaWahl.test.tsx` prüft die ARIA-Struktur der Radiogruppe, die vollständige Tastaturbedienung nach APG (Pfeiltasten, Pos1/Ende, Leertaste, Umlauf, Roving Tabindex) und lässt axe-core über das gerenderte Markup laufen. Farbkontraste sind hier abgeschaltet: jsdom rendert kein CSS. 3. **E2E – `npm run test:e2e`** `e2e/barrierefreiheit.spec.ts` startet die echte, gebaute Electron-App und lässt axe-core gegen WCAG 2.0/2.1/2.2 A und AA laufen. Zusätzlich geprüft: Landmarken, zugängliche Namen der Sektionen und der sichtbare Fokusring bei echter Tastaturnavigation. Der AAA-Zielwert (7:1) wird erhoben und berichtet, aber nicht erzwungen. **Einmal pro Farbschema** misst dagegen `e2e/barrierefreiheit-ansichten.spec.ts`: Sie fährt jede Ansicht durch `hell`, `dunkel` und `hochkontrast`. Den Umbruch bei 400 Prozent Vergrößerung misst `e2e/anzeigegroesse.spec.ts`. Bis Fassung 0.27.2 stand die Zusage „einmal pro Theme“ hier bei `barrierefreiheit.spec.ts` — die setzt nur ein einziges Thema, und zwar für die AAA-Erhebung. Automatisierte Tests decken erfahrungsgemäß etwa ein Drittel der Kriterien ab. Vor jeder Auslieferung zusätzlich manuell prüfen: vollständiger Durchlauf nur mit der Tastatur, ein Screenreader-Durchgang (NVDA unter Windows, VoiceOver unter macOS) und ein Blick auf die Anwendung bei 200 % Systemzoom. --- ## Paketierung `npm run dist:win` erzeugt einen NSIS-Installer, `npm run dist:mac` ein DMG. Beides landet in `release/`. **Code-Signierung ist bewusst deaktiviert** (`identity: null` für macOS, keine Zertifikatsangaben für Windows), weil die Anwendung privat weitergegeben wird. Das ist den Nutzenden zu erklären: - **Windows:** SmartScreen meldet beim ersten Start „Windows hat Ihren PC geschützt“ → „Weitere Informationen“ → „Trotzdem ausführen“. - **macOS:** Gatekeeper blockiert die App. Rechtsklick auf die App → „Öffnen“ → im Dialog erneut „Öffnen“. Alternativ Systemeinstellungen → Datenschutz & Sicherheit → „Dennoch öffnen“. Anwendungssymbole gehören nach `build-resources/` (`icon.ico` für Windows, `icon.icns` oder `icon.png` ab 512 × 512 px für macOS). Ohne eigene Symbole verwendet electron-builder das Electron-Standardsymbol. `npm run dist:mac` muss auf einem Mac laufen – electron-builder kann DMGs nicht unter Windows erzeugen. ### Native Module `better-sqlite3` ab Version 12 liefert **N-API-Prebuilds** mit (`node_modules/better-sqlite3/prebuilds/`). Die sind ABI-stabil und laufen ohne Neubau sowohl unter Node als auch unter Electron. Deshalb steht in `electron-builder.yml` `npmRebuild: false`, und `npm run rebuild` wird im Normalfall nicht gebraucht. Sollte der Startbildschirm dennoch „Datenbank: Fehler“ melden, hilft `npm run rebuild` (setzt Build-Werkzeuge voraus). --- ## Bekannte Einschränkungen - **`@axe-core/playwright` wird nicht verwendet.** Das Paket ruft intern `browserContext.newPage()` auf; Electron beantwortet das mit `Protocol error (Target.createTarget): Not supported`. Stattdessen injiziert `e2e/axe-hilfe.ts` die axe-core-Quelle direkt über `page.evaluate()`. Das ist gleichwertig und lässt die strenge Produktions-CSP unangetastet. - **ESLint ist auf 9.x festgelegt.** `eslint-plugin-jsx-a11y` unterstützt ESLint 10 noch nicht. Da die a11y-Regeln hier verbindlich sind, hat der vollständige Regelsatz Vorrang vor der neuesten ESLint-Hauptversion. - Die E2E-Tests brauchen einen vorherigen `npm run build`. Fehlt `out/` **oder ist der Bau älter als der Quelltext**, bricht `bauPruefen()` mit einer verständlichen Meldung ab — es wird nicht übersprungen. Das ist Absicht: Ein übersprungener Test meldet grün, und diese Suite würde dann die vorige Fassung prüfen. Übersprungen wird nur, was ausdrücklich einen Grund nennt, und das sind zwei Fälle: die **zwei** Prüfungen zum Store-Paket, die ein registriertes MSIX brauchen (`e2e/store-paket.spec.ts`), und die **18** Prüfungen gegen das gepackte Programm (`e2e/gepackt.spec.ts` und `e2e/fensterlage.spec.ts`), solange kein Paket dasteht oder es älter ist als Quelltext und Inhalte — `tools/paketstand.mjs` entscheidet das, und `tools/gate-stempel.mjs` sagt es am Ende des Gate-Berichts. Hier stand bis zum 03.09.2026 nur der erste der beiden Fälle.