# Vertrag 3: Output-API Was ein Modul am Ende einer Sitzung an die Plattform liefert. Plus Endpoint zum Zwischenspeichern des Resume-States. ## Endgültiges Ergebnis bei Modul-Abschluss Wenn Schüler*in das Modul abschließt (egal ob bestanden oder abgebrochen), pusht das Modul **genau einen** Result-Call: ```http POST /v2beta/api/result Authorization: Bearer Content-Type: application/json { "moduleSlug": "logistik", "moduleVersion": "2.0.0", "specVersion": "1.0", "completed": true, "abandoned": false, "score": 78, "scoreMax": 100, "durationS": 1452, "difficulty": "L1", "mode": "teacher_started", "milestonesReached": [ "first-order-accepted", "level-1-cleared" ], "lehrplanCoverage": { "AT-GW-1.4.2": "full", "AT-GW-2.1.1": "partial", "AT-GW-3.5.4": "none" }, "freeTextAnswers": [ { "promptId": "reflexion-end", "promptText": "Warum hast du dich für die Bahnverbindung entschieden?", "answerText": "Weil sie billiger war und nicht im Stau steht." } ], "artifacts": [ { "type": "image/png", "url": "/v2beta/sims/logistik/uploads/student-4711/route-final.png", "label": "Letzte gewählte Route", "thumbnail": "/v2beta/sims/logistik/uploads/student-4711/route-final-thumb.png" } ], "moduleSpecificData": { "ordersCompleted": 6, "ordersFailed": 2, "totalCO2Kg": 142, "totalKm": 1845 }, "ts": "2026-05-15T14:48:00Z" } ``` ### Pflicht-Felder | Feld | Typ | Beschreibung | |---|---|---| | `moduleSlug` | String | Aus Manifest | | `moduleVersion` | String | Aus Manifest | | `specVersion` | String | Welche Spec-Version genutzt wurde | | `completed` | Boolean | Modul ehrlich abgeschlossen (nicht abgebrochen) | | `abandoned` | Boolean | Schüler*in hat Tab geschlossen / Stunde zu Ende ohne Abschluss | | `score` | Integer 0–N | Punktzahl | | `scoreMax` | Integer | Maximal mögliche Punktzahl | | `durationS` | Integer | Reine Bearbeitungszeit (ohne Pausen >5min) | | `difficulty` | String | Aus Input-Param | | `mode` | String | Aus Input-Param | | `lehrplanCoverage` | Object | Pro Anker aus Manifest: `full` / `partial` / `none` | | `ts` | ISO-Timestamp | Zeitpunkt des Abschlusses | ### Optionale Felder - `milestonesReached` — alle erreichten Milestone-IDs (Subset von `manifest.telemetryMilestones`) - `freeTextAnswers` — Reflexionsfragen-Antworten der Schüler*in (für Lehrperson-Auswertung) - `artifacts` — Modul-Output (Bilder, Karten, Audio) als URLs - `moduleSpecificData` — Frei strukturiertes JSON mit Modul-eigenen Kennzahlen für die Auswertung ### Lehrplan-Coverage Modul muss pro deklariertem Lehrplan-Anker entscheiden: - `full` — Schüler*in hat zu diesem Anker gearbeitet UND erfolgreich abgeschlossen - `partial` — hat zu diesem Anker gearbeitet, aber nicht voll geschafft - `none` — hat diesen Anker in dieser Sitzung nicht berührt (z.B. weil bestimmtes Level nicht erreicht) ## Resume-State zwischenspeichern Während der Sitzung kann (und soll) das Modul regelmäßig den State speichern, damit Schüler*in nach Crash / Logout fortsetzen kann: ```http PUT /v2beta/api/module/state Authorization: Bearer Content-Type: application/json { "moduleSlug": "logistik", "state": { "": "..." }, "summary": "Tag 3, Level 2, 4 von 8 Aufträgen erledigt" } ``` **Response 200:** ```json { "ok": true, "savedAt": "2026-05-15T14:23:00Z" } ``` **Empfohlene Frequenz**: jede 30s wenn State sich verändert hat, oder bei jedem Milestone. Nicht öfter (Server-Last). **Achtung**: `state` ist ein opakes Modul-internes Format. Plattform speichert es als JSON-Blob in `module_state_v2`-Tabelle, ohne es zu parsen. Das Feld `summary` ist für die Lehrperson-Anzeige. ## Artifacts hochladen Wenn das Modul Bilder/Audio/Karten produziert, die in der Lehrer-Auswertung sichtbar sein sollen: ```http POST /v2beta/api/artifact Authorization: Bearer Content-Type: multipart/form-data [file] moduleSlug=logistik label=Letzte gewählte Route ``` **Response 201:** ```json { "id": 8842, "url": "/v2beta/sims/logistik/uploads/student-4711/route-final.png", "thumbnail": "/v2beta/sims/logistik/uploads/student-4711/route-final-thumb.png" } ``` URL und Thumbnail dann in `artifacts`-Array des Result-Calls referenzieren. ## Was die Plattform mit dem Result macht 1. Speichert in `assessments_v2` (mit `created_by_version='v2'`) 2. Aktualisiert `student_progress_v2` (Modul-Fortschritt für Lerngeschichte-Anzeige) 3. Aktualisiert `lehrplan_progress_v2` (welche Anker hat Schüler*in abgedeckt) 4. Pushed an Lehrer-Dashboard (Live-Update via Server-Sent-Events) 5. Macht Result für die Klassen-Auswertung sichtbar ## Level-Unlock-Update Beim Result-Empfang aktualisiert die Plattform **automatisch** den Level-Unlock-Status der Schüler*in (in `student_level_unlock_v2`): - `attempts_per_level[difficulty] += 1` - `best_score_per_level[difficulty]` = max von alt und neu - Wenn `completed = true` UND `score / scoreMax >= 0.6` UND Schüler*in war auf `highest_unlocked_level`: - `highest_completed_level = difficulty` - `highest_unlocked_level` = nächster Level aus `manifest.difficultyLevels` (falls vorhanden) Modul muss sich darum nicht kümmern — passiert serverseitig. ## ✅ Geklärt mit Thomas (2026-05-17) - **`freeTextAnswers`**: mit Klarnamen an die Lehrperson (wie V1). Pädagogisch nötig — Lehrperson kann gezielt auf einzelne Schüler*in eingehen. Andere Schüler*innen sehen die Antworten nicht. - **Artifacts löschen (DSGVO)**: per Lehrperson-Cockpit „Sitzung komplett löschen" — eine Aktion löscht Assessment + zugeordnete Artifacts. Separater DELETE-Endpoint kommt mit dem Cockpit. - **`moduleSpecificData`**: bleibt opakes JSON. Cross-Modul-Vergleiche laufen über `score`, `scoreMax`, `lehrplanCoverage` — Modul-eigene Kennzahlen sind Modul-Sache, kein Standardisierungs-Druck.