CG-Feedback-Monitor/CLAUDE.md
2026-07-06 19:08:24 +02:00

164 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# CLAUDE.md — Assignment Monitor
Progressive Web App zur Live-Bewertung von Capgemini-Trainees (Institutees) während Kunden-Assignments.
**Vollständige Dokumentation:** `docs/` — bei neuer Session zuerst `docs/HANDOVER.md` lesen.
---
## Stack
- React 18 + TypeScript + Vite (PWA)
- Tailwind CSS v4 — `@import "tailwindcss"` in CSS, **kein** `tailwind.config.js`; Brandfarbe als `@theme { --color-brand }` in `src/index.css`
- **Lokaler Node/Express-Server + SQLite** (seit 2026-07-04) — löst Dexie/IndexedDB als primären Datenspeicher ab, siehe eigener Abschnitt unten
- React Router v6
- Capgemini-Brandfarbe: `#0070AD` (Tailwind-Klasse `brand`, JS-Konstante `BRAND_COLOR` in `src/config/constants.ts`)
## Projektstruktur (Stand 2026-07-04)
```
src/
config/constants.ts ← RATING_OPTIONS, INST_COLORS, AI_CONFIG, RATING_NUM_MAP, BRAND_COLOR, DEFAULT_RATING_TREND_WEIGHT_STEP
db/
index.ts ← Re-Export (types + queries), rpcClient.ts eingebunden
types.ts ← alle Interfaces, FeedbackRating, AppSettings — von Client UND Server importiert
rpcClient.ts ← rpc(module, fn, ...args) — POST /api/rpc, einziger Netzwerk-Kontaktpunkt des Clients
queries/ ← Client-DB-Zugriffsschicht (siehe eigener Abschnitt unten)
schema.ts, seeds/ ← Dexie/IndexedDB-Legacy-Code, wird nicht mehr gelesen (siehe Datenbank-Abschnitt)
pages/
meeting/ ← MeetingView.tsx, ConversationTab.tsx, AssessmentTab.tsx, CriterionTagPicker.tsx
RatingWeightConfig.tsx ← Settings-Tab für Zeitgewichtungs-Faktor (in Configuration.tsx eingebunden)
...übrige Pages unverändert am alten Ort
utils/
ratingTrend.ts ← zeitgewichteter Bewertungs-Vorschlag + Trend-Erkennung + weightedAverageScore/weightedAverageRating (genutzt von Evaluation.tsx, AssignmentFeedbackPage.tsx, AssessmentTab.tsx, AssignmentDetail.tsx)
server/
index.ts ← Express-App, ein Endpunkt POST /api/rpc
database.ts ← node:sqlite-Verbindung (server/data/assignment-monitor.sqlite3), Schema aus tableRegistry.ts
rpcModules.ts ← Dispatch-Registry { appSettings, consultants, ..., backup } → server/db/queries/*.ts
db/
tableRegistry.ts ← EINE Quelle der Wahrheit: alle 21 Tabellen + Spalten + JSON-/Bool-Codecs (treibt Schema UND Backup)
crud.ts ← generische SQL-CRUD-Bausteine über die Registry
queries/ ← Server-Pendant zu src/db/queries/*.ts, gleiche Funktionsnamen, echtes SQL statt Dexie
scripts/verifyBackupRoundtrip.ts ← automatisierter Vollständigkeits-Beweis für Backup-Export/-Import
```
### Datenbank: lokaler Server statt IndexedDB (seit 2026-07-04)
Grund: Mehrere Browser (Chrome, Edge, ...) auf demselben Laptop sollen dieselben Daten sehen, unabhängig vom Browser-Cache — als Testschritt vor einem späteren Umzug auf einen Docker-Container auf einem Linux-Server. Browser können keine SQLite-Datei direkt öffnen, daher läuft ein kleiner Node/Express-Server (`server/`, Port 4000) mit **`node:sqlite`** (Node-eingebautes SQLite-Modul, **kein** `better-sqlite3`!). Alle Browser sprechen über `http://localhost:5173` (Vite-Dev-Proxy `/api``http://localhost:4000`) denselben Server an.
**Abweichung vom ursprünglich gewählten Stack:** `better-sqlite3` scheiterte lokal an einer fehlenden/kaputten Python-Installation (node-gyp-Kompilierung). `node:sqlite` bietet dieselbe synchrone API (`DatabaseSync`, `.prepare().run()/.get()/.all()`), braucht keine native Kompilierung und ist ab dieser Node-Version (25.x) ohne Flag nutzbar (Node meldet es noch als "experimental" — rein informativ, funktioniert stabil). Falls auf einem anderen Rechner/Server `node:sqlite` fehlt (ältere Node-Version), wäre `better-sqlite3` dort nachzuholen, sofern eine funktionierende Build-Toolchain (Python + C++-Compiler) vorhanden ist.
**RPC statt REST:** Ein einziger Endpunkt `POST /api/rpc` mit Body `{ module, fn, args }` statt ~80 einzelnen Routen — Client (`src/db/rpcClient.ts`) und Server (`server/rpcModules.ts`) sind darüber 1:1 gekoppelt: jede Funktion aus `src/db/queries/*.ts` hat ein gleichnamiges Pendant in `server/db/queries/*.ts`.
**Dexie/IndexedDB ist toter Code, aber noch nicht entfernt:** `src/db/schema.ts` und `src/db/seeds/*.ts` bleiben unverändert im Repo liegen, werden aber von nichts mehr gelesen (Seed-Aufrufe in `main.tsx` wurden entfernt). Vollständiges Entfernen ist eine spätere, bewusste Entscheidung.
**Backup ist jetzt server-seitig:** `src/utils/dbBackup.ts` ruft `rpc('backup', 'exportAll'/'importAll', ...)` statt direkt gegen Dexie zu laufen. `server/db/queries/backup.ts` iteriert exakt `tableRegistry.ts` — dieselbe Liste, die auch das SQL-Schema erzeugt, damit eine neue Tabelle nicht mehr unbemerkt aus dem Backup fallen kann (das ist genau einmal mit `appSettings` passiert, siehe Erledigt-Historie). `server/scripts/verifyBackupRoundtrip.ts` (`npm run verify-backup -- <pfad>`) beweist automatisiert Zeilenzahl- und Inhaltsgleichheit pro Tabelle nach einem Import→Export-Zyklus.
### DB-Zugriffsschicht (`src/db/queries/`, seit 2026-07-04, intern auf RPC umgestellt)
Alle Komponenten greifen auf Daten ausschließlich über benannte Funktionen aus `src/db/queries/*.ts` zu (re-exportiert über `'../db'`) — nach außen unverändert seit der ursprünglichen Extraktion, nur die Innenseite ruft jetzt `rpc(...)` statt `db.<table>.*` auf. Ein Modul pro Domäne: `appSettings.ts`, `consultants.ts` (Groups+Consultants), `assignmentTypes.ts` (AssignmentTypes+PhaseTemplates), `assignments.ts`, `feedbackStructure.ts` (Dimensions/Categories/CriterionItems — `filterVisibleFeedbackStructure()` bleibt reine Client-Funktion ohne RPC), `meetings.ts` (MeetingInstances inkl. kanonischem `!deletedAt && status==='done'`-Filter als `listDoneMeetingsChronological()`, Cascade-Deletes, Export-Datenaggregation `getMeetingExportData()`), `conversationEntries.ts`, `assessments.ts`, `assignmentFeedback.ts`.
## Entwicklung
```bash
npm run dev:all # Frontend (Vite, :5173) + lokaler Server (Express, :4000) zusammen starten
npm run dev # Nur Frontend (Server muss separat laufen, sonst laufen alle DB-Zugriffe ins Leere)
npm run server # Nur den lokalen Server
npx tsc -b # TypeScript-Check über alle 3 Projekte (App/Node/Server) — muss vor jeder Abgabe fehlerfrei sein
```
**Wichtig:** `npx tsc --noEmit` am Root prüft **nichts** (0 Dateien, `tsc --noEmit --listFiles` bestätigt das) — der Root-`tsconfig.json` hat `"files": []` und nur `references`. Ohne `-b` (Build-Modus) werden referenzierte Projekte nicht ausgewertet. Der bisher in diesem Dokument dokumentierte Workflow `npx tsc --noEmit` war dadurch vermutlich seit Einführung der Project References ein stiller No-Op — **immer `npx tsc -b` verwenden.**
---
## Kritische Eigenheiten
### Doppelte `criteriaId`-Semantik
```
Assessment.criteriaId → FeedbackCriterionItem.id (Gesamtbewertung)
ConversationSkillScore.criteriaId → FeedbackCategory.id (Schnellbewertung pro Protokoll-Eintrag)
ConversationLineTag.criterionItemId → FeedbackCriterionItem.id (Kriterium-Zuordnung einer einzelnen Zeile innerhalb eines ConversationEntry.note-Blocks, seit 2026-07-02 — bewusst anders benannt als "criteriaId" um obige Verwechslungsgefahr nicht fortzuschreiben)
```
### AssignmentType.criteriaIds
Referenziert `FeedbackCriterionItem.id[]`. Leer = alle Kriterien anzeigen.
Filterlogik: `filterVisibleFeedbackStructure(allCategories, allItems, criteriaIds)` in `src/db/queries/feedbackStructure.ts` — seit dem `db/queries/`-Refactoring (2026-07-04) die einzige Implementierung, vorher identisch dupliziert in `MeetingView.tsx` und `AssignmentDetail.tsx`. Nicht erneut inline schreiben.
### Rating-Skala
```typescript
type FeedbackRating = 'na' | 'not_client_ready' | 'partially_client_ready' | 'nearly_client_ready' | 'client_ready'
// Numerisch: na=0, not=1, partially=2, nearly=3, fully=4
// N/A gilt als "nicht gesetzt" — aus allen Berechnungen ausschließen!
```
Aggregation: gewichteter Durchschnitt → `avg < 1.5` Not · `< 2.5` Partially · `< 3.5` Nearly · `≥ 3.5` Fully
**Bug-Klasse, auf die immer prüfen:** Filter der Form `a.score !== null` schließen N/A NICHT aus (N/A ist der String `'na'`, nicht `null`!). Richtig ist immer `a.score !== null && a.score !== 'na'`. Gefunden und gefixt in `Evaluation.tsx` (2026-07-02); dieselbe Prüfung nötig überall, wo `Assessment.score` oder `ConversationSkillScore.score` gemittelt wird.
**Zweite Bug-Klasse, auf die immer prüfen (Kriterium-Gewichtung):** `FeedbackCriterionItem.weight` geht seit 2026-07-03 von `0,0` bis `2,0` (Config → Feedback, vorher grobe `×1``×5`-Stufen). Jede gewichtete Mittelung MUSS normieren — `Σ(score×weight) / Σ(weight)`, niemals `score×weight` direkt als Endwert interpretieren (würde die Bewertungsstufe verzerren, z.B. Score 3 „Nearly" × Gewicht 0,5 = 1,5, was fälschlich zwischen Not/Partially läge). Zusätzlich: wenn `Σweight === 0` (z.B. alle beteiligten Kriterien einer Kategorie auf Gewicht 0 gesetzt), **nicht** dividieren — `NaN < 1.5/2.5/3.5` ist überall `false` und fällt sonst still auf den letzten Bucket „Fully" durch. Immer `if (wSum === 0) return null` (bzw. `continue`) vor der Division. Seit dem `db/queries/`-Refactoring (2026-07-04) **eine** Implementierung: `weightedAverageScore()`/`weightedAverageRating()` in `src/utils/ratingTrend.ts`, genutzt von `buildGroupMeetingScores()`, `catMode()` (`AssessmentTab.tsx`) und der Meeting-Chip-Anzeige in `AssignmentDetail.tsx` (dort fehlte der `Σweight===0`-Guard vorher — beim Konsolidieren mitgefixt). Bei jedem neuen gewichteten Durchschnitt diese Funktion wiederverwenden, nicht neu implementieren.
### Routing
```
/assignment/:id/meeting/:meetingId ← meetingId ist MeetingInstance.id, nicht Assignment-ID
```
MeetingView liest: `const { id, meetingId: meetingIdParam } = useParams()`
### Sonstige Fallstricke
- `MeetingInstance.generalNotes` kann `undefined` sein → immer `?? ''` als Fallback
- `PhaseKey` ist `string`, kein Union-Typ
- Tailwind v4: Brandfarbe ist `brand` (Tailwind-Klasse, via `@theme` in `index.css`) — **kein** `bg-[#0070AD]` mehr neu schreiben
- Auswertungslogik greift auf **zwei komplett unabhängige Bewertungs-Strukturen** zu, die nicht automatisch synchron sind:
- `Assessment` (Tabelle `assessments`) — pro Meeting erfasste Kriterium-Bewertungen, Basis für `/evaluation`
- `FeedbackCategoryRating` — ausschließlich manuell auf `AssignmentFeedbackPage` gesetzt, unabhängig von Meeting-Daten, Basis für das finale Abschluss-Feedback
- Seit 2026-07-02: `src/utils/ratingTrend.ts` berechnet aus den `Assessment`-Daten einen **zeitgewichteten Vorschlag** pro Kategorie (spätere Meetings zählen stärker, Gewichtungsfaktor konfigurierbar unter Config → Gewichtung) inkl. Trend-Erkennung (Verbesserung/Verschlechterung). Der Vorschlag wird auf `AssignmentFeedbackPage` als Badge angezeigt und bleibt frei überschreibbar — die zwei Strukturen bleiben bewusst getrennt.
- Meeting-Filter bei Aggregationen über mehrere Meetings: immer `!m.deletedAt && m.status === 'done'` — sonst fließen Papierkorb-/unfertige Meetings mit ein
- **Weichmacher/Füllwörter in Formulierungen** (z.B. "ein bisschen") werden bewusst **nicht** über eine eigene Text-Erkennungslogik behandelt — stattdessen legt man dafür ein eigenes, niedrig gewichtetes Kriterium an (z.B. "Sprachliche Präzision", Gewicht < 1) und taggt/notiert entsprechende Zeilen ganz normal (meist mit `(!)`). Die bestehende Tagging- + Notation-Vorschlag-Mechanik deckt das automatisch ab.
- Der `äh`-Zähler (`ConversationEntry.fillerCount`) bleibt bewusst **rein informativ** keine automatische Schwellenwert-Bewertung. Würde eine eigene Design-Entscheidung brauchen (ab wann gilt die Zahl als "zu hoch"), aktuell reicht die reine Anzeige als Grundlage für die manuelle Bewertung.
- `ConversationEntry` entspricht weiterhin **einem ganzen Sprecher-Turn** (ein zusammenhängender, mehrzeiliger Text-Block bis "✓ Schließen") bewusst so belassen, ein Zwischenstand mit "ein Entry pro Zeile" wurde am 2026-07-02 wieder verworfen, weil er die Lesbarkeit des Protokolls zerstört hat. Kriterium-Zuordnung passiert stattdessen **pro Zeile innerhalb** eines Entry über `ConversationEntry.lineTags` (Matching per exaktem Zeilentext, nicht per Index robust gegenüber nachträglichem Einfügen/Löschen von Zeilen). Zeilen splitten immer über `splitNoteLines()` in `src/utils/notationParser.tsx`, nicht erneut `note.split('\n')` inline schreiben (sonst laufen Trim-/Filter-Verhalten auseinander).
---
## Stil-Regeln
- **Keine Kommentare** außer für nicht-offensichtliche Invarianten (kein Was", nur Warum")
- **Keine Auto-Resize-Textarea via `rows={1}` + JS** stattdessen View/Edit-Toggle oder `fieldSizing: 'content'` als Inline-Style
- **`RATING_OPTIONS`-Array** nicht neu definieren immer aus dem bestehenden Array in der jeweiligen Datei verwenden
- **Kein `useEffect` für DB-Calls** außer bei initialem Load lieber direkte `async`-Funktionen
---
## Offene Punkte (priorisiert)
| Prio | Aufgabe | Datei |
|---|---|---|
| H | **KI-Generierung derzeit funktionsunfähig**: `generateWithAi()` schlägt beim Ausführen fehl, vermutlich blockiert das Capgemini-Netzwerk/Firewall den Call zu `openrouter.ai`. Root Cause noch nicht untersucht ggf. mit internem IT-Support klären. | `src/pages/AssignmentFeedbackPage.tsx` |
| H | **Größeres Vorhaben (noch nicht gescoped):** Mehrstufiges, pro Dimension konfigurierbares Prompt-System mit Spezialwissen je Dimension + Platzhaltersystem Ablösung des aktuellen Einzel-Prompts in `buildAiPrompt`. Braucht eigene Design-Session. | `src/pages/AssignmentFeedbackPage.tsx` |
| M | **Selbstlernendes Tagging (Idee, noch nicht gescoped):** Zeile-zu-Kriterium-Zuordnungen sammeln und daraus beim Tippen automatische Tag-Vorschläge ableiten (ähnliche Formulierungen gleiches Kriterium). Braucht eigene Design-Session (Ähnlichkeits-/Lernmechanik, Datenbasis, UI). | `src/pages/meeting/CriterionTagPicker.tsx` |
| M | **Migration der echten Daten aussteht:** Aktuelle IndexedDB-Daten müssen noch einmalig über Config Backup Export (alte Version) exportiert und über Config Backup Import (neue, serverbasierte Version) in die SQLite-DB eingespielt werden. Siehe Datenbank-Abschnitt oben. | |
| L | Dexie/IndexedDB-Legacy-Code vollständig entfernen (`src/db/schema.ts`, `src/db/seeds/`) aktuell bewusst nur stillgelegt, nicht gelöscht | `src/db/schema.ts`, `src/db/seeds/` |
| L | Server-seitiges "Seed-if-empty" für Assignment-Typen/Feedback-Struktur (aktuell nur clientseitig vorhanden und nicht mehr wirksam) nicht dringend, da der Backup-Import denselben Zweck erfüllt | `server/db/queries/` (neu) |
| L | SQLite WASM + OPFS Migration durch den lokalen Server-Ansatz (2026-07-04) überholt/hinfällig | `src/db/schema.ts` |
**Erledigt (2026-07-04, zweite Runde):** Lokaler Node/Express-Server mit `node:sqlite` (statt IndexedDB) eingeführt, RPC-Bridge (`POST /api/rpc`) zwischen `src/db/queries/*.ts` (Client) und neuem `server/db/queries/*.ts` (Server) siehe eigener Abschnitt "Datenbank: lokaler Server statt IndexedDB" oben für Details, Abweichungen (node:sqlite statt better-sqlite3) und die wichtige `npx tsc -b`-statt-`--noEmit`-Korrektur. Ziel: mehrere Browser auf demselben Laptop teilen sich dieselben Daten, unabhängig vom Browser-Cache, als Zwischenschritt vor einem Docker/Linux-Deployment.
**Erledigt (2026-07-04):** `db/queries/`-Layer eingeführt alle 20 Komponenten/Utils mit vorherigem direktem `db.<table>.*`-Zugriff nutzen jetzt benannte Funktionen aus `src/db/queries/*.ts` (siehe eigener Abschnitt oben), reine Extraktion ohne beabsichtigte Verhaltensänderung. Dabei bewusst mitkonsolidiert: die drei unabhängig implementierten Varianten der gewichteten-Durchschnitt-Logik (`ratingTrend.ts`, `AssessmentTab.tsx`, `AssignmentDetail.tsx`) laufen jetzt über eine einzige Funktion `weightedAverageScore()`/`weightedAverageRating()` in `ratingTrend.ts` dabei einen fehlenden `Σweight===0`-Guard in `AssignmentDetail.tsx` gefixt (identische Bug-Klasse wie die zweite Bug-Klasse oben, dort war er noch nicht behoben). Ebenfalls konsolidiert: die "AssignmentType.criteriaIds schränkt sichtbare Kriterien ein"-Filterlogik (vorher dupliziert in `MeetingView.tsx`/`AssignmentDetail.tsx`, jetzt `filterVisibleFeedbackStructure()`) und die doppelten Datenabfragen in `meetingExport.ts`s Markdown-/JSON-Export (jetzt `getMeetingExportData()` in `meetings.ts`). `dbBackup.ts` und Seed-Code bleiben bewusst bei direktem `db`-Zugriff (siehe Abschnitt oben). `npx tsc --noEmit` läuft fehlerfrei.
**Erledigt (2026-07-02):** Konfiguration zentralisiert (`config/constants.ts`), `db/index.ts` in types/schema/seeds aufgeteilt, `MeetingView.tsx` in 3 Dateien aufgeteilt (`pages/meeting/`), `Evaluation.tsx` auf `feedbackCriterionItems` migriert + nach Assignment gruppiert + Papierkorb/Status-Filter + N/A-Bug gefixt, `zustand` entfernt (war ungenutzt). Zeitgewichteter Bewertungs-Vorschlag mit Trend-Erkennung umgesetzt (`src/utils/ratingTrend.ts`, neue Tabelle `appSettings`, neuer Config-Tab Gewichtung“, Vorschlags-Badge + Übernehmen/Trend-in-Text auf `AssignmentFeedbackPage`, `buildAiPrompt` um Trend-Abschnitt erweitert und Legacy-`db.criteria`-Bug dabei gefixt). Kriterium-Zuordnung für Freitext-Notizen umgesetzt: `ConversationEntry.lineTags` (Zeilentext Kriterium, nach einem verworfenen Zwischenstand mit "ein Entry pro Zeile" siehe Sonstige Fallstricke), `CriterionTagPicker.tsx` (Chip+Popover mit Suche, unverändert seit erster Version) wird pro Zeile direkt an der Zeile via `NotationText`s neuer optionaler `renderLineAddon`-Prop gerendert; Zuordnung passiert bewusst nur nachträglich (nicht live während des Tippens), um den Schreibfluss nicht zu stören. `buildAiPrompt` führt Assessment-Notizen und getaggte Zeilen jetzt pro Kriterium zusammen, unzugeordnete Zeilen bleiben im allgemeinen Protokoll-Block. **(2026-07-03)** Vier Folge-Fixes: `meetingExport.ts` löste Kriterien über die Legacy-Tabelle `db.criteria` auf (identischer Bug wie der frühere `buildAiPrompt`-Fix) dadurch war die Gesamtbewertung im Export faktisch leer; jetzt auf `feedbackCriterionItems` umgestellt, Markdown-Export zeigt zusätzlich das Kriterium pro getaggter Protokollzeile. `dbBackup.ts`s `TABLE_NAMES` enthielt `appSettings` nicht Voll-Backups verloren die Gewichtungs-Config stillschweigend; jetzt ergänzt samt Kommentar, dass neue Tabellen dort manuell nachgezogen werden müssen. Notation Bewertungs-Vorschlag: `detectNotationRating()` in `notationParser.tsx` (`!`→Not, `(!)`Partially, `(+)`Nearly, `+`Fully) kombiniert mit `lineTags` liefert in `AssessmentTab.tsx` einen "Vorschlag aus Protokoll"-Badge pro Kriterium-Item, unabhängig vom bestehenden "Aus Protokoll"-Button (andere Datenquelle). `AssignmentCreate.tsx`: Assignment-Nummer ist jetzt Pflichtfeld (nur auf Formular-Ebene, Typ bleibt optional wegen Altdaten). Kriterium-Gewichtung auf `0,02,0` in `0,1`-Schritten verfeinert (`FeedbackStructureConfig.tsx`, vorher `×1``×5`), dabei einen latenten Division-durch-Null-Bug gefixt, der durch das neu erlaubte Gewicht `0` real wurde (`catMode()` in `AssessmentTab.tsx`, `buildGroupMeetingScores()` in `ratingTrend.ts` siehe zweite Bug-Klasse oben). Sprecher eines bereits erfassten Protokoll-Beitrags kann nachträglich geändert werden (`ConversationTab.tsx`, im Bearbeiten-Modus neben Füllwörtern ändert sofort `ConversationEntry.instituteeId`, kein separater Speichern-Schritt nötig). N/A-Aggregationsbug in `buildAiPrompt` (`AssignmentFeedbackPage.tsx`) gefixt war die letzte offene Stelle dieser Bug-Klasse außer der jetzt migrierten `FeedbackDraftPage.tsx`. `FeedbackDraftPage.tsx` vollständig auf `feedbackCriterionItems`/`feedbackCategories` migriert (letzter Konsument der Legacy-Tabellen ist jetzt nur noch der Seed-Code), Notizen jetzt korrekt pro Kriterium über `lineTags` statt aller Protokoll-Notizen ungefiltert an jedes Kriterium gehängt, Score-Anzeige auf `RATING_OPTIONS`-Labels umgestellt (statt `/5`-Rohwert). Toter Code (verwaiste AssignmentType/PhaseTemplate-CRUD-Reste) aus `FeedbackStructureConfig.tsx` entfernt. Dashboard zeigt keine Assignment-Nummer" war bereits gelöst, nur die Doku war veraltet richtiggestellt.
---
## Datenschutz
- Alle Daten lokal seit 2026-07-04 in `server/data/assignment-monitor.sqlite3` (lokaler Server auf demselben Rechner) statt im Browser-IndexedDB; weiterhin **kein Cloud-Upload**, Server ist nur unter `localhost` erreichbar (kein Auth-Schutz nötig, solange das so bleibt siehe Offene Punkte, falls sich das ändert, z.B. bei Docker/Linux-Deployment mit Netzwerkzugriff)
- `server/data/` ist per `.gitignore` ausgeschlossen Datenbankdatei nie committen
- GDPR-relevant: Institutee-Namen, Kundennamen, Gesprächsnotizen
- KI-API-Key in `localStorage` nie loggen oder ausgeben
- Backup-JSON enthält alle personenbezogenen Daten vertraulich behandeln