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

21 KiB
Raw Blame History

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 /apihttp://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

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

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.tss 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 NotationTexts 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.tss 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