waffensachkunde

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

/ app README.md

17,1 KB Rohdatei

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 nur das Anwendungsgerüst. Die Lerninhalte und das Datenbankschema kommen aus einem anderen Arbeitsstrang (content/, data-pipeline/).


Technischer Stack

BausteinVersionZweck
Electron43.4.1Desktop-Laufzeit
React19.2.8Oberfläche im Renderer-Prozess
Vite7.3.6Bundler
electron-vite5.0.0Bindeglied Vite ↔ Electron
TypeScript6.0.3strict plus zusätzliche Verschärfungen
better-sqlite313.0.3Lokale Datenbank des Lernstands
electron-builder26.15.3Paketierung (NSIS, DMG)
Vitest4.1.11Unit-Tests
Playwright1.62.1E2E-Tests gegen die echte Anwendung
axe-core4.13.0Automatisierte Barrierefreiheitsprüfung
ESLint / Prettier9.39.5 / 3.9.6Statische Analyse und Formatierung

Verzeichnisstruktur

app/
├── src/
│   ├── main/                  Main-Prozess (Node, voller Systemzugriff) – 31 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         Öffnen der SQLite-Datei
│   │   ├── schema.ts            Grundschema und die zehn 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/ (38), pruefung/ (13), profil/ (3),
│   │       │                        suche/ (2), ueber/ (3), hilfe/ (1)
│   │       ├── hooks/             26 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 (41 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) – 72 Dateien
│   ├── 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 Dateien
│   ├── 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

SkriptWirkung
npm run devStartet Vite-Dev-Server und Electron mit Hot Reload.
npm run buildTypprüfung, danach Produktionsbundles nach out/.
npm startZeigt einen bereits gebauten Stand (electron-vite preview).
npm testUnit-Tests einmalig (Vitest).
npm run test:watchUnit-Tests im Beobachtungsmodus.
npm run test:e2eBaut die App und führt danach die Playwright-Tests aus.
npm run lintESLint über das gesamte Projekt.
npm run lint:fixESLint mit automatischer Korrektur.
npm run typecheckTypeScript für alle vier Teilprojekte (node, web, tests, e2e).
npm run formatPrettier schreibt alle Dateien.
npm run format:checkPrettier prüft ohne zu schreiben (für CI).
npm run dist:winWindows-Installer (NSIS) nach release/.
npm run dist:macmacOS-Abbild (DMG) nach release/ – muss auf macOS laufen.
npm run rebuildBaut 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 <meta>-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 <meta>-Fassung lässt frame-ancestors bewusst weg: Diese Direktive wirkt ausschließlich im HTTP-Kopf, im <meta>-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

BereichRegel
ZielniveauWCAG 2.2 Stufe AA vollständig; AAA als Zielwert, wo erreichbar.
TextkontrastMinimum 4,5:1, Zielwert 7:1.
UI-KontrastRänder, Bedienelemente und Fokusring mindestens 3:1.
Sprachelang="de" am <html>; alle sichtbaren Texte auf Deutsch.
TastaturJede Funktion ohne Maus bedienbar; sichtbarer :focus-visible-Ring.
Skip-LinkErster Tabstopp springt zum Hauptinhalt.
StrukturGenau eine <h1>, keine übersprungenen Ebenen, Landmarken mit Namen.
Farbe alleinZustände nie nur über Farbe (Radiogruppe nutzt zusätzlich einen Glyph).
KlickflächenMindestens 44 × 44 px (--klickflaeche-min).
Bewegungprefers-reduced-motion: reduce schaltet alle Übergänge praktisch ab.
SchriftenNur systemseitig vorhandene Schriften – keine Webfont-Downloads.
WidgetsARIA-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 (<html data-thema="…">) 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.
  1. 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.
  1. 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 – einmal pro Theme, damit alle drei Paletten gemessen werden. 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.

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 (die beiden Prüfungen zum Store-Paket, die ein registriertes MSIX brauchen).