- Status offen / in Arbeit / zur Abnahme / abgenommen, mit Zeitstempeln - Notizfeld fuer den Azubi, Dateianhaenge bis 25 MB - Speicherpfad nur aus selbst erzeugter UUID, nie aus dem Dateinamen - Auslieferung ueber /anhaenge/<kennung> mit Rechtepruefung bei jedem Abruf, CSP sandbox und nosniff; SVG ausdruecklich nicht erlaubt - Anhaenge haengen an Aufgabe UND Azubi - Ausbilder hakt Kriterien einzeln ab, Note und Rueckmeldung, Abnahme sperrt - Zurueckgeben zur Nacharbeit - "Was auf dich wartet" zeigt offene Abnahmen - Abgenommene Aufgaben zaehlen im Wochenfortschritt mit Behoben: Next.js deckelt Serveraktionen ab Werk bei 1 MB - damit waere kein Handyfoto durchgekommen. bodySizeLimit jetzt 30 MB, Vorabpruefung im Browser, client_max_body_size im Proxy-Beispiel auf 30m. Geaendert: Seiten verweigern fremde Daten mit 404 statt mit einem Fehler. Eigene Seiten fuer "Nicht gefunden" und Stoerfaelle. 24 neue Tests, insgesamt 157. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
211 lines
9.7 KiB
Markdown
211 lines
9.7 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.
|