diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d23cdf..c043235 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,51 @@ Vor `npm version` wandert der Abschnitt „Unveröffentlicht" unter eine neue ## [Unveröffentlicht] +## [0.5.0] – 2026-09-10 + +Praxisaufgaben: bearbeiten, belegen, abnehmen. + +### Hinzugefügt + +- Bearbeitungsstand je Aufgabe: *offen*, *in Arbeit*, *zur Abnahme*, + *abgenommen*; Beginn und Abgabe werden mit Zeitstempel vermerkt +- Notizfeld für den Azubi (bis 8000 Zeichen) – dieselben Notizen helfen später + beim Berichtsheft +- **Dateianhänge**: Fotos vom Aufbau, Bildschirmfotos, ein Netzplan als PDF. + Bis 25 MB je Datei, Positivliste erlaubter Typen (JPEG, PNG, WebP, GIF, HEIC, + PDF, Text, CSV) + - Der Speicherpfad wird ausschließlich aus einer selbst erzeugten UUID + gebildet, nie aus dem übermittelten Dateinamen + - Ausgeliefert wird nur über `/anhaenge/`, mit Rechteprüfung bei + **jedem** Abruf und `Content-Security-Policy: sandbox`, damit ein Anhang + nicht im Kontext der Anwendung ausgeführt werden kann + - SVG ist ausdrücklich nicht erlaubt – es kann Skripte enthalten +- Der Ausbilder hakt die Bewertungskriterien einzeln ab, vergibt eine Note oder + kurze Bewertung und schreibt eine Rückmeldung +- Abnehmen sperrt die Aufgabe für den Azubi; Zurückgeben setzt sie auf + „in Arbeit", damit er nacharbeiten kann +- „Was auf dich wartet" auf der Ausbilderstartseite zeigt die offenen Abnahmen + mit Azubi und Abgabezeitpunkt +- Abgenommene Aufgaben zählen im Wochenfortschritt mit +- Eigene Seite „Nicht gefunden" für den angemeldeten Bereich +- 24 Tests für Dateiprüfung, Namenssäuberung, Speicherpfad und Pfadausbruch + +### Behoben + +- **Anhänge über 1 MB scheiterten stillschweigend.** Next.js deckelt + Serveraktionen ab Werk bei 1 MB – damit wäre kein einziges Handyfoto + durchgekommen, obwohl die Anwendung 25 MB zusagt. `bodySizeLimit` steht jetzt + auf 30 MB, und zu große Dateien werden schon im Browser abgefangen, bevor sie + übertragen werden. Im Beispiel für den Reverse Proxy steht + `client_max_body_size` entsprechend auf 30m + +### Geändert + +- Seiten verweigern fremde Daten jetzt mit **404** statt mit einem Fehler. + Damit verrät die Anwendung nicht, ob es die angefragten Daten überhaupt gibt – + und der Benutzer sieht einen deutschen Satz statt „Application error". + Serveraktionen brechen weiterhin hart ab + ## [0.4.0] – 2026-09-10 Merkkarten mit Wiederholung nach Zeitabstand – der tägliche kleine Stapel @@ -185,6 +230,7 @@ anmelden und verwalten. - `npm run konto:anlegen` für das erste Ausbilderkonto [Unveröffentlicht]: #unveröffentlicht +[0.5.0]: #050--2026-09-10 [0.4.0]: #040--2026-09-10 [0.3.0]: #030--2026-09-10 [0.2.0]: #020--2026-09-10 diff --git a/DEVLOG.md b/DEVLOG.md index 29503ba..ec05a01 100644 --- a/DEVLOG.md +++ b/DEVLOG.md @@ -7,6 +7,40 @@ Format: `HH:MM–HH:MM (Dauer) · was gemacht wurde` --- +## 2026-09-10 · Baustein 5 – Praxisaufgaben + +| Von | Bis | Dauer | Tätigkeit | +|---|---|---|---| +| 11:55 | 12:05 | 0:10 | Datenmodell für Aufgabenstand und Anhänge, Migration, Schema | +| 12:05 | 12:20 | 0:15 | Dateiregeln (Typen, Größe, Namenssäuberung, Speicherpfad) und 24 Tests dazu | +| 12:20 | 12:35 | 0:15 | Repositories, Serveraktionen, Ausliefer-Route mit Rechteprüfung | +| 12:35 | 13:00 | 0:25 | Aufgabenliste, Aufgabenseite, Bewertungsformular, Abnahme | +| 13:00 | 13:20 | 0:20 | Durchstich – dabei den 1-MB-Deckel gefunden und behoben | +| 13:20 | 13:35 | 0:15 | Fremdzugriff geprüft, Seiten auf 404 umgestellt, Screenshots, Dokumente | + +**Summe Baustein 5: rund 1 h 40 min** + +### Was aufgefallen ist + +- **Der wichtigste Fund des Tages:** Next.js begrenzt Serveraktionen ab Werk auf + 1 MB Rumpfgröße. Meine eigene Grenze von 25 MB war damit nie erreichbar – + jedes Handyfoto (2 bis 5 MB) wäre stillschweigend gescheitert, mit einem + Fehler nur im Serverprotokoll. Genau das Abnahmekriterium „eine Praxisaufgabe + mit Foto abgeben" hätte nicht funktioniert. Gefunden hat den Fehler nur der + Durchstich mit einer echten Datei, nicht die Testdatei mit 200 Byte. + Gegenprobe danach: 3,8-MB-Foto hochgeladen, kommt mit 3 987 781 Byte zurück. +- Die eigene Fehlerseite für „kein Zugriff" hat nicht funktioniert, weil + Next.js Fehlermeldungen im Produktionsbetrieb schwärzt – der Text kommt nie + im Browser an. Statt daran herumzubasteln liefern die Seiten jetzt 404. Das + ist ohnehin die bessere Antwort: Ob es die Daten gibt, geht einen Unbefugten + nichts an. Die Anhang-Route machte das von Anfang an so. +- Anhänge hängen an Aufgabe **und** Azubi. Beim ersten Entwurf hätte ein + zweiter Azubi die Fotos des ersten an derselben Aufgabe gesehen. +- Geprüft mit einem eigens angelegten zweiten Ausbilderkonto: fremder Anhang + 404, eigener 200, der zuständige Ausbilder 200. Danach wieder gelöscht. + +--- + ## 2026-09-10 · Baustein 4 – Merkkarten | Von | Bis | Dauer | Tätigkeit | diff --git a/ENTSCHEIDUNGEN.md b/ENTSCHEIDUNGEN.md index c7a9880..f0add15 100644 --- a/ENTSCHEIDUNGEN.md +++ b/ENTSCHEIDUNGEN.md @@ -164,3 +164,47 @@ kommen zuletzt, damit zuerst der Rückstand abgebaut wird. 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 `/`. 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/`. 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 `::`. 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. diff --git a/README.md b/README.md index 91b3480..7e103e0 100644 --- a/README.md +++ b/README.md @@ -9,9 +9,9 @@ und – der wichtigste Teil – den gesetzlich vorgeschriebenen Ausbildungsnachw Gebaut für **einen Ausbilder und ein bis drei Auszubildende**. Kein Lernplattform-Baukasten, keine Mandanten, keine Cloud. -> **Stand:** Baustein 1 von 10 ist fertig (Fundament). Der -> [Fahrplan](ROADMAP.md) zeigt, was folgt, das -> [Änderungsprotokoll](CHANGELOG.md), was schon drin ist. +> **Stand:** Bausteine 1 bis 5 von 10 sind fertig – Fundament, Inhalte, +> Wochenplan, Merkkarten, Praxisaufgaben. Der [Fahrplan](ROADMAP.md) zeigt, +> was folgt, das [Änderungsprotokoll](CHANGELOG.md), was schon drin ist. --- @@ -41,8 +41,8 @@ docker compose exec lernbar sh -c \ 'node werkzeuge/konto.js --name "Vorname Nachname" --email ausbilder@betrieb.de --rolle ausbilder' ``` -> Solange das Konto-Werkzeug im Abbild noch nicht liegt (Baustein 1), geht es -> vom Entwicklungsrechner aus: +> Solange das Konto-Werkzeug im Abbild noch nicht liegt, geht es vom +> Entwicklungsrechner aus: > `npm run konto:anlegen -- --name "…" --email … --rolle ausbilder` Danach im Browser anmelden, unter **Verwaltung** das Azubikonto anlegen und @@ -75,7 +75,7 @@ server { ssl_certificate /etc/ssl/lernbar.crt; ssl_certificate_key /etc/ssl/lernbar.key; - client_max_body_size 25m; # Fotos vom Aufbau als Anhang + client_max_body_size 30m; # Fotos vom Aufbau als Anhang (Grenze der Anwendung: 25 MB) location / { proxy_pass http://127.0.0.1:3000; @@ -136,8 +136,6 @@ Datei und Stelle. Prüfen ohne Start: npm run inhalte:pruefen ``` -*(Wird mit Baustein 2 scharf geschaltet.)* - --- ## Datensicherung diff --git a/ROADMAP.md b/ROADMAP.md index bbe9abc..d39af91 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -56,7 +56,14 @@ Legende: `[ ]` offen · `[~]` in Arbeit · `[x]` fertig ## 0.4 – Nachweise -- [ ] **B5 · Praxisaufgaben mit Anhängen und Abnahme** +- [x] **B5 · Praxisaufgaben mit Anhängen und Abnahme** + - [x] Status offen / in Arbeit / zur Abnahme / abgenommen + - [x] Notizen des Azubis, Dateianhänge (Fotos, Bildschirmfotos, PDF) + - [x] Ausbilder hakt Kriterien einzeln ab, Note und Rückmeldung + - [x] Abnehmen sperrt die Aufgabe, Zurückgeben zur Nacharbeit + - [x] Anhänge nur über Kennung, Berechtigung bei jedem Abruf geprüft + - [x] „Was auf dich wartet" zeigt offene Abnahmen + - [x] Aufgaben zählen im Wochenfortschritt mit - [ ] **B6 · Checklisten mit Durchläufen und PDF** - [ ] **B7 · Berichtsheft: Tagesnotizen, Vorschläge, Prüfung, Abzeichnen, PDF** diff --git a/docs/bilder/02-ausbilder-ueberblick.png b/docs/bilder/02-ausbilder-ueberblick.png index 39ed84f..5342161 100644 Binary files a/docs/bilder/02-ausbilder-ueberblick.png and b/docs/bilder/02-ausbilder-ueberblick.png differ diff --git a/docs/bilder/03-verwaltung.png b/docs/bilder/03-verwaltung.png index 970dff9..5dff273 100644 Binary files a/docs/bilder/03-verwaltung.png and b/docs/bilder/03-verwaltung.png differ diff --git a/docs/bilder/04-wochenplan.png b/docs/bilder/04-wochenplan.png index f1c1e94..9dcb1b0 100644 Binary files a/docs/bilder/04-wochenplan.png and b/docs/bilder/04-wochenplan.png differ diff --git a/docs/bilder/05-aufgabe-abnahme.png b/docs/bilder/05-aufgabe-abnahme.png new file mode 100644 index 0000000..3ce39ca Binary files /dev/null and b/docs/bilder/05-aufgabe-abnahme.png differ diff --git a/docs/bilder/10-azubi-woche.png b/docs/bilder/10-azubi-woche.png index 1cdeffa..edde01e 100644 Binary files a/docs/bilder/10-azubi-woche.png and b/docs/bilder/10-azubi-woche.png differ diff --git a/docs/bilder/11-handouts-liste.png b/docs/bilder/11-handouts-liste.png index b1fd117..9f1d02f 100644 Binary files a/docs/bilder/11-handouts-liste.png and b/docs/bilder/11-handouts-liste.png differ diff --git a/docs/bilder/12-handouts-suche.png b/docs/bilder/12-handouts-suche.png index a8ba06d..1e6080f 100644 Binary files a/docs/bilder/12-handouts-suche.png and b/docs/bilder/12-handouts-suche.png differ diff --git a/docs/bilder/13-handout-n3.png b/docs/bilder/13-handout-n3.png index 747535a..23b2b81 100644 Binary files a/docs/bilder/13-handout-n3.png and b/docs/bilder/13-handout-n3.png differ diff --git a/docs/bilder/14-merkkarten.png b/docs/bilder/14-merkkarten.png index 5d983ae..e778d2a 100644 Binary files a/docs/bilder/14-merkkarten.png and b/docs/bilder/14-merkkarten.png differ diff --git a/docs/bilder/15-glossar.png b/docs/bilder/15-glossar.png index a1f232c..f845144 100644 Binary files a/docs/bilder/15-glossar.png and b/docs/bilder/15-glossar.png differ diff --git a/docs/bilder/16-aufgaben-liste.png b/docs/bilder/16-aufgaben-liste.png new file mode 100644 index 0000000..c8db751 Binary files /dev/null and b/docs/bilder/16-aufgaben-liste.png differ diff --git a/docs/bilder/17-aufgabe-azubi.png b/docs/bilder/17-aufgabe-azubi.png new file mode 100644 index 0000000..a6aa65e Binary files /dev/null and b/docs/bilder/17-aufgabe-azubi.png differ diff --git a/docs/bilder/20-dunkel.png b/docs/bilder/20-dunkel.png index 52b28bc..b65ac83 100644 Binary files a/docs/bilder/20-dunkel.png and b/docs/bilder/20-dunkel.png differ diff --git a/docs/bilder/30-handy-handout.png b/docs/bilder/30-handy-handout.png index 319a7d4..79c71e8 100644 Binary files a/docs/bilder/30-handy-handout.png and b/docs/bilder/30-handy-handout.png differ diff --git a/docs/bilder/31-handy-lernmodus-frage.png b/docs/bilder/31-handy-lernmodus-frage.png index 979e834..ea8f77e 100644 Binary files a/docs/bilder/31-handy-lernmodus-frage.png and b/docs/bilder/31-handy-lernmodus-frage.png differ diff --git a/docs/bilder/32-handy-lernmodus-antwort.png b/docs/bilder/32-handy-lernmodus-antwort.png index dbc3b82..a5642d9 100644 Binary files a/docs/bilder/32-handy-lernmodus-antwort.png and b/docs/bilder/32-handy-lernmodus-antwort.png differ diff --git a/next.config.ts b/next.config.ts index df064ed..e17b92a 100644 --- a/next.config.ts +++ b/next.config.ts @@ -7,6 +7,12 @@ const konfiguration: NextConfig = { serverExternalPackages: ['better-sqlite3', '@node-rs/argon2', 'pdfkit'], poweredByHeader: false, reactStrictMode: true, + experimental: { + // Next.js deckelt Serveraktionen sonst bei 1 MB – damit wäre kein einziges + // Handyfoto durchgekommen. Etwas über der eigenen Grenze von 25 MB, damit + // eine zu große Datei die *eigene* Meldung bekommt und nicht die des Gerüsts. + serverActions: { bodySizeLimit: '30mb' }, + }, eslint: { ignoreDuringBuilds: true }, async headers() { return [ diff --git a/scripts/screenshots.ts b/scripts/screenshots.ts index 38afd2a..0ab6a11 100644 --- a/scripts/screenshots.ts +++ b/scripts/screenshots.ts @@ -78,6 +78,15 @@ async function main() { await seite.waitForSelector('h1'); await bild(seite, '04-wochenplan'); + await seite.goto(`${BASIS}/aufgaben`); + await seite.waitForSelector('h1'); + const abnahme = await seite.$('a[href*="/aufgaben/1.1?azubi="]'); + if (abnahme) { + await abnahme.click(); + await seite.waitForSelector('#abnahme'); + await bild(seite, '05-aufgabe-abnahme'); + } + await abmelden(seite); // --- Azubi --- @@ -101,6 +110,14 @@ async function main() { await seite.waitForSelector('h1'); await bild(seite, '14-merkkarten'); + await seite.goto(`${BASIS}/aufgaben`); + await seite.waitForSelector('h1'); + await bild(seite, '16-aufgaben-liste'); + + await seite.goto(`${BASIS}/aufgaben/1.1`); + await seite.waitForSelector('h1'); + await bild(seite, '17-aufgabe-azubi'); + await seite.goto(`${BASIS}/glossar`); await seite.waitForSelector('h1'); await bild(seite, '15-glossar'); diff --git a/src/app/(angemeldet)/aufgaben/StatusMarke.tsx b/src/app/(angemeldet)/aufgaben/StatusMarke.tsx new file mode 100644 index 0000000..2650bcc --- /dev/null +++ b/src/app/(angemeldet)/aufgaben/StatusMarke.tsx @@ -0,0 +1,21 @@ +import type { AufgabenStatus } from '@/lib/db/schema'; + +/** + * Status einer Praxisaufgabe. Nie nur über die Farbe – jede Stufe trägt ein + * eigenes Zeichen und ihren Namen. + */ +export const statusText: Record = { + offen: { text: 'offen', ton: 'neutral', zeichen: '○' }, + in_arbeit: { text: 'in Arbeit', ton: 'warnung', zeichen: '◐' }, + zur_abnahme: { text: 'zur Abnahme', ton: 'warnung', zeichen: '▲' }, + abgenommen: { text: 'abgenommen', ton: 'gut', zeichen: '✓' }, +}; + +export function StatusMarke({ status }: { status: AufgabenStatus }) { + const marke = statusText[status]; + return ( + + {marke.zeichen} {marke.text} + + ); +} diff --git a/src/app/(angemeldet)/aufgaben/[id]/Formulare.tsx b/src/app/(angemeldet)/aufgaben/[id]/Formulare.tsx new file mode 100644 index 0000000..94329e4 --- /dev/null +++ b/src/app/(angemeldet)/aufgaben/[id]/Formulare.tsx @@ -0,0 +1,225 @@ +'use client'; + +import { useActionState, useRef, useState } from 'react'; +import { Absenden, Meldung } from '@/komponenten/Formularmeldung'; +import { + anhangHochladen, + bewerten, + eigeneNotizSpeichern, + eigenenStatusSetzen, + type Rueckmeldung, +} from '../aktionen'; +import type { AufgabenStatus } from '@/lib/db/schema'; + +export function StatusWaehlen({ aufgabeId, status }: { aufgabeId: string; status: AufgabenStatus }) { + const stufen: { wert: AufgabenStatus; text: string }[] = [ + { wert: 'offen', text: 'Offen' }, + { wert: 'in_arbeit', text: 'In Arbeit' }, + { wert: 'zur_abnahme', text: 'Zur Abnahme' }, + ]; + + return ( +
+ + {stufen.map((stufe) => ( + + ))} +
+ ); +} + +export function NotizFormular({ + aufgabeId, + notiz, + gesperrt, +}: { + aufgabeId: string; + notiz: string; + gesperrt: boolean; +}) { + const [zustand, aktion] = useActionState(eigeneNotizSpeichern, {}); + + return ( +
+ + + +