CG-Feedback-Monitor/CLAUDE.md
Lars 234f87d408 V2-Dev-Stand: Skala 1-10, Benefits/Concerns, DEV-Isolation und Gitea-Doku.
Bündelt die Neuentwicklung in AssigmentMonitorV2 (eigene Ports/DB), die Umstellung auf numerische Bewertungen, Meeting-Checklisten, KI-Tagging und die geplante Feedback-Kaskade — als Basis für Versionsverwaltung in Gitea.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-08 17:35:05 +02:00

244 lines
70 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.
**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 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)
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 (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.
**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 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.
- **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 Dexie/rpcClient/Seeds 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
- **`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 |
|---|---|---|
| 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-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 „<Label>" (<Bewertung>): <Kommentar>` über `composeChecklistNote()` dadurch landet der Checkpunkt-Name selbst (nicht nur der freie Kommentar) in `aiPrompt.ts::kriterienScores`, das `Assessment.note` bereits einsammelt; kein Änderung an `aiPrompt.ts` nötig. Bekannte Einschränkung: Wird dasselbe Kriterium zusätzlich direkt über den (weiterhin vorhandenen) Gesamtbewertungs-Tab bearbeitet, gewinnt der jeweils letzte Schreibzugriff gleiches Verhalten wie beim bestehenden Aus Protokoll"-Button, keine neue Fehlerklasse.
3. **Keine eigene Checkliste-Tab-Karte mehr** `ChecklistTab` wird jetzt direkt oberhalb der Gesprächs-Zeitleiste innerhalb des Gesprächsprotokoll-Tabs gerendert (`tab === 'conversation'`), nicht mehr über einen separaten dritten Tab. Der Tab-Button Gesprächsprotokoll" erscheint entsprechend schon bei `hasConv || hasChecklist`, damit auch reine Checklisten-Phasen (ohne Protokoll-Funktion) eine Heimat haben.
**Sechster Nachtrag (2026-07-20): Bug + fehlende Diagnose bei „Vorschläge analysieren" behoben.** Nutzer meldete: Aufruf lieferte keine Vorschläge, ohne jede Rückmeldung, was schiefläuft. Zwei Ursachen gefunden und behoben (`MeetingView.tsx::analyzeSuggestions`):
1. **Bug:** Checklisten-Punkte ohne effektives Kriterium wurden nur analysiert, wenn bereits ein Kommentar eingetragen war (`if (!comment) continue`) reine Bewertungen ohne Freitext fielen dadurch komplett aus der Analyse heraus, obwohl der Checklisten-Punkt-Name (Label) selbst schon ein brauchbares Signal für die KI ist. Jetzt wird bei fehlendem Kommentar das Label allein als Text übergeben (`comment ? \`${ci.label}: ${comment}\` : ci.label`).
2. **Fehlende Diagnose:** Sowohl das stille `return` bei `lineRefs.length === 0` (nichts zu analysieren) als auch der Fall KI hat für keine Zeile/keinen Punkt einen Vorschlag geliefert" gaben bisher keinerlei Rückmeldung. Neu: `suggestionInfo`-Statuszeile (blau, neben dem bestehenden roten Fehlerbanner) mit Zählung X Zeile(n)/Punkt(e) analysiert, Y Vorschläge erhalten" sowie ein aufklappbares „🔍 Rohantwort ansehen"-Panel (`lastAnalysisRaw`), das die komplette, ungeparste KI-Antwort zeigt macht Format-/Parsing-Probleme künftig direkt sichtbar statt raten zu müssen.
**Fünfter Nachtrag (2026-07-20): Checkliste in allen Export-/Berichts-Pfaden nachgezogen.** Bei der Prüfung stellte sich heraus, dass sämtliche bestehenden Export-/Berichtsfunktionen die Checkliste komplett ignorierten (vor Einführung der Checkliste geschrieben, nicht mitgepflegt). Nachgezogen:
- `server/db/queries/meetings.ts::getMeetingExportData()` liefert jetzt zusätzlich `checklistItems`/`checklistResponses` (nur wenn `template.hasChecklist`) einzige Datenquelle für Meeting-Export, dadurch automatisch in MD/JSON verfügbar.
- Neue Bulk-Funktionen `listChecklistItemDefsForPhaseTemplates()`/`listChecklistResponsesForMeetings()` (Server + Client) analog zu `listAssessmentsForMeetings()` für Berichte über mehrere Meetings/Phasentypen hinweg (`MeetingHistory.tsx`, Assignment-Report-PDF), vermeidet N+1-Abfragen.
- `src/utils/meetingExport.ts` (Markdown + JSON), `src/utils/pdfExport.tsx::AssignmentReportDocument` (PDF) und `src/pages/MeetingHistory.tsx` (Gesamtprotokoll-Ansicht) zeigen die Checkliste jetzt jeweils in derselben Position (nach dem Gesprächsprotokoll, vor der Gesamtbewertung) Label, effektives Kriterium (individuelle Zuordnung > Konfigurations-Vorgabe, gleiche Auflösung wie in `MeetingView.tsx`), Bewertung, Kommentar.
- `src/utils/structureExport.ts` (Markdown) und `pdfExport.tsx::AssignmentTypesDocument` (PDF) — der Assignment-Typen-Bericht listet jetzt pro Phase zusätzlich `hasChecklist` und die konfigurierten Checklisten-Punkte (inkl. Scope und ggf. verknüpftem Kriterium), analog zum bereits bestehenden Kriterien-Mapping.
- Bewusst nicht angefasst: `AssignmentDetail.tsx`s Chip-Reihe (nur Rating-Aggregation, kein direkter Bezug zur Checkliste) — wäre ein separates, kleines UI-Ergänzung, kein Export/Bericht im eigentlichen Sinn.
**Vierter Nachtrag (2026-07-19): „Vorschläge analysieren" + Kriterium-Vorschläge jetzt auch für die Checkliste, individuelle Zuordnung pro Antwort.** Bisher war die Kriterium-Verknüpfung eines Checklisten-Punkts ausschließlich in der Config fix hinterlegt (`ChecklistItemDef.linkedCriterionItemId`) und für „🏷 Vorschläge analysieren" unsichtbar. Neu:
- **`ChecklistResponse.criterionItemId`** (neue, automatisch migrierte Spalte) überschreibt pro Antwort individuell die Konfigurations-Vorgabe — `getEffectiveChecklistCriterion()` in `MeetingView.tsx` liefert `response.criterionItemId ?? ci.linkedCriterionItemId ?? null`. Zuordnung direkt in `ChecklistTab.tsx` über denselben `CriterionTagPicker` wie im Protokoll (Chip + Popover, inkl. Suche und Vorschlägen) — keine separate Komponente nötig.
- **`analyzeSuggestions()` bezieht jetzt auch Checklisten-Kommentare ein:** Antworten ohne effektives Kriterium (weder individuell noch konfiguriert) mit vorhandenem Kommentar werden mit `${Label}: ${Kommentar}` als zusätzliche Zeile in denselben gebündelten KI-Call eingespeist wie Protokoll-/Notizzeilen. Da Checklisten-Antworten nicht textbasiert (wie `lineTags`) adressierbar sind, führt ein neuer optionaler `checklistKey`-Marker (`${checklistItemId}:${instituteeId ?? ''}`) am `LineRef` das Routing der KI-Antwort in eine eigene Map `checklistSuggestions` statt in die text-basierte `lineSuggestions`-Map.
- **Lokale (KI-freie) Checklisten-Vorschläge** analog zu `localLineSuggestions`: neuer `useMemo` `localChecklistSuggestions` wendet `findSimilarTaggedLines()` auf den Kommentartext jeder unzugeordneten Antwort an (Schwelle ≥ 0,25) — sofort verfügbar, ohne „Vorschläge analysieren" auszulösen. Merge mit den KI-Vorschlägen (`mergedChecklistSuggestions`, KI hat Vorrang) liefert `getChecklistSuggestions()`, das `ChecklistTab.tsx` genau wie im Protokoll als 💡-Chip im `CriterionTagPicker` anzeigt.
- **Mirroring nach `Assessment`** nutzt jetzt konsequent das *effektive* Kriterium (individuell > konfiguriert) statt nur `ci.linkedCriterionItemId`. Wechselt die individuelle Zuordnung nachträglich auf ein anderes Kriterium, bleibt ein bereits gespiegelter `Assessment`-Eintrag beim alten Kriterium bewusst unangetastet (keine rückwirkende Korrektur/Löschung) — gleiches einfaches Prinzip wie beim Löschen eines Checklisten-Punkts.
**Dritter Nachtrag (2026-07-19): Direkte Kriterien-Vorschläge ohne KI.** Zwei Ergänzungen zur Live-Formulierungshilfe, beide ohne KI-Call: (1) Das „💭 Ähnliche frühere Notizen"-Panel (`ConversationTab.tsx`/`MeetingView.tsx`) zeigt jetzt zusätzlich das Kriterium der jeweiligen Alt-Zeile an (`„Text" → 🏷 Kriterium`), aufgelöst aus dem bereits vorhandenen `criterionItemId` in `SimilarTaggedLine` — reine Zusatzinfo, keine neue Berechnung nötig. (2) Neuer `useMemo` `localLineSuggestions` in `MeetingView.tsx`: berechnet für **alle** noch nicht zugeordneten Zeilen (Gesprächsbeiträge + allgemeine Notizen, gleiche Sammel-/Ausschlusslogik wie `analyzeSuggestions`) lokale Kriterium-Vorschläge über `findSimilarTaggedLines()`, gefiltert auf Ähnlichkeit ≥ 0,25 („mittel", gleiche Schwelle wie in `criterionSuggestionPrompt.ts`) — macht den 💡-Vorschlags-Chip im `CriterionTagPicker` **sofort verfügbar, ohne „🏷 Vorschläge analysieren" auszulösen**. Wird mit den KI-Vorschlägen (`lineSuggestions`) zu `mergedLineSuggestions` zusammengeführt — KI-Vorschläge haben Vorrang, wenn für dieselbe Zeile beides vorliegt (`lineSuggestions`-Einträge überschreiben `localLineSuggestions` beim Merge). Geprüft und bestätigt: „🏷 Vorschläge analysieren" überschreibt nie eine bereits bestehende Zuordnung — `analyzeSuggestions` schließt bereits getaggte Zeilen von vornherein von der KI-Anfrage aus (`taggedTexts.has(line)`-Filter), und `CriterionTagPicker` zeigt bei vorhandener Zuordnung ohnehin immer den Tag-Chip statt des Vorschlags-Chips (Vorschläge sind nur bei leerer Zuordnung überhaupt sichtbar).
**Zweiter Nachtrag (2026-07-19):** Zwei UI-Korrekturen nach echtem Nutzungsfeedback. (1) `ChecklistTab.tsx` nahm zu viel Bildschirmplatz ein (eine Karte mit großen Buttons + immer sichtbarem Kommentarfeld pro Institutee/Punkt) — umgebaut auf Zeilen-Darstellung: `<select>` statt fünf breiter Bewertungs-Buttons, Kommentar nur über ein 💬-Icon ausklappbar (Icon zeigt blau eingefärbt an, wenn bereits ein Kommentar hinterlegt ist). (2) Die Live-Formulierungshilfe (`💭 Ähnliche frühere Notizen`, `ConversationTab.tsx` + `MeetingView.tsx`) zeigte Vorschläge bisher nur rein lesend an — jetzt per Klick übernehmbar, ersetzt dabei die gerade getippte (letzte) Zeile der Notiz. Wichtige Falle dabei: In `MeetingView.tsx` hätte ein reiner Klick auf den Vorschlag zuerst den `onBlur`-Handler der Notizen-Textarea ausgelöst (`finishEditingGeneralNotes()`, beendet den Editier-Modus und entfernt damit Textarea+Vorschlagsliste aus dem DOM, bevor der Klick verarbeitet wird) — behoben über `onMouseDown={e => e.preventDefault()}` auf den Vorschlags-Buttons, verhindert den Fokuswechsel/Blur überhaupt erst (gleiches Muster wie `stopPropagation` beim `CriterionTagPicker` in den allgemeinen Notizen, nur mit `preventDefault` statt `stopPropagation`, da hier kein Klick-Bubbling sondern der native Blur-vor-Click-Ablauf das Problem ist).
**Konzept (2026-07-18): Live-Formulierungshilfe, Beobachter-Fokus, Meeting-Checkliste.** Zwei unabhängige Erweiterungen, in einer Design-Session gemeinsam geprüft:
- **Live-Formulierungshilfe (kein KI-Call):** Während des Tippens in `ConversationTab.tsx`/allgemeinen Notizen liefert ein debounced Aufruf der bereits bestehenden `findSimilarTaggedLines()` (`src/utils/criterionSuggestion.ts`) ein rein lesendes Referenz-Panel „Ähnliche frühere Notizen" — bewusst **kein** Autocomplete/Text-Einfügung (würde die 2026-07-02 getroffene „nicht live während des Tippens"-Entscheidung für die Zuordnung unterlaufen; hier geht es nur um unverbindliche Formulierungs-Inspiration, kein Eingriff in den Text). Kein neues Backend nötig.
- **Beobachter-Fokus (global, keine Bewerter-Identität nötig):** Die App hat kein Login-/Nutzerkonzept — „was wird typischerweise fokussiert" bezieht sich daher bewusst auf alle Bewerter zusammen. Leistet im Kern bereits `learnedPatternHints` (Wie wird formuliert) + `getCriterionUsageMap()` (Was wird häufig kommentiert, bereits für die Config→Feedback-Verwendungs-Badges gebaut). Nur eine neue, rein lesende Anzeige nötig, kein neues Datenmodell.
- **Meeting-Checkliste pro Phasentyp:** Neues Feld `PhaseTemplate.hasChecklist` (Opt-in wie `hasConversation`/`hasAssessment`) + neue Tabelle `ChecklistItemDef` (`phaseTemplateId`, `label`, `order`, optional `linkedCriterionItemId`, `scope: 'perInstitutee' | 'meetingWide'` — pro Eintrag konfigurierbar). Wichtiger Fund: `PhaseTemplate.criteriaIds` existiert zwar schon, wird aber aktuell nirgends live im Meeting ausgewertet (nur für `getCriterionUsageMap`-Zählung) — bewusst NICHT für die Checkliste umgewidmet, da es bereits eine andere, etablierte Sichtbarkeits-Scoping-Semantik hat. Antwort-Ablage maximiert Wiederverwendung bestehender Strukturen statt einer vierten Bewertungs-Struktur: „pro Institutee + verknüpft" → direkt in `Assessment` (bestehende Funktionen); „meeting-weit + verknüpft" → direkt in `MeetingInstance.generalNoteTags` (bestehender, institutee-unabhängiger Mechanismus); nur „frei definiert" (verknüpft oder nicht, jeweils pro Institutee oder meeting-weit) braucht die einzige wirklich neue Tabelle `ChecklistResponse` (`meetingInstanceId`, `instituteeId?`, `checklistItemId`, `rating`, `note`) — bewusst **ohne** Aggregation in Evaluation/gewichteten Durchschnitt (reine lokale Meeting-Notiz, vermeidet eine dritte manuell synchron zu haltende Bewertungsstruktur neben Assessment/FeedbackCategoryRating). UI: neuer optionaler Tab „Checkliste" in `MeetingView.tsx`, Konfiguration als neuer Sub-Tab in `AssignmentTypeConfig.tsx`, Kriterium-Verknüpfung über das bestehende `criterionOptionGroups()`-Dropdown-Muster aus `FeedbackStructureConfig.tsx`. **Präzisiert bei der Umsetzungsplanung (gleicher Tag):** „meeting-weit + verknüpft" hat keine saubere Heimat (`Assessment` verlangt zwingend `instituteeId` — Kriterien sind in diesem Modell immer Personen-Kompetenzen, nie Meeting-Kollektiv-Eigenschaften; `generalNoteTags` kennt keine Bewertungsstufe, nur Text+Tag). Deshalb: Kriterium-Verknüpfung ist nur bei `scope: perInstitutee` möglich, `scope: meetingWide`-Punkte sind immer „frei" (kein Kriterium-Dropdown im UI). Vereinfacht die Ablage auf 3 statt 4 Fälle, siehe Umsetzungsplan.
**Konzept (2026-07-16, noch nicht umgesetzt): Selbstlernendes Kriterien-Tagging.** Löst den Offene-Punkte-Eintrag „Selbstlernendes Tagging" oben ab (bisher nur Idee, jetzt fertig gescoped in einer eigenen Design-Session, siehe Vorgabe oben). Ziel: eine priorisierte, KI-gestützte Vorschlagsliste für die Kriterium-Zuordnung noch nicht getaggter Protokollzeilen, direkt im `CriterionTagPicker` aufrufbar. Kernentscheidungen:
- **Kein neues Datenmodell für den Lernkorpus.** Die Lernbasis ist der bereits bestehende `lineTags`/`generalNoteTags`-Bestand, global über alle Assignments hinweg (siehe `getCriterionUsageMap()`/`getCriterionUsageDetails()` in `server/db/queries/feedbackStructure.ts`). Jede neu vergebene Zuordnung vergrößert den Korpus automatisch — kein separater Trainingsschritt, keine Migration nötig.
- **Zweistufiger Ablauf.** (1) Rein lokale Ähnlichkeitssuche ohne KI-Call (Wortüberschneidung/Jaccard über normalisierten Zeilentext; `parseNotationPrefix()`/`splitNoteLines()` zur Normalisierung wiederverwendet) liefert pro noch nicht zugeordneter Zeile ~58 ähnlichste Alt-Beispiele als Kontext-Hinweis. (2) EIN gebündelter KI-Call pro Protokoll-Ansicht (nicht pro Zeile) analysiert alle noch nicht zugeordneten Zeilen des gesamten offenen Protokolls auf einmal und liefert pro Zeile eine priorisierte Kandidatenliste (Kriterium-IDs) statt nur eines Einzelvorschlags. Fallback auf Einzel-Zeilen-Calls ist vorgesehen, aber erst nach einem ersten Testlauf zu entscheiden, falls die Bündelung qualitativ nicht ausreicht — nicht vorab bauen.
- **Expliziter Trigger-Button** („🏷 Vorschläge analysieren"), kein automatischer Call beim Öffnen des Meetings — gleiches Muster wie die bestehende KI-Feedback-Generierung (keine versteckten API-Kosten/Latenz im Hintergrund).
- **Neuer Platzhalter `{{KRITERIEN_STRUKTUR}}`** für den Tagging-Prompt: vollständige, ID-referenzierte Struktur Dimension → Kategorie (Name+Beschreibung) → Kriterium (Name+Beschreibung), gefiltert über die bestehende `filterVisibleFeedbackStructure()` anhand `AssignmentType.criteriaIds` — dieselbe Filterlogik wie überall sonst im Projekt, nicht neu implementiert. Kriterien werden über ihre ID referenziert statt über den Namen (robust gegen Namenskollisionen beim Parsen der KI-Antwort). `FeedbackDimension` hat aktuell kein `description`-Feld (anders als Kategorie/Kriterium) — „alle Erklärungsebenen" bezieht sich daher auf Dimension-Name + Kategorie-Name/Beschreibung + Kriterium-Name/Beschreibung. Weitere neue Platzhalter: `{{ASSIGNMENT_TYP}}`, `{{PROTOKOLL_ZEILEN}}` (nummerierte, noch nicht zugeordnete Zeilen des gesamten Protokolls), `{{AEHNLICHE_ALT_ZUORDNUNGEN}}` (Ergebnis aus Schritt 1).
- **EIN gemeinsames Prompt-Template statt eines pro Assignment-Typ** — die Typ-Abhängigkeit steckt im Inhalt von `{{KRITERIEN_STRUKTUR}}`, nicht in der Anzahl der Templates (gleiches Prinzip wie `{{KATEGORIEN_STRUKTUR}}` heute schon). Persistiert als neues Feld `AppSettings.criterionTaggingPromptTemplate` (automatische Spalten-Migration wie gehabt), editierbar in einem neuen, eigenen Abschnitt in `AiPromptConfig.tsx` („KI-Prompt: Kriterien-Zuordnungsvorschläge") — gleiches Aufklapp-Textarea-Muster + eigene Platzhalter-Legende wie der bestehende Standard-Prompt-Bereich. Kein Eintrag pro Dimension nötig, da es nur eine Protokoll-weite Analyse gibt, nicht pro Dimension.
- **Antwortformat:** Zeilennummer + priorisierte Kriterium-ID-Liste als Klartext-Marker (`ZEILE 3: 12, 47, 5`, `-` bei keinem Vorschlag) statt JSON — bewusst gleiches Prinzip wie `parseDimensionAiResponse` (JSON/Codefence-Parsing-Bugs waren 2026-07-07 der Grund für die Umstellung auf Marker-Text dort). Parser tolerant gegenüber fehlenden/kaputten Zeilen.
- **Datenschutz-Hinweis:** Gesprächsnotizen gehen für Schritt 2 an dieselbe bereits genutzte externe KI-API wie die bestehende Feedback-Generierung (gleiches Muster, kein neues Risiko-Muster) — vor Produktivnutzung trotzdem kurz mit dem Datenschutzbeauftragten abstimmen, da es eine neue Nutzungsart derselben Datenklasse ist.
**Nachtrag (2026-07-17):** Zusätzliche Lernebene beschlossen — bewusster Mittelweg zwischen reiner Wortüberschneidung und einem separaten trainierten Klassifikator (Letzteres verworfen, da ein eigener Trainings-/Aktualisierungsschritt der Prämisse „kein separater Trainingsschritt" widersprochen hätte). Der ohnehin gebündelte KI-Aufruf liefert pro Analyse-Lauf zusätzlich einen `MUSTER:`-Block mit kurzen, generalisierten Formulierungsmustern **pro Kriterium** (nicht pro Zeile), z.B. „Kunde wird häufig nach offenen Fragen gefragt, bevor...". Diese landen in einem neuen, optionalen Feld `FeedbackCriterionItem.learnedPatternHints` (automatische Spalten-Migration wie gehabt, kein neues Datenmodell/keine neue Tabelle) und werden bei jedem weiteren Lauf überschrieben/verfeinert — reines Nebenprodukt des normalen „🏷 Vorschläge analysieren"-Klicks, kein separater Trainingslauf. Neuer Platzhalter `{{GELERNTE_MUSTER}}` speist diese Muster (nur für die im aktuellen Assignment-Typ sichtbaren Kriterien) zusätzlich zu `{{AEHNLICHE_ALT_ZUORDNUNGEN}}` in künftige Prompts ein — verbessert die Vorschlagsqualität über die Zeit, ersetzt den KI-Call aber bewusst nicht.
**Erledigt (2026-07-17): Selbstlernendes Kriterien-Tagging umgesetzt** (siehe Konzept + Nachtrag oben). Neuer Button „🏷 Vorschläge analysieren" in `MeetingView.tsx` löst EINEN gebündelten KI-Call für das gesamte offene Protokoll aus (Gesprächsbeiträge + allgemeine Notizen, Kundenzitate ausgenommen). Neue Bausteine: `server/db/queries/criterionSuggestions.ts` (`getGlobalLineTagCorpus()`, flacht `lineTags`/`generalNoteTags` global ab, gleiches Iterationsmuster wie `getCriterionUsageMap()`), `src/utils/criterionSuggestion.ts` (rein lokale Jaccard-Ähnlichkeitssuche, kein KI-Call), `src/utils/criterionSuggestionPrompt.ts` (`buildCriterionStructureText()`/`buildCriterionSuggestionPromptContext()`/`parseCriterionSuggestionResponse()` — Response-Format bewusst Klartext-Marker `ZEILE n: <IDs>` + `MUSTER:`-Block statt JSON, gleiches Robustheitsprinzip wie `parseDimensionAiResponse`). `AppSettings.criterionTaggingPromptTemplate` (ein gemeinsames Template statt eines pro Assignment-Typ, editierbar in `AiPromptConfig.tsx` unter „KI-Prompt: Kriterien-Zuordnungsvorschläge") und `FeedbackCriterionItem.learnedPatternHints` sind neue, automatisch migrierte Spalten. `CriterionTagPicker.tsx` zeigt bei fehlender Zuordnung einen 💡-Vorschlags-Chip (Top-Vorschlag per Klick übernehmbar, Alternativen + volle Suche im Popover) statt des leeren „+"-Buttons. Nebenbei extrahiert: `src/utils/aiClient.ts` (`callOpenRouter()`/`getAiSettings()`/`saveAiSettings()`) aus `AssignmentFeedbackPage.tsx`, damit die bestehende KI-Feedback-Generierung und das neue Tagging dieselbe OpenRouter-Anbindung nutzen statt zwei leicht abweichende Implementierungen zu pflegen — reiner Refactor, kein Verhaltensunterschied. Verifiziert: `npx tsc -b` fehlerfrei; neue RPC-Endpunkte (`criterionSuggestions.getGlobalLineTagCorpus`, `appSettings.getOrSeedCriterionTaggingPromptTemplate`) direkt gegen die laufende Dev-Instanz getestet (der Server läuft unter `tsx watch` und hat Schema-Migration + neue Module automatisch übernommen, ohne manuellen Neustart). Manueller Browser-Test des Gesamtflusses (Klick auf „Vorschläge analysieren" in einem echten Meeting, Übernehmen eines Vorschlags) steht noch aus — sollte vor breiter Nutzung nachgeholt werden.
**Erledigt (2026-07-13, dritte Runde):** Kriterium-Zuordnung für „Allgemeine Notizen" (`MeetingInstance.generalNoteTags`, neue Spalte, gleicher Typ `ConversationLineTag[]` wie `ConversationEntry.lineTags`) — anders als die bestehende Zeilen-Zuordnung in Gesprächsbeiträgen ist diese **nicht** an ein Institutee gebunden, sondern gilt für **alle** Institutees des Assignments gleichzeitig, weil allgemeine Meeting-Notizen keinem einzelnen Sprecher zugeordnet sind. Erfassung in `MeetingView.tsx`: derselbe `CriterionTagPicker` wie bei Gesprächsbeiträgen, per `renderLineAddon` an `NotationText` gehängt (Klick auf den Picker mit `stopPropagation`, da der umgebende Notizen-Block sonst in den Editier-Modus wechseln würde); verwaiste Zuordnungen werden beim Verlassen des Editier-Modus anhand der aktuellen Zeilen bereinigt (gleiches Muster wie `saveEdit` bei Gesprächsbeiträgen). Fünf bestehende Konsumenten von `lineTags` wurden konsistent um die neue, ungefilterte Quelle ergänzt (Recherche in zwei unabhängigen Durchläufen bestätigt, dass dies eine vollständige Liste ist): `AssessmentTab.suggestedRating()` (💡-Vorschlags-Badges, jetzt pro Kriterium für jedes Institutee identisch beeinflusst), `aiPrompt.ts::buildDimensionPromptContext` (`{{KRITERIEN_SCORES}}` bekommt die zugeordneten Zeilen zusätzlich, `{{NOTIZEN_ALLGEMEIN_MEETING}}` blendet bereits zugeordnete Zeilen aus — exakt das bestehende Muster von `{{PROTOKOLL_UNZUGEORDNET}}`), `meetingExport.ts` (Kriterien-ID-Sammlung + `_(→ Kriteriumsname)_`-Annotation pro Zeile im Markdown-Export), `FeedbackDraftPage.tsx` (Legacy-Feedback-Entwurf, gleiche Erweiterung von Kriterien-Sammlung und Notizen), `MeetingHistory.tsx` (reine Anzeige, sonst wäre die neue Zuordnung dort unsichtbar geblieben). Bei leerem/fehlendem `generalNoteTags` ist das Verhalten überall identisch zu vorher — bestehende Daten/Prompts brechen nicht.
**Erledigt (2026-07-13, zweite Runde):** Chronologisches Gesamtprotokoll aller Meetings eines Assignments (`MeetingHistory.tsx`, Route `/assignment/:id/history`) — Anlass: Recap-Calls nach jedem Kundenmeeting brauchen einen schnellen Blick auf die Historie vorheriger Meetings, zusätzlich hilfreich bei der Gesamtdurchsprache im finalen Feedback-Call. Rein lesende Seite, baut bewusst auf bereits vorhandenen Bulk-Abfragen auf statt neuen Server-Code zu schreiben: `listDoneMeetingsChronological()` (kanonischer Trash-Filter + Sortierung, siehe unten) liefert die Meetings, `listConversationEntriesForMeetings()`/`listAssessmentsForMeetings()` (beide bereits vorhanden, u.a. schon von `AssignmentDetail.tsx` genutzt) liefern Notizen/Bewertungen über alle Meetings auf einen Schlag statt N+1-Abfragen pro Meeting. Rendering nutzt bestehende Bausteine statt Neuimplementierung: `NotationText` (`notationParser.tsx`) für Notiz-Formatierung, dieselbe `weightedAverageRating()`+`RATING_BG`-Chip-Logik wie die Meeting-Kacheln in `AssignmentDetail.tsx`. Kriterium-Tags werden hier bewusst nur als statisches Badge angezeigt (kein interaktiver `CriterionTagPicker`), da die Seite reine Anzeige ist. Das jeweils neueste Meeting wird optisch hervorgehoben und die Seite scrollt beim Laden automatisch dorthin — deckt den Recap-Anwendungsfall ab, während beim Hochscrollen die volle Historie für die Feedback-Gesamtdurchsprache verfügbar bleibt. Erreichbar über neue Buttons sowohl auf `AssignmentDetail.tsx` („📜 Gesamtprotokoll") als auch direkt aus `MeetingView.tsx` heraus („📜 Verlauf"), damit man auch mitten im Recap-Call ohne Umweg über die Detailseite wechseln kann.
**Erledigt (2026-07-14):** KI-Feedback-Generierung (siehe 2026-07-07) anhand echter Testläufe nachgeschärft. `RATING_OPTIONS` (`src/config/constants.ts`) hat ein neues `full`-Feld ("Not/Partially/Nearly/Fully Client Ready", "Not Applicable") als einzige Quelle der Wahrheit für ausgeschriebene Bewertungslabels — wichtig fürs KI-Prompting, da die kurzen Labels ("Not") für ein Modell ohne Kontext zum Zweck des Trainee-Programms (Ziel: "Client Ready" werden) nicht aussagekräftig genug sind. `src/utils/notationParser.tsx` hat eine neue Funktion `parseNotationPrefix()` (liefert Rating/Zitat-Flag/Resttext, `detectNotationRating()` ist jetzt darauf aufgebaut, keine doppelte Symbol-Zuordnung mehr). In `aiPrompt.ts` löst `formatNotationLine()` jede Notiz-Zeile (`{{KRITERIEN_SCORES}}`-Notizen, `{{NOTIZEN_ALLGEMEIN_MEETING}}`, `{{PROTOKOLL_UNZUGEORDNET}}`) konsistent auf — Format `Label — Text` (bewusst **keine** eckigen Klammern, da `[Titel]` bereits für Slide-/Themen-Referenzen reserviert ist und sonst mit aufgelösten Bewertungen kollidieren würde). Zwei fachliche Korrekturen dabei: (1) `{{KRITERIEN_SCORES}}` lässt Kriterien ohne jede Bewertung/Notiz jetzt komplett aus (reines Token-Rauschen), ebenso ganze Kategorien ohne verwertbare Kriterien. (2) Mit `>` markierte Kundenzitate werden aus `{{NOTIZEN_ALLGEMEIN_MEETING}}`/`{{PROTOKOLL_UNZUGEORDNET}}` jetzt **komplett herausgefiltert** statt nur aufgelöst — Kundenaussagen sind fachlicher Assignment-Kontext, keine Aussage über den Berater, und würden besonders beim Feedback-Call mit dem Kunden ein verzerrtes Bild erzeugen (der Kunde gibt ohnehin separates Feedback). Explizit einem Kriterium zugeordnete Zitate (bewusste Tagging-Entscheidung des Bewerters) werden davon nicht angefasst. Output-Format umgestellt von Fließtext-Absätzen auf Stichpunkte (`- `-Zeilen) — leichter in die offizielle Feedback-App zu übertragen; Development-Needs-Stichpunkte müssen jetzt explizit als Handlungsempfehlung formuliert sein (z.B. "Sollte darauf achten, dass ..."). Zusätzlich eine explizite Anti-Halluzinations-Anweisung im Prompt ("nutze ausschließlich die angegebenen Daten, erfinde nichts") ergänzt, nachdem die KI vereinzelt Details erfunden hatte. **Wichtige Einschränkung, die bei jeder künftigen Prompt-Engine-Änderung zu beachten ist:** Da jede Dimension einen **vollständig unabhängigen** Prompt-Text in der DB hat (bewusste Design-Entscheidung, siehe 2026-07-07), wirken Verbesserungen an `AI_CONFIG.defaultDimensionPromptTemplate` NICHT automatisch auf bereits existierende Dimensionen — nur auf neu angelegte oder explizit per "Auf Standard zurücksetzen" aktualisierte. Alle Änderungen dieser Runde wurden nach Rückfrage per Skript einmalig auf alle 5 bestehenden Dimensionen nachgezogen (nur unbedenklich, weil vorher per Read-Check bestätigt wurde, dass noch keine individuellen Anpassungen existierten).
**Erledigt (2026-07-14, dritte Runde):** PDF-Export ergänzt (`src/utils/pdfExport.tsx`, neue Abhängigkeit `@react-pdf/renderer` — reines JS, kein natives Kompilieren nötig, läuft browser- und Node-seitig). Zwei Dokument-Typen: `AssignmentReportDocument` (Gesamtprotokoll wie `MeetingHistory.tsx` + Feedback-Anhang aller Institutees, unabhängig vom Status Entwurf/Final) und `InstituteeFeedbackDocument` (nur das Feedback einer Person). Wichtig: Der Feedback-Teil zeigt ausschließlich die **finalen** Felder (`achievements`/`developmentNeeds`/`text`/`rating`) — niemals die `suggested*`-KI-Vorschläge, die noch nicht per „Übernehmen" bestätigt wurden. Notation in den Protokollzeilen wird wie in der App selbst als Icon/Farbe dargestellt (✓/✗/✓~/✗~/, aus `parseNotationPrefix()` abgeleitet) — bewusst anders als die textuelle "Label — Text"-Auflösung in `aiPrompt.ts`, da das PDF für Menschen ist, die die App-Notation schon kennen, nicht für ein KI-Modell ohne Kontext. Erzeugung/Download sind getrennt (`buildAssignmentReportPdfBlob`/`buildInstituteeFeedbackPdfBlob` liefern nur den Blob, `exportAssignmentReportPdf`/`exportInstituteeFeedbackPdf` triggern zusätzlich den Browser-Download) — dadurch auch außerhalb des Browsers (Node/tsx) testbar, was beim Verifizieren genutzt wurde (die Download-Hälfte braucht `document`/`URL`, ist also browser-only). Buttons an drei Stellen: `MeetingHistory.tsx` (Gesamt-Report), `AssignmentFeedbackPage.tsx` (eigenes Feedback), `AssignmentDetail.tsx` (Schnellzugriff für beide Varianten ohne Navigation). Nachträglich (nach erstem Testlauf) Seitenumbrüche korrigiert: `wrap={false}` auf ganzen Meeting-/Dimension-Blöcken erzwang unsinnige Umbrüche/Lücken, sobald ein Block länger als eine Seite war — stattdessen nur noch kleine Kopfzeilen-Blöcke (Meeting-Titel+Datum, Dimension-/Institutee-Name, Achievements/Development-Needs-Label) mit `wrap={false}` + `minPresenceAhead` vor Verwaisung geschützt, der eigentliche Inhalt darf frei umbrechen. Zusätzlich erzwungener Seitenwechsel (`break`) vor dem „Feedback"-Abschnitt und vor jedem weiteren Institutee, damit Protokoll und Feedback klar getrennt bleiben. Ebenfalls ergänzt: Kriterium-Zuordnung pro Protokollzeile (`lineTags`/`generalNoteTags`) wird jetzt wie in `MeetingHistory.tsx`/`meetingExport.ts` als kleiner „→ Kriteriumsname"-Zusatz hinter der Zeile angezeigt (indigo, per `visibleItems`-Lookup) — vorher zeigte das PDF nur die Notation, aber keine Kriterium-Zuordnung.
**Erledigt (2026-07-16, zweite Runde):** Sicherer Umbau der Feedback-Struktur (Config → Feedback), motiviert durch laufende Umbenennungen/Löschungen/Verschiebungen von Kriterien ohne Überblick, wo diese bereits verwendet werden. Neu in `server/db/queries/feedbackStructure.ts`: `getCriterionUsageMap()` — EIN Durchlauf über `assessments`, `conversationEntries.lineTags`, `meetingInstances.generalNoteTags`, `assignmentTypes.criteriaIds`, `phaseTemplates.criteriaIds` liefert eine `criterionId → CriterionUsage`-Map (Zählungen + betroffene Meetings), wird beim Laden von `FeedbackStructureConfig.tsx` mitgeladen und zeigt pro Kriterium einen aufklappbaren „N Verwendungen"-Badge. **Verschieben** zwischen Kategorien per einfachem Dropdown (kein Drag & Drop, keine neue Abhängigkeit — nutzt das bereits vorhandene `updateFeedbackCriterionItem(id, { categoryId })`). **Löschen mit Verwendung** zeigt jetzt ein Inline-Panel mit zwei Wegen: „Übertragen auf ▾" (`reassignCriterionReferences(fromId, toId)`, verschiebt Assessments/Tags/Sichtbarkeitslisten auf ein anderes Kriterium, dann Löschen) oder „Trotzdem löschen" (Bewertungen werden gelöscht, Protokoll-Zuordnungen nur entfernt, Notiztext bleibt). **Wichtiger Edge-Case bei `reassignCriterionReferences`:** `assessments` hat keine DB-Unique-Constraint auf `(meetingInstanceId, instituteeId, criteriaId)` — ein blindes Umschreiben von `criteriaId` könnte eine Dubletten-Zeile für dieselbe Meeting/Institutee-Kombination erzeugen. Bei Konflikt wird die Quell-Zeile stattdessen in die bestehende Ziel-Zeile gemergt (Notizen zusammengeführt, vorhandener Score der Zielzeile hat Vorrang) und gelöscht; Rückgabewert `mergedConflicts` macht das der UI gegenüber transparent. `deleteFeedbackCriterionItem()` räumt jetzt selbst alle Referenzen auf (Verteidigung in der Tiefe, unabhängig vom UI-Pfad), `deleteFeedbackCategory()`/`deleteFeedbackDimension()` cascaden ab jetzt **durch** diese Funktion statt per direktem Bulk-Delete, damit die Aufräum-Logik auch bei Kategorie-/Dimension-Löschung greift. Verifiziert per Skript auf rein synthetischen Testdaten (temporäre Dimension/Kategorie/2 Kriterien + ein Test-Meeting unter einem echten Assignment, absichtlich erzeugter Konfliktfall bestätigt korrektes Merge-Verhalten ohne Dublette, danach vollständig wieder entfernt).
**Nachtrag (gleicher Tag):** Reine Zählungen/Meeting-Liste reichten nicht — beim Konsolidieren zweier Kriterien musste man sonst weiterhin jedes Meeting einzeln durchsuchen. Neu: `getCriterionUsageDetails(criterionId)` liefert pro Kriterium eine **Einzelinstanzen-Liste** (`CriterionUsageInstance`, discriminated union `assessment`/`lineTag`/`generalNoteTag`, mit Meeting/Assignment/Person/Text aufgelöst) — bewusst lazy pro Kriterium beim Aufklappen geladen, nicht eager für alle (im Unterschied zu `getCriterionUsageMap`), da hier echte Texte/Namen aufgelöst werden. Direkt im aufgeklappten Verwendungs-Badge kann jede einzelne Instanz jetzt per „→ anderes Kriterium…"-Dropdown + „Übertragen" gezielt umgehängt oder per „Entfernen" einzeln entfernt werden (`reassignUsageInstance()`/`removeUsageInstance()`) — ohne das ganze Kriterium zu löschen und ohne alle Verwendungen auf einmal zu bewegen. Die Konflikt-Merge-Logik für Assessments wurde dafür aus `reassignCriterionReferences` in eine gemeinsame Hilfsfunktion `mergeOrReassignAssessment()` extrahiert, damit Bulk- und Einzel-Reassign nicht zwei leicht unterschiedliche Implementierungen derselben Regel pflegen. Ebenfalls per Skript auf synthetischen Testdaten verifiziert (Einzel-Entfernen erhält den Notiztext, Einzel-Übertragen löst denselben Merge-Konflikt korrekt wie die Bulk-Variante).
**Zweiter Nachtrag (gleicher Tag):** Nach erstem Testlauf gemeldet: Ein Kriterium zeigte nach Bereinigung aller Bewertungen/Protokoll-Tags weiterhin „4 Verwendungen" — Ursache war, dass `assignmentTypeNames`/`phaseTemplateLabels` (Sichtbarkeits-Zuordnung in `AssignmentType.criteriaIds`/`PhaseTemplate.criteriaIds`) zwar in die Zählung eingehen, aber bis dahin nur als reiner Text angezeigt wurden — keine Einzelinstanz, kein Weg, sie von hier aus zu bereinigen. `CriterionUsageInstance` um zwei weitere Kinds ergänzt (`assignmentTypeVisibility`, `phaseTemplateVisibility`, mit `criterionId`-Feld, damit `reassignUsageInstance`/`removeUsageInstance` wissen, welche ID sie in der jeweiligen `criteriaIds`-Liste ersetzen/entfernen sollen). Damit sind jetzt **alle** in der Zählung enthaltenen Verwendungsarten auch einzeln direkt aus dem aufgeklappten Badge heraus behebbar, nicht nur die drei Protokoll-/Bewertungsarten. Client-Anzeige unterscheidet per `isDataInstance()`-Typprädikat zwischen den beiden Darstellungsformen (Meeting/Person-Kontext vs. reiner Name). Ebenfalls per Skript auf synthetischen Testdaten (temporärer Assignment-Typ + Phase-Vorlage, danach entfernt) verifiziert.
**Dritter Nachtrag (gleicher Tag):** Zwei UI-Bugs nach erstem echten Nutzungstest gefixt (`FeedbackStructureConfig.tsx`). (1) Alle „Ziel-Kriterium"-Dropdowns (Verschieben/Übertragen) gruppierten nur nach Dimension und übersprangen die Kategorie-Ebene — HTML kennt kein verschachteltes `<optgroup>`, daher jetzt eine kombinierte Optgroup-Beschriftung `"Dimension → Kategorie"` über die neue gemeinsame Hilfsfunktion `criterionOptionGroups()` (an allen drei Stellen genutzt: Verschieben-Dropdown, Lösch-Dialog-Übertragen, Einzel-Verwendungs-Übertragen). (2) Schwerwiegenderer Bug: Die Ziel-Auswahl pro Einzelverwendung war über den **Array-Index** in der Instanzen-Liste verknüpft (`` `${itemId}:${idx}` ``). Nach jedem „Übertragen" wird die Liste neu vom Server geladen, wodurch sich die Indizes der verbleibenden Instanzen verschieben — eine bei Index 3 gespeicherte Ziel-Auswahl gehörte danach plötzlich zu einer anderen Instanz, und ein Klick auf „Übertragen" verschob nachweislich die falsche Zeile. Gefixt durch einen **stabilen, inhaltsbasierten Schlüssel** (`instanceKey()`: `assessmentId` bzw. `conversationEntryId`/`meetingInstanceId` + Zeilentext bzw. `assignmentTypeId`/`phaseTemplateId`) statt des Index — Auswahlen bleiben dadurch auch nach einem Reload an der richtigen Instanz hängen, und bereits übertragene/entfernte Instanzen verschwinden korrekt aus der Liste statt eine falsche Zeile zu verändern.
**Erledigt (2026-07-16):** Zwei weitere Exporte ergänzt, jeweils als Markdown UND PDF. (1) „Feedback-Struktur" (Config → Feedback, neue Buttons „⬇ MD"/„⬇ PDF" im Header): alle Dimensionen/Kategorien/Kriterien inkl. Gewichtung und aller Beschreibungen (`FeedbackCategory.description`/`FeedbackCriterionItem.description`, siehe 2026-07-07/2026-07-15). (2) „Assignment-Typen-Bericht" (Config → Ass.-Typen, gleiche Buttons): pro Assignment-Typ die Phasen sowie das Kriterien-Mapping (`AssignmentType.criteriaIds`) — bei leerer/undefined `criteriaIds` „Alle Kriterien", sonst nach Dimension/Kategorie gruppierte Liste der zugeordneten Kriteriennamen (exakt dieselbe Lookup-Logik wie im „Kriterien"-Tab von `AssignmentTypeConfig.tsx`, nicht neu erfunden). Markdown-Erzeugung in neuer Datei `src/utils/structureExport.ts` (nutzt bestehendes `downloadFile()` aus `meetingExport.ts`), PDF-Dokumente als zwei weitere Komponenten in `pdfExport.tsx` (gleiche Styles/Pagination-Helfer wie die bestehenden Berichte). Bei der Verifikation aufgefallen, aber bewusst nicht angefasst: Phasen-Labels enthalten in den Bestandsdaten teils schon "(Lead)"/"(Kunde)" als Teil des Freitexts, wodurch der Bericht das zusätzlich per `PhaseTemplate.withWhom` angehängte "(Lead)"/"(Kunde)" doppelt zeigt — das ist eine Dateneingabe-Redundanz, keine Automatisierung, mit der ein Freitextfeld sicher bereinigt werden könnte, daher nicht angetastet.
**Erledigt (2026-07-15):** Analog zu `FeedbackCriterionItem.description` (2026-07-07) jetzt auch `FeedbackCategory.description` — Erläuterung, welche Themen/Beobachtungen in eine Kategorie einzahlen sollen, editierbar in `FeedbackStructureConfig.tsx` direkt unter dem Kategorienamen. Grund: Die KI kannte bisher nur die Kriterien-Ebene im Detail, nicht aber, wonach auf Kategorie-/Dimensionsebene grundsätzlich gefragt ist. Bewusst **keinen neuen Platzhalter** eingeführt, sondern die bestehende `{{KATEGORIEN_STRUKTUR}}` in `aiPrompt.ts` angereichert (`Name — Erläuterung` statt nur `Name`) — dadurch profitieren auch bereits bestehende, individuell angepasste Dimension-Prompts automatisch, ohne dass sie erneut synchronisiert werden müssen (der Platzhalter-Token selbst ändert sich nicht, nur was dahinter berechnet wird).
**Erledigt (2026-07-14, zweite Runde):** Zwei weitere Nachschärfungen der KI-Feedback-Generierung (siehe Eintrag oben). (1) Abschnittsmarker wie `[Background]`/`[Whiteboard - Storyboard]` (reine Navigationshilfe des Bewerters, an welcher Stelle der Präsentation/des Dialogs eine Beobachtung entstand) werden jetzt aus `{{NOTIZEN_ALLGEMEIN_MEETING}}`/`{{PROTOKOLL_UNZUGEORDNET}}` gefiltert, wenn ihnen keine Inhalts-Zeile folgt (`dropDanglingMarkers()` in `aiPrompt.ts`, Lookahead auf die jeweils nächste Zeile) — bleiben aber erhalten, wenn direkt danach noch etwas steht, da sie dann als Kontext-Header dienen. (2) `AI_CONFIG.defaultDimensionPromptTemplate` (der Basis-Prompt für neue Dimensionen und „Auf Standard zurücksetzen") ist nicht mehr rein im Code gepflegt, sondern zusätzlich in `appSettings` (Singleton-Tabelle, neue Spalte `defaultDimensionPromptTemplate`) persistiert und über einen neuen, aufklappbaren Bereich ganz oben im „KI-Prompts"-Tab (`AiPromptConfig.tsx`) direkt in der App editierbar — analog zum bestehenden Zeitgewichtungs-Faktor-Muster (`getOrSeedDefaultDimensionPromptTemplate()`/`saveDefaultDimensionPromptTemplate()` in `server/db/queries/appSettings.ts`, exakt wie `getOrSeedRatingTrendWeightStep()`). Der Code-Konstante dient jetzt nur noch als einmaliger Erstbefüllungswert für neue Installationen; `feedbackStructure.ts` (Auto-Anlage neuer Dimensionen, Backfill, Lazy-Create) liest den Default jetzt ausschließlich aus `appSettings`. Wie bei jeder Custom-Prompt-Änderung gilt weiterhin: bereits bestehende, individuell abweichende Dimension-Prompts ändern sich dadurch nicht automatisch mit.
**Erledigt (2026-07-13):** Papierkorb + Archiv für Assignments umgesetzt. Dashboard zeigt standardmäßig nur noch aktive Assignments (`statusFilter`-Default auf `'active'`), trashed/archivierte Assignments sind in keinem Status-Tab mehr sichtbar. `Assignment` hat zwei neue optionale Felder `deletedAt`/`archivedAt` (analog zu `MeetingInstance.deletedAt`). Löschen (`softDeleteAssignment`) ist nur möglich, wenn `status === 'done'` UND für **alle** `instituteeIds` ein Feedback mit Status `'final'` vorliegt (`isAssignmentFeedbackComplete()` in `server/db/queries/assignments.ts`) — Gate wird sowohl serverseitig erzwungen (wirft sonst) als auch clientseitig auf `AssignmentDetail.tsx` zum Deaktivieren des Buttons genutzt. Löschung ist zweistufig: `softDeleteAssignment` setzt nur `deletedAt` (Papierkorb, neue Seite `/assignments/trash`), `restoreAssignment` löscht es wieder (zurück nach „Abgeschlossen"). Nach 30 Tagen wird der Eintrag automatisch endgültig entfernt — kein Cron/Timer, sondern ein Lazy-Sweep (`purgeExpiredAssignmentTrash()`), der am Anfang von `listAssignmentsWithDetails()` mitläuft (Server läuft nicht dauerhaft, ein echter Scheduler wäre unzuverlässig). `hardDeleteAssignment()` kaskadiert manuell durch den kompletten Assignment-Baum (`meetingInstances` inkl. deren Kindern über das bestehende `hardDeleteMeetingInstance()`, `feedbackDrafts`, `assignmentFeedbacks` inkl. `feedbackCategoryRatings`/`feedbackDimensionTexts`/`feedbackCategoryTexts`) — Muster 1:1 von der bestehenden Meeting-Papierkorb-Kaskade in `server/db/queries/meetings.ts` übernommen. Zusätzlich unabhängiges Archiv (`archiveAssignment`/`unarchiveAssignment`, neue Seite `/assignments/archive`) für dauerhafte Aufbewahrung (spätere Auswertung/ML) — bewusst ohne Feedback-Zwang und ohne Lösch-Option in der UI, aber umkehrbar (Rückholen möglich). Neue Spalten `deletedAt`/`archivedAt` auf `assignments` werden automatisch per bestehendem `ALTER TABLE`-Migrationsmechanismus in `database.ts` nachgezogen, kein manuelles Migrationsskript nötig.
**Erledigt (2026-07-07):** KI-Feedback-Generierung neu konzipiert und umgesetzt — löst den zuvor diagnostizierten `response_format`/Markdown-Codefence-Bug strukturell statt punktuell. Statt einem Monolith-Call für alle Dimensionen macht `generateAllDimensions()` (`AssignmentFeedbackPage.tsx`) jetzt **sequenziell einen Call pro Dimension**, mit Fortschrittsanzeige und Teil-Fehlerbehandlung (fehlgeschlagene Dimensionen einzeln erneut versuchbar, `dimensionErrors`). Jede Dimension hat einen **vollständig frei editierbaren Prompt** (`FeedbackDimensionPrompt`, neuer Admin-Tab „KI-Prompts" → `AiPromptConfig.tsx`), der bei `addFeedbackDimension()` automatisch mit einem Default-Template vorbelegt wird (`AI_CONFIG.defaultDimensionPromptTemplate`) — neue Dimensionen funktionieren dadurch ohne Zutun. Antwortformat ist jetzt einfacher Text statt JSON (`KATEGORIE:`/`ACHIEVEMENTS:`/`DEVELOPMENT_NEEDS:`-Marker, geparst in `src/utils/aiPrompt.ts::parseDimensionAiResponse`), da pro Call ohnehin nur eine Dimension behandelt wird — macht den JSON/Codefence-Bug strukturell unmöglich statt ihn nur zu patchen. KI-generierte Texte landen zunächst als **Vorschlag** (`suggestedAchievements`/`suggestedDevelopmentNeeds` auf `FeedbackDimensionText`, `suggestedText` auf neuer Tabelle `FeedbackCategoryText`) und werden erst durch expliziten „Übernehmen"-Klick in die echten Felder kopiert (gleiches Muster wie die bestehende Rating-Vorschlags-Badge) — persistiert, übersteht also einen Seitenwechsel. Neu: kurzer KI-Text zusätzlich auf Kategorie-Ebene (`FeedbackCategoryText`), nicht nur auf Dimension-Ebene. Cross-Boundary-Falle dabei gefunden: `src/config/constants.ts` importierte bisher `FeedbackRating` aus dem Barrel `'../db'` (re-exportiert Dexie/rpcClient/Seeds) statt direkt aus `'../db/types'` — beim Import von `AI_CONFIG` in `server/db/queries/feedbackStructure.ts` hätte das transitiv Browser-only-Code (`fetch`, `localStorage`) in den Server-TS-Scope gezogen; auf direkten Typ-Import umgestellt und `src/config/constants.ts` zusätzlich explizit in `tsconfig.server.json`s `include` aufgenommen (gleiches Muster wie das dort schon gelistete `src/db/types.ts`). Nachträglich (gleicher Tag, nach erstem Testlauf) zwei Lücken gefixt/ergänzt: (1) `database.ts` migrierte nur neue Tabellen, nie Spalten einer bereits bestehenden Tabelle — `suggestedAchievements` u.ä. auf `feedbackDimensionTexts` fehlten dadurch auf bereits laufenden Installationen und scheiterten erst zur Laufzeit; jetzt gleicht `database.ts` zusätzlich per `PRAGMA table_info` ab und zieht fehlende Spalten per `ALTER TABLE ... ADD COLUMN` nach (siehe Datenbank-Abschnitt oben). (2) Prompt-Vorschau ergänzt: pro Dimension ein „👁 KI-Prompt ansehen"-Toggle auf `AssignmentFeedbackPage.tsx`, der den vollständig gerenderten Prompt (Template + aufgelöste Platzhalter) ohne API-Call anzeigt — Feedback war, dass die generierten Texte „brauchbar, aber nicht konkret genug" waren und ohne Einblick in den tatsächlich gesendeten Prompt nicht nachvollziehbar. Zusätzlich neues optionales Feld `FeedbackCriterionItem.description` (editierbar in `FeedbackStructureConfig.tsx`, direkt unter dem Kriteriumsnamen), das automatisch in `{{KRITERIEN_SCORES}}` einfließt (`src/utils/aiPrompt.ts`) — macht die KI-Vorgabe pro Kriterium einmal zentral pflegbar statt in jedem Dimension-Prompt wiederholt werden zu müssen. Bewusst **nicht** reaktiviert: die bereits vorhandene, aber seit Langem unverdrahtete Legacy-Tabelle `CriterionLevelDescription` (Text pro Kriterium UND Rating-Stufe) — wäre eine präzisere Rubrik gewesen, aber deutlich mehr Pflege-/UI-Aufwand; bei Bedarf spätere, bewusste Entscheidung.
**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