- Durchlauf aus einer Vorlage, mit Datum, Ticket und Kunde - Schritt fuer Schritt abhaken, Bemerkung je Schritt - Fortschritt gesamt und je Gruppe (Vorbereiten/Durchfuehren/Pruefen/Abschliessen) - Abschliessen erst, wenn alles abgehakt ist; Meldung nennt die offenen Schritte - Abgeschlossene Durchlaeufe sind gesperrt und bleiben als Nachweis - Titel und Schritttexte werden beim Start kopiert, nicht verwiesen - PDF-Export mit gezeichneten Ankreuzkaestchen und Unterschriftenzeilen - Gemeinsame PDF-Grundlage fuer alle kuenftigen Ausgaben - Ausbilder liest mit und darf das PDF abrufen, hakt aber nicht ab - Abgeschlossene Durchlaeufe zaehlen im Wochenfortschritt mit Behoben beim Durchstich: Die Fusszeile wurde unterhalb des Satzspiegels gesetzt, wodurch PDFKit je Fusszeile eine leere Seite anhaengte - aus zwei Seiten wurden sechs. Ein Test zaehlt jetzt die Seitenobjekte im PDF. 29 neue Tests, insgesamt 186. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
260 lines
12 KiB
Markdown
260 lines
12 KiB
Markdown
# Entscheidungen
|
||
|
||
Kurze Notizen zu Festlegungen, die im Auftrag offen waren. Jede Notiz nennt
|
||
die Lage, die Wahl und den Grund.
|
||
|
||
---
|
||
|
||
## E1 · Stack: Next.js 15 mit App Router
|
||
|
||
Der Auftrag verweist auf eine Stackvorgabe im „Kopf des Prompts", die nicht
|
||
mitgeliefert wurde. Auf Rückfrage: **Next.js 15 (App Router), React 19,
|
||
TypeScript**. Datenzugriff über Drizzle ORM auf better-sqlite3.
|
||
|
||
Serveraktionen ersetzen eine eigene REST-Schicht. Das hält die Rechteprüfung
|
||
dort, wo die Daten geholt werden, statt sie über eine API zu verteilen.
|
||
|
||
## E2 · Zeitstempel als ISO-8601 in UTC
|
||
|
||
Der Auftrag verlangt „alle Zeitstempel mit Zeitzone". Gespeichert wird
|
||
`2026-09-10T07:15:00.000Z`, also ein eindeutiger Zeitpunkt in UTC. Angezeigt
|
||
wird durchgehend in Europe/Berlin.
|
||
|
||
Grund: UTC-Strings sortieren sich lexikografisch richtig, was bei SQLite ohne
|
||
echten Zeitstempeltyp entscheidend ist. Ein gespeicherter Ortsversatz
|
||
(`+02:00`) würde beim Wechsel zwischen Sommer- und Winterzeit falsch sortieren.
|
||
|
||
Reine Kalenderdaten – Berichtszeitraum, Tagesnotiz, Ausbildungsbeginn – sind
|
||
`YYYY-MM-DD` ohne Uhrzeit, weil sie auch keine haben.
|
||
|
||
## E3 · Eigener Migrationslauf statt drizzle-kit
|
||
|
||
Migrationen sind handgeschriebene SQL-Dateien unter
|
||
`src/lib/db/migrationen/`, die beim Start der Reihe nach genau einmal
|
||
angewendet und mit SHA-256-Prüfsumme vermerkt werden.
|
||
|
||
Grund: volle Kontrolle über Indizes und `CHECK`-Bedingungen (Statuswerte sind
|
||
Aufzählungen, keine freien Zeichenketten), und beim Start ist im Protokoll
|
||
sichtbar, was passiert ist. Wird eine bereits angewendete Datei nachträglich
|
||
verändert, bricht der Start mit klarer Meldung ab.
|
||
|
||
Der Preis: das Drizzle-Schema muss von Hand zu den SQL-Dateien passen. Dagegen
|
||
läuft ein Test, der die migrierte Datenbank gegen das Schema abgleicht.
|
||
|
||
## E4 · Schlüssel sind UUIDs, keine laufenden Nummern
|
||
|
||
Textspalten mit `crypto.randomUUID()`. Läuft auf SQLite und PostgreSQL
|
||
gleichermaßen, und beim Zurückspielen einer Sicherung gibt es keine Kollisionen
|
||
mit zwischenzeitlich vergebenen Nummern.
|
||
|
||
## E5 · Debian-slim als Grundlage des Abbilds, nicht Alpine
|
||
|
||
`better-sqlite3` und `@node-rs/argon2` bringen für glibc fertige Binärdateien
|
||
mit, für musl nicht. Auf Alpine müsste im Abbild übersetzt werden – längerer
|
||
Bau, größere Bauschicht, mehr Fehlerquellen. Der Größenunterschied im
|
||
Laufzeitabbild ist bei einem selbst gehosteten Dienst ohne Belang.
|
||
|
||
## E6 · Passwortprüfung mit gleichbleibender Antwortzeit
|
||
|
||
Bei unbekannter E-Mail-Adresse wird trotzdem einmal gegen einen festen
|
||
Argon2-Wert gerechnet. Sonst verrät die Antwortzeit, welche Adressen es gibt.
|
||
Die Fehlermeldung ist in beiden Fällen dieselbe.
|
||
|
||
## E7 · Anhänge liegen im Dateisystem, nicht in der Datenbank
|
||
|
||
Unter `<daten>/anhaenge/`, in der Datenbank steht nur der Verweis. Hält die
|
||
Datenbankdatei klein und die Sicherung schnell; das Archiv nimmt beides mit.
|
||
|
||
## E8 · Sicherung als Shell-Skript, nicht als Node-Werkzeug
|
||
|
||
`scripts/im-container/sicherung.sh` läuft im Laufzeitabbild, das kein
|
||
TypeScript kennt, und auf dem Entwicklungsrechner unter Git Bash. Eine zweite
|
||
Fassung in TypeScript wäre eine zweite Fehlerquelle. Die konsistente Kopie der
|
||
Datenbank entsteht über die Sicherungsschnittstelle von SQLite – ein `cp` wäre
|
||
bei aktivem WAL-Modus nicht verlässlich.
|
||
|
||
## E9 · Kein `main`-Zweig ohne Freigabe
|
||
|
||
Die verbindliche Arbeitsweise verbietet Commits direkt auf `main`. Die
|
||
Erstfassung entsteht deshalb vollständig auf `feat/lernbar-erstfassung`.
|
||
`main` bleibt leer, bis eine Freigabe erfolgt.
|
||
|
||
---
|
||
|
||
<!-- Weitere Entscheidungen werden unten angefügt und fortlaufend nummeriert. -->
|
||
|
||
## E10 · Rahmenplan: A1–A10, B1–B3, C1–**C5**
|
||
|
||
Der Auftrag nennt die Berufsbildpositionen `A 1 bis A 10`, `B 1 bis B 3` und
|
||
`C 1 bis C 4`. Die Verordnung selbst gliedert anders: Sie deckt alle vier
|
||
Fachrichtungen ab und hat deshalb sechs Abschnitte. Übersetzt:
|
||
|
||
| Kürzel hier | Abschnitt der FIAusbV | Inhalt |
|
||
|---|---|---|
|
||
| A1 – A10 | Abschnitt A | fachrichtungsübergreifend |
|
||
| B1 – B3 | Abschnitt C | Fachrichtung Systemintegration |
|
||
| C1 – C5 | Abschnitt F | integrativ zu vermitteln |
|
||
|
||
Abweichung: Abschnitt F hat **fünf** Positionen, nicht vier. Die fünfte
|
||
(„Vernetztes Zusammenarbeiten unter Nutzung digitaler Medien") ist 2020 neu
|
||
dazugekommen. Sie wegzulassen würde ausgerechnet in der Abdeckungsansicht eine
|
||
Lücke erzeugen, die gegenüber der IHK zählt. Deshalb sind alle fünf drin.
|
||
|
||
Jede Position trägt im Feld `verordnung` ihre amtliche Fundstelle, damit die
|
||
Übersetzung nachvollziehbar bleibt.
|
||
|
||
## E11 · Kennung einer Merkkarte: Handout-Kürzel plus laufende Nummer
|
||
|
||
Eine Karte heißt `N3-1`, `N3-2` und so weiter – abgeleitet aus dem Handout und
|
||
der Reihenfolge im YAML-Kopf. Damit steht die Kennung fest, ohne dass sie in der
|
||
Datei gepflegt werden muss.
|
||
|
||
Der Preis: Wird eine Karte **mittendrin** eingefügt oder gelöscht, verschieben
|
||
sich die Kennungen der folgenden, und ihr Lernstand rutscht mit. Neue Karten
|
||
gehören deshalb ans Ende der Liste. Bei einem Handout mit ein bis drei Karten
|
||
ist das zumutbar; eine gepflegte Kennung je Karte wäre mehr Aufwand für die
|
||
Person, die Inhalte schreibt, als sie einbringt.
|
||
|
||
## E12 · Glossarverweise: Link statt Überfahren
|
||
|
||
Der Auftrag lässt die Wahl zwischen automatischer Verlinkung und einer
|
||
Erklärung beim Überfahren mit der Maus. Es wird beides, aber der Link führt:
|
||
Ein reiner `title`-Hinweis ist am Handy nicht erreichbar und mit der Tastatur
|
||
nur mit Mühe. Der Verweis zeigt auf die Stelle im Glossar und trägt die
|
||
Erklärung zusätzlich im `title`.
|
||
|
||
Zurückhaltung ist eingebaut: Jeder Begriff wird höchstens einmal je Seite
|
||
verlinkt, und nie innerhalb von Code, Überschriften, Tabellenköpfen oder
|
||
bestehenden Verweisen. Ein Text, in dem jedes zweite Wort unterstrichen ist,
|
||
liest sich schlechter als einer ganz ohne.
|
||
|
||
## E13 · Kein `gray-matter`
|
||
|
||
Der YAML-Kopf einer Markdown-Datei wird selbst herausgelöst (zehn Zeilen).
|
||
Gründe: `gray-matter` merkt sich Ergebnisse und liefert beim zweiten Einlesen
|
||
desselben Textes ein Objekt ohne das Feld `matter` – im Betrieb fällt das nie
|
||
auf, im Test sofort. Und der Zeilenversatz für die Fehlermeldungen ist so
|
||
nachweislich richtig statt nachgebaut.
|
||
|
||
## E14 · SM-2 mit drei Bewertungsstufen statt sechs
|
||
|
||
Das Original arbeitet mit einer Skala von 0 bis 5. In der Praxis unterscheidet
|
||
niemand ehrlich zwischen „fast richtig" und „mit Mühe richtig" – und eine
|
||
Selbstbewertung, die man nicht ehrlich abgeben kann, verfälscht die Rechnung.
|
||
|
||
Deshalb: **sitzt** / **unsicher** / **nochmal**. Die Leichtigkeit bewegt sich
|
||
weiterhin zwischen 1,3 und 2,8, die Abstände folgen 1, 3, dann Abstand mal
|
||
Leichtigkeit. Gedeckelt wird bei 180 Tagen – die Ausbildung dauert drei Jahre,
|
||
weiter zu planen bringt nichts.
|
||
|
||
## E15 · „nochmal" bringt die Karte in derselben Sitzung wieder
|
||
|
||
Erste Fassung legte sie nur auf den nächsten Tag. Das ist falsch: Wer „nochmal"
|
||
drückt, will die Karte *jetzt* noch einmal sehen. Sie wird deshalb hinten an den
|
||
laufenden Stapel gehängt.
|
||
|
||
## E16 · Der tägliche Stapel wird bei 20 Karten gedeckelt
|
||
|
||
Ohne Deckel sitzt jemand nach drei Wochen Pause vor 52 fälligen Karten und
|
||
macht es gar nicht. Sortiert wird nach Überfälligkeit; nie gelernte Karten
|
||
kommen zuletzt, damit zuerst der Rückstand abgebaut wird.
|
||
|
||
## E17 · Als „gelernt" gilt eine Karte ab Fach 4
|
||
|
||
Fach 4 heißt: Abstand über drei Tage, also mindestens dreimal in Folge gekonnt.
|
||
Das ist die Schwelle, ab der die Karte im Wochenfortschritt als erledigt zählt.
|
||
Wer sie einmal richtig hatte, hat sie noch nicht gelernt.
|
||
|
||
## E18 · Anhänge: Pfad aus UUID, Auslieferung nur über eine geprüfte Route
|
||
|
||
Drei Regeln, die zusammengehören:
|
||
|
||
1. Der **Speicherpfad** ist `<jahr>/<uuid><endung>`. Der übermittelte Dateiname
|
||
landet nur als Anzeigename in der Datenbank und wird nie zum Bilden eines
|
||
Pfades benutzt. `speicherpfad()` wirft, wenn die Kennung keine UUID ist.
|
||
2. **Ausgeliefert** wird ausschließlich über `/anhaenge/<kennung>`. Es gibt
|
||
keinen Weg, über die Adresse eine beliebige Datei anzufordern, und die
|
||
Berechtigung wird bei jedem Abruf neu geprüft – nicht nur beim Hochladen.
|
||
3. Der Antwortkopf trägt `Content-Security-Policy: sandbox` und
|
||
`X-Content-Type-Options: nosniff`. Anhänge sind Daten, kein Programm.
|
||
|
||
SVG ist trotz `image/` **nicht** erlaubt: Es ist XML und kann Skripte enthalten.
|
||
|
||
## E19 · Anhänge hängen an Aufgabe **und** Azubi
|
||
|
||
Der Bezug lautet `<aufgabeId>::<azubiId>`. Beim ersten Entwurf hing er nur an
|
||
der Aufgabe – bei zwei Azubis hätte der zweite die Fotos des ersten gesehen.
|
||
|
||
## E20 · Kriterien werden über ihre Position gespeichert
|
||
|
||
`kriterien_erfuellt` ist eine JSON-Liste der Positionen in der Aufgabendatei,
|
||
etwa `[0,2,3]`. Wird die Reihenfolge der Kriterien nachträglich geändert,
|
||
verschiebt sich die Zuordnung. Neue Kriterien gehören deshalb ans Ende –
|
||
dieselbe Regel wie bei den Merkkarten (siehe E11).
|
||
|
||
## E21 · Fehlende Berechtigung ist ein 404, kein 403
|
||
|
||
Seiten rufen `erfordereSichtAufAzubi()` auf, das mit „nicht gefunden" endet.
|
||
Ein 403 würde bestätigen, dass es die angefragten Daten gibt. Serveraktionen
|
||
brechen weiterhin mit einem Fehler ab – dort wäre eine Weiterleitung sinnlos.
|
||
|
||
Nebeneffekt: Next.js schwärzt Fehlermeldungen im Produktionsbetrieb. Eine eigene
|
||
„Kein Zugriff"-Seite ließe sich anhand des Fehlertextes gar nicht erkennen.
|
||
|
||
## E22 · Der Deckel für Serveraktionen liegt über der eigenen Grenze
|
||
|
||
`serverActions.bodySizeLimit` steht auf 30 MB, die eigene Prüfung auf 25 MB.
|
||
Der Abstand ist Absicht: Eine zu große Datei soll die **eigene**, verständliche
|
||
Meldung bekommen und nicht den Rumpfgrößenfehler des Gerüsts, der nur im
|
||
Serverprotokoll auftaucht. Zusätzlich prüft der Browser vorab, damit am
|
||
Mobilfunk nicht erst 30 MB übertragen und dann abgelehnt werden.
|
||
|
||
## E23 · PDF mit Standardschriften statt eingebetteter Schrift
|
||
|
||
Die PDF-Ausgabe benutzt Helvetica, also eine der Schriften, die jeder
|
||
PDF-Betrachter mitbringt. Das spart eine eingebettete Schriftdatei im
|
||
Repository und hält die Dateien klein (ein Checklistenblatt wiegt rund 7 KB).
|
||
|
||
Der Preis ist der Zeichenvorrat **WinAnsi**. Enthalten sind alle deutschen
|
||
Umlaute, ß, €, § und °. Nicht enthalten sind Zeichen wie ✓, ○, ▲ oder Emoji.
|
||
Deshalb:
|
||
|
||
- `fuerPdf()` ersetzt typografische Feinheiten (Anführungszeichen,
|
||
Auslassungspunkte, geschützte Leerzeichen) und wirft nicht Darstellbares weg.
|
||
- Ankreuzkästchen und Haken werden **gezeichnet**, nicht gesetzt. Das sieht in
|
||
jedem Betrachter gleich aus und ist unabhängig von der Schrift.
|
||
|
||
Sollte später ein Zeichen außerhalb von WinAnsi wirklich gebraucht werden, ist
|
||
der Weg das Einbetten einer freien Schrift – nicht das Aufweichen dieser Regel.
|
||
|
||
## E24 · Fußzeilen brauchen einen aufgehobenen unteren Rand
|
||
|
||
PDFKit legt eine neue Seite an, sobald Text unterhalb des Satzspiegels
|
||
geschrieben wird. Eine Fußzeile steht dort per Definition. Ohne Gegenmaßnahme
|
||
entsteht je Seite und je Textaufruf eine leere Folgeseite.
|
||
|
||
`fusszeilen()` setzt deshalb `page.margins.bottom` kurzzeitig auf 0 und stellt
|
||
den Wert danach wieder her. Ein Test zählt die Seitenobjekte im erzeugten PDF –
|
||
damit fällt ein Rückfall sofort auf.
|
||
|
||
## E25 · Ein Durchlauf kopiert die Texte seiner Vorlage
|
||
|
||
`checkliste_lauf.titel` und `checkliste_schritt.text` werden beim Start aus der
|
||
Vorlage übernommen, statt zur Anzeigezeit nachgeschlagen zu werden.
|
||
|
||
Grund: Ein abgeschlossener Durchlauf ist ein Nachweis. Er muss auch in zwei
|
||
Jahren noch dasselbe aussagen – auch wenn die Vorlage unter `content/`
|
||
inzwischen umformuliert, erweitert oder gelöscht wurde. Die Seite zeigt einen
|
||
Hinweis, wenn es die Vorlage nicht mehr gibt, bleibt aber vollständig lesbar.
|
||
|
||
## E26 · Mitlesen und Bearbeiten sind getrennte Rechte
|
||
|
||
Beim Checklisten-Durchlauf fallen die beiden Rechte auseinander:
|
||
|
||
- **Lesen** darf der Eigentümer und sein Ausbilder (`laufZumLesen`).
|
||
- **Bearbeiten** darf nur der Eigentümer (`laufZumBearbeiten`).
|
||
|
||
Der Ausbilder hakt also nichts für seinen Azubi ab. Im Nachweis soll keine
|
||
Arbeit stehen, die der Azubi nicht selbst gemacht hat. Für die Abnahme gibt es
|
||
die Praxisaufgaben – dort ist es genau andersherum.
|