# 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; offene Punkte kanonisch: `docs/STATUS_UND_OFFENE_PUNKTE.md` (Stand 2026-08-16, inkl. C6/C7/T2 erledigt). **DEV-Isolation:** Dieses Repo (`AssigmentMonitorV2`) ist von Produktiv (`c:\dev\AssigmentMonitor`) getrennt — Ports **5174/4001**, DB `assignment-monitor-v2-dev.sqlite3`. Regeln: `docs/DEV_ISOLATION.md`. Produktiv-Code und -Daten niemals anfassen. --- ## Stack - **React 19** + 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 7** (`react-router-dom`) - Capgemini-Brandfarbe: `#0070AD` (Tailwind-Klasse `brand`, JS-Konstante `BRAND_COLOR` in `src/config/constants.ts`) - Bewertungsskala: **1–10** (`RatingScore`) — siehe `docs/SCALE_1_TO_10.md` (nicht mehr Client-Ready-Enum) ## Projektstruktur (Stand 2026-08; Erledigt-Historie unten weiter gültig) ``` src/ config/constants.ts ← RATING_SCORES, Score-Styles, AI_CONFIG, BRAND_COLOR, DEFAULT_RATING_TREND_WEIGHT_STEP, … db/ index.ts ← Re-Export (types + queries) types.ts ← alle Interfaces, RatingScore, 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) pages/ meeting/ ← MeetingView.tsx, ConversationTab.tsx, AssessmentTab.tsx, CriterionTagPicker.tsx RatingWeightConfig.tsx ← Settings-Tab für Zeitgewichtungs-Faktor (in Configuration.tsx eingebunden) AssignmentTrash.tsx ← Papierkorb-Ansicht für Assignments (`/assignments/trash`), seit 2026-07-13 AssignmentArchive.tsx ← Archiv-Ansicht für Assignments (`/assignments/archive`), seit 2026-07-13 MeetingHistory.tsx ← Rein lesendes chronologisches Gesamtprotokoll aller Meetings eines Assignments (`/assignment/:id/history`), seit 2026-07-13 AiPromptConfig.tsx ← Settings-Tab „KI-Prompts" — ein editierbares Prompt-Template je Dimension, Default-Vorbelegung + Reset (in Configuration.tsx eingebunden, seit 2026-07-07) ...ü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) aiPrompt.ts ← Prompt-Engine für die KI-Feedback-Generierung (seit 2026-07-07): Platzhalter-Kontext pro Dimension, Rendering, Antwort-Parsing — siehe eigener Abschnitt „KI-Feedback-Generierung" unten pdfExport.tsx ← PDF-Export (seit 2026-07-14, `@react-pdf/renderer`): Assignment-Report (Gesamtprotokoll + Feedback aller Institutees) und Einzel-Feedback-PDF pro Institutee server/ index.ts ← Express-App, ein Endpunkt POST /api/rpc database.ts ← node:sqlite-Verbindung (V2: assignment-monitor-v2-dev.sqlite3), Schema aus tableRegistry.ts, Pfad-Guards rpcModules.ts ← Dispatch-Registry inkl. checklists, meetingCategoryRatings, assignmentTransfer, backup, … db/ tableRegistry.ts ← EINE Quelle der Wahrheit: alle 26 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 auf demselben Laptop sollen dieselben Daten sehen. Browser können keine SQLite-Datei direkt öffnen, daher lokaler Node/Express-Server mit **`node:sqlite`** (**kein** `better-sqlite3`!). **V2-Defaults:** API Port **4001**, Vite **5174**, Proxy `/api` → `http://localhost:4001`, Datei `assignment-monitor-v2-dev.sqlite3`. (Produktiv bleibt 4000/5173 — nie vermischen, siehe `docs/DEV_ISOLATION.md`.) **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. **Spalten-Migration bei bestehenden Tabellen (seit 2026-07-07):** `database.ts` legt fehlende Tabellen per `CREATE TABLE IF NOT EXISTS` an — das migriert aber **nie** eine bereits existierende Tabelle. Wird `tableRegistry.ts` um eine neue Spalte einer bereits vorhandenen Tabelle ergänzt (z.B. `suggestedAchievements` auf `feedbackDimensionTexts`), gleicht `database.ts` das zusätzlich per `PRAGMA table_info` ab und zieht fehlende Spalten per `ALTER TABLE ... ADD COLUMN` automatisch nach. Ohne das bleibt die alte Spaltenmenge bestehen und jeder Zugriff auf die neue Spalte scheitert erst zur Laufzeit mit „no column named ..." — genau das ist beim Einführen des KI-Prompt-Systems passiert und wurde dabei gefixt. **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-Legacy entfernt (2026-08-16):** `dexie`-Dependency, `src/db/schema.ts` und `src/db/seeds/` sind weg. Primärspeicher bleibt Express + `node:sqlite`; Struktur-Seed über Starter/`npm run setup`. **Backup ist server-seitig** über `tableRegistry`. Zusätzlich: **Starter-Paket** (nur Struktur, `docs/starter/`, `npm run setup`) und **Assignment-Transfer** (ein Assignment inkl. Berater/Notizen/Checklisten, Merge-Import — `assignmentTransfer`). ### DB-Zugriffsschicht (`src/db/queries/`, seit 2026-07-04, intern auf RPC umgestellt) Module u.a.: `appSettings`, `consultants`, `assignmentTypes`, `assignments`, `feedbackStructure` (`resolveVisibleStructure`/`filterVisibleFeedbackStructure`), `meetings` (`listDoneMeetingsChronological`, `getMeetingExportData`), `conversationEntries`, `assessments`, `assignmentFeedback`, `meetingCategoryRatings`, `checklists`, `criterionSuggestions`, **`assignmentTransfer`**. ## Entwicklung ```bash npm run dev:all # Frontend :5174 + Server :4001 npm run setup # Starter-Struktur einspielen (Feedback/Typen/Prompts/Checklisten-Defs) npm run pack:zip # Team-ZIP → dist-release/ (ohne DB) npm run pack:laptop # Umzug: Code + volle Dev-DB → dist-release/ npm run starter:export npx tsc -b # TypeScript — 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 RatingScore = number // 1–10; unset = null — siehe docs/SCALE_1_TO_10.md // Farbbänder in appSettings.ratingColorBands / Config → Skala // N/A-String-Enum (not_client_ready …) ist abgelöst — nicht neu einführen ``` Aggregation: gewichteter Durchschnitt über `weightedAverageScore`/`weightedAverageRating` in `ratingTrend.ts` — bei `Σweight === 0` null. ### 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. - **Cross-Boundary-Imports von `src/` nach `server/`**: Dateien, die (wie `src/db/types.ts`) sowohl vom Client als auch vom Server importiert werden, müssen frei von Laufzeit-Abhängigkeiten auf Browser-Only-Code sein — auch `import type` reicht nicht als Schutz, wenn die importierte Datei selbst wieder aus dem `'../db'`-Barrel importiert (der zieht `rpcClient` mit, siehe `src/config/constants.ts`-Fix vom 2026-07-07: Import auf `'../db/types'` direkt umgestellt). Jede neue Datei dieser Art muss zusätzlich explizit in `tsconfig.server.json`s `include` aufgenommen werden (sonst „File is not listed within the file list of project" bzw. sie wird stillschweigend nicht mitkompiliert) — Muster: `src/db/types.ts`, `src/config/constants.ts`. - `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 - **Score-UI** über `ScorePicker` / `RATING_SCORES` / `resolveScoreStyle` — keine alten Enum-`RATING_OPTIONS` neu erfinden - Gewichtete Mittel immer über `weightedAverageScore`/`weightedAverageRating` in `ratingTrend.ts` - **Kein `useEffect` für DB-Calls** außer bei initialem Load — lieber direkte `async`-Funktionen --- ## Offene Punkte (priorisiert) **Kanonisch und aktuell:** `docs/STATUS_UND_OFFENE_PUNKTE.md` (2026-08-16). Kurz: | Prio | Aufgabe | |---|---| | H | **UX.3** Dashboard · **UX.4** Meeting-Live-UX | | M | UX.5–UX.8; **T4** Cascade Smoke; **T1** npm-Audit; **T5** Legacy; **T3** Backup-Reminder; **T6** Datenschutz | | L | Create-Enrollments, Assignment-Nr. in URL | | — | **C0/C6/C7/T2/UX.1–UX.2** + Cascade 1–3/1b: **erledigt** | **Erledigt (2026-08-16):** C0 Skala-Smoke, C6 Tagging-Batches, C7 KI-Client, T2 Dexie-Legacy, UX.1 Design-Tokens + UX.2 App-Chrome (`index.css`, `App.tsx`, Source Sans 3). **Erledigt (2026-07-19): Live-Formulierungshilfe, Beobachter-Fokus, Meeting-Checkliste umgesetzt** (Konzept siehe Eintrag unten, dort auch die Design-Begründungen). Neue Dateien: `src/utils/criterionOptions.tsx` (aus `FeedbackStructureConfig.tsx` extrahiertes `criterionOptionGroups()`, jetzt auch von `AssignmentTypeConfig.tsx` genutzt), `server/db/queries/checklists.ts` + `src/db/queries/checklists.ts` (neues RPC-Modul `checklists`), `src/pages/meeting/ChecklistTab.tsx` (neuer dritter Meeting-Tab, nur sichtbar wenn `PhaseTemplate.hasChecklist`). `AssignmentTypeConfig.tsx` bekam pro Phase eine „Checkliste"-Checkbox + aufklappbaren Punkte-Editor (Label, Scope-Auswahl, bei `perInstitutee` optionales Kriterium-Dropdown). `deletePhaseTemplate`/`deleteAssignmentType` cascaden jetzt zusätzlich über `checklistItemDefs` (gleiches Verteidigungsprinzip wie bei `deleteFeedbackCriterionItem`). Live-Formulierungshilfe: neuer State `corpus` in `MeetingView.tsx` (einmal pro Meeting-Load geholt, nicht pro Tastendruck), debounced (~800ms) Aufruf von `findSimilarTaggedLines()` sowohl in `ConversationTab.tsx` (aktive Notiz) als auch für die allgemeinen Notizen — rein lesendes „💭 Ähnliche frühere Notizen"-Panel. Fokus-Übersicht als neue aufklappbare „📊 Fokus-Übersicht"-Box in `FeedbackStructureConfig.tsx` (Top 5 Kriterien nach `getCriterionUsageMap()`-Verwendung, inkl. `learnedPatternHints` als Kontext). Verifiziert: `npx tsc -b` fehlerfrei (dabei einen fehlenden `hasChecklist`-Wert im toten Dexie-Seed-Code `src/db/seeds/assignmentTypes.ts` nachgezogen, da dieser weiterhin vom Client-Build mitkompiliert wird); neue RPC-Endpunkte (`checklists.listChecklistItemDefsForPhaseTemplate`, `assignmentTypes.listPhaseTemplates`) direkt gegen die laufende Dev-Instanz getestet — Schema-Migration (neue Spalte `hasChecklist`, neue Tabellen `checklistItemDefs`/`checklistResponses`) griff automatisch ohne manuellen Server-Neustart. Manueller Ausfüll-Test einer Checkliste in einem echten Meeting (inkl. Prüfung, dass verknüpfte Punkte korrekt in `Assessment` landen) steht noch aus. **Nachtrag (2026-07-19, nach erstem echten Nutzungstest):** Drei Korrekturen an der Checkliste, nachdem sich die ursprüngliche Einschränkung in der Praxis als zu eng erwies: 1. **Kriterium-Verknüpfung jetzt für beide Scopes möglich** (vorher nur `perInstitutee`). Bei `scope: meetingWide` + verknüpft schreibt `MeetingView.tsx::saveChecklistResponse` dieselbe Bewertung/Notiz per Fan-out in `Assessment` **für jeden aktuellen Institutee gleichzeitig** — ein meeting-weiter Checkpunkt gilt für alle Beteiligten gleichermaßen. Löst das ursprüngliche Problem („`Assessment` verlangt `instituteeId`") durch Duplizieren statt durch Ausschluss. 2. **`ChecklistResponse` ist jetzt immer die Quelle für die Checkliste-UI selbst**, unabhängig von Kriterium-Verknüpfung (vorher las/schrieb die UI bei verknüpften Punkten direkt `Assessment`, was zu einem unsauberen Round-Trip führte, sobald die Notiz auch das Checklisten-Label enthalten sollte). Bei Verknüpfung spiegelt `saveChecklistResponse()` zusätzlich **einseitig** (Checkliste → Assessment, nie zurück) nach `Assessment`, inkl. komponierter Notiz `Checkliste „