# V3.1 – UX-Polish und Verlauf-Tab-Reife

**Status:** Zur Implementierung freigegeben
**Erstellt:** 2026-05-05
**Überarbeitet:** 2026-05-05 (nach ChatGPT-Review Runden 1, 2 und 3)
**Autor:** Marcus (mit Claude als Mentor)

---

## Ziel

V3.1 ist der konsequente Nachschlag zu V3.0: Was der Produkttest aufgedeckt hat,
wird hier bereinigt. Kein großes Architektur-Feature, kein neues Maven-Modul –
**gezielter UX-Schliff und Robustheit**.

Schwerpunkte:

1. **Polieren** – sichtbare Schwächen aus dem V3.0-Produkttest beheben
   (#77, #80, #81, #83, #84, #88, #91)
2. **Verlauf-Tab reifen lassen** – Suche, Mehrfachauswahl, DB-Neuanlage
   (#82, #86, #87)
3. **Quick Win** – Mausrad-Zoom im PDF-Viewer als kleiner,
   wertvoller Gebrauchskomfort (#32)

Die fachliche Kernverarbeitung bleibt vollständig unverändert.

---

## Einordnung

V3.0 ist der abgeschlossene Ausgangspunkt. Hexagonale Architektur,
Modulstruktur, headless-Betrieb, `.properties`-Konfigurationswahrheit
und Flyway-DB-Evolution bleiben unangetastet.

V3.1 fügt **kein neues Maven-Modul** hinzu.

**Headless-Betrieb:** Der `adapter-in-cli`-Pfad erhält keine neue Bedienfunktion.
Er ist jedoch von der globalen Lock-File-Pfadauflösung (#91) und einer
ggf. notwendigen Flyway-Schemamigration (#88) betroffen – beide Änderungen
wirken beim Programmstart, unabhängig von GUI oder CLI.

---

## Scope

### In V3.1 enthalten

| # | Thema | Kategorie |
|---|---|---|
| #77 | Fehlende Tooltips | UX |
| #80 | Dirty-Indikator für Konfigurations-Tab | UX |
| #81 | Enum-Werte statt deutscher Bezeichnungen (Status-ComboBox + Versuche-Tabelle) | UX |
| #82 | Verlauf-Tab: Live-Filter bei Suche | GUI |
| #83 | KI-Begründung bei SUCCESS-Versuch verwirrend leer | UX |
| #84 | Aktionsbuttons nach Laufende nicht sofort reaktiviert | Bug |
| #86 | Mehrfachauswahl im Verlauf-Tab (Strg+A, Strg+Klick, Shift+Klick) | GUI |
| #87 | Neue leere SQLite-Datenbank anlegen | GUI |
| #88 | FAILED_FINAL-Einträge zeigen keine Fehlerursache im Verlauf-Tab | UX |
| #91 | Lock-File relativer Pfad – Fallback wie Log-Verzeichnis | Robustheit |
| #32 | Mausrad-Zoom in PDF-Vorschau | GUI |

### Explizit nicht in V3.1

- Automatischer Scheduler / Quellordner-Überwachung (#22) → V3.x
- PDF-Viewer Render-DPI (#23) → V3.2
- F1-Hilfe (#69) → V3.2
- Dark Mode (#70) → V3.x
- Log-Viewer in der GUI (#72) → V3.2
- Token- und Kosten-Tracking (#74) → V3.2
- Excel-Export (#75) → V3.2
- Automatische Update-Prüfung (#76) → V3.2
- Änderung der fachlichen Kernverarbeitung
- Neue Maven-Module, neue KI-Provider, Architekturbrüche

---

## Unverrückbare Leitplanken (unverändert gegenüber V3.0)

- Java 21, Maven Multi-Module, hexagonale Architektur
- Shade-JAR als primäres Distributionsartefakt
- GUI ist Standardstart, `--headless` bleibt vollständig erhalten
- `.properties` bleibt die einzige Konfigurationswahrheit
- Kein Webserver, kein Applikationsserver
- GUI offiziell nur unter Windows; headless für Windows Server / Task Scheduler
- JavaFX-Threading: I/O auf Worker-Thread, UI-Updates via `Platform.runLater()`
- Kein JavaFX in Domain oder Application
- JavaDoc-Standard für alle neuen öffentlichen Ports, Use-Cases, DTOs und Adapter-Methoden
- Notwendige Code-Kommentare auf Deutsch; Logging auf Deutsch
- Flyway ist die einzige Schema-Evolutionsquelle (kein manuelles DDL im Code)

---

## Status-Mapping-Tabelle (unverändert gegenüber V3.0)

Diese Tabelle ist weiterhin die einzige autoritative Quelle für Status-Darstellung
in der GUI. Sie gilt verbindlich für alle V3.1-Features, die Statuswerte anzeigen –
insbesondere #81 (Status-ComboBox, Versuche-Tabelle).

**Alle acht Statuswerte müssen vollständig unterstützt werden.**
Kein Enum-Rohname darf für Endnutzer sichtbar sein.

| Domain-Status (`ProcessingStatus`) | GUI-Icon | Farbe | GUI-Text (Tooltip) | Summary-Kategorie |
|---|---|---|---|---|
| `SUCCESS` | `✓` | Grün | „Erfolgreich verarbeitet und umbenannt." | erfolgreich |
| `FAILED_RETRYABLE` | `↻` | Orange | „Temporärer Fehler – wird beim nächsten Lauf automatisch erneut versucht." | wird wiederholt |
| `FAILED_FINAL` | `×` | Rot | „Dauerhaft nicht verarbeitbar – z. B. kein Textinhalt (Foto-PDF), Passwortschutz oder beschädigte Datei. Kein weiterer automatischer Versuch." | fehlgeschlagen |
| `SKIPPED_ALREADY_PROCESSED` | `≡` | Grau | „Übersprungen – wurde bereits in einem früheren Lauf erfolgreich verarbeitet." | übersprungen |
| `SKIPPED_FINAL_FAILURE` | `⊘` | Dunkelgrau | „Endgültig übersprungen nach wiederholten Fehlern." | endgültig übersprungen |
| `READY_FOR_AI` | `⟳` | Blau | „Wartet auf Verarbeitung." | – |
| `PROPOSAL_READY` | `◇` | Hellblau | „KI-Vorschlag liegt vor, wartet auf Bestätigung." | – |
| `PROCESSING` | `▶` | Hellgrau | „Wird gerade verarbeitet." | – |

**Wichtig:** Farbe ist niemals das einzige Unterscheidungsmerkmal.
Icon und Tooltip-Text müssen den Status allein eindeutig beschreiben.

---

## UX-Polishing-Features

### #77 – Fehlende Tooltips

#### Problem

Der V3.0-Produkttest hat GUI-Elemente identifiziert, die noch keinen Tooltip
tragen. Die Infrastruktur (`GuiTooltipTexts`, `setTooltip()`) existiert bereits
aus #66 – es fehlt nur die konsequente Anwendung.

#### Lösung

Vor der Implementierung führt Claude Code eine **vollständige Bestandsaufnahme**
durch: Alle interaktiven Elemente auf allen Tabs werden gegen vorhandene Tooltips
geprüft. Maßgeblich ist die Bestandsaufnahme – die Zahl 16 stammt aus dem
Produkttest und ist nicht bindend. Werden mehr fehlende Elemente gefunden,
werden alle ergänzt.

Fehlende Tooltips werden in `GuiTooltipTexts` als Konstanten ergänzt und
im jeweiligen GUI-Tab via `element.setTooltip(new Tooltip(GuiTooltipTexts.XY))`
gesetzt. Keine hartcodierten Strings.

**Tooltips auf `TableColumn`-Headern (Sonderfall JavaFX):**

`TableColumn` ist kein normaler JavaFX-Node; `setTooltip()` ist darauf nicht
direkt anwendbar. **Kein Skin-/Lookup-Hack.** Falls Header-Tooltips benötigt
werden, wird ein `Label` als Column-Graphic gesetzt:

```java
Label headerLabel = new Label("Spaltenname");
headerLabel.setTooltip(new Tooltip("Erklärungstext"));
column.setGraphic(headerLabel);
column.setText("");
```

Bei der Umsetzung muss geprüft werden, dass Sortierung, Header-Breite
und bestehendes CSS durch das Column-Graphic-Pattern nicht sichtbar
verschlechtert werden.

Falls das Projekt bereits eine stabile eigene Lösung für Column-Tooltips
besitzt, wird diese wiederverwendet.

**Zu prüfende Tabs und Elemente (Anhaltspunkte):**

| Tab | Verdächtige Elemente |
|---|---|
| Verlauf | Tabellenspalten-Header, Suchfeld, Such-Button, Aktions-Buttons (Reset, Löschen) |
| Verlauf (Detail) | Status-Icon, Versuche-Tabelle Spalten, KI-Begründung-Bereich |
| Prompt | Speichern-Button, Zurücksetzen-Button, TextArea |
| Allgemein | Fortschrittsbalken, Summary-Banner-Elemente |

**Technisch:** Ausschließlich `adapter-in-gui` und `GuiTooltipTexts`.
Keine Architektur-Änderungen.

---

### #80 – Dirty-Indikator für Konfigurations-Tab

#### Problem

Der Prompt-Tab zeigt bereits einen `*`-Dirty-Indikator im Tab-Titel und warnt
beim Verlassen mit ungespeicherten Änderungen. Der Konfigurations-Tab hat dieses
Verhalten nicht – Nutzer verlieren versehentlich Änderungen.

#### Lösung

**Dirty-State-Tracking mit Baseline-Snapshot:**

Beim Laden einer Konfiguration wird ein **Baseline-Snapshot** des geladenen Zustands
gespeichert. Dirty-State entsteht durch Vergleich des aktuellen Formularinhalts
mit dem Snapshot – nicht durch blindes „erster Listener feuert".

Während programmgesteuertem Laden oder Normalisieren von Feldinhalten wird
Dirty-Tracking temporär unterdrückt (Flag `loadingInProgress`), damit
programmatische Feldänderungen keinen unechten Dirty-State auslösen.

- Beim ersten echten Nutzerwechsel gegenüber dem Snapshot: Tab-Titel wechselt
  auf `* Konfiguration`
- Dirty-Flag wird zurückgesetzt bei: Speichern, Speichern unter,
  Laden einer neuen Konfiguration (nach Bestätigungsdialog)

**Bestätigungsdialog bei Navigation mit Dirty State:**

Beim Laden einer neuen Konfiguration oder beim Schließen der Anwendung
mit ungespeicherten Konfig-Änderungen:
> „Die Konfiguration enthält ungespeicherte Änderungen. Jetzt speichern?"
> [Speichern] [Verwerfen] [Abbrechen]

**Kopplung mit #87 (Neue Datenbank):**

Legt der Nutzer über „Neue Datenbank anlegen..." eine neue DB-Datei an,
wird der DB-Pfad im Konfigurationsmodell geändert und der Konfig-Tab
in den Dirty-State versetzt. Der bestehende Bestätigungsdialog greift
beim nächsten Schließen oder Ladevorgang.

**UX-Konsistenz mit Prompt-Tab:**

Die UX muss identisch zum Prompt-Tab sein: Sternchen im Tab-Titel,
Warn-/Speicherdialog beim Verlassen, Rücksetzen nach Speichern.
Die **technische Umsetzung** darf im Konfig-Tab über Baseline-Snapshot
und `loadingInProgress` erfolgen, wenn die komplexere Formularlogik
das erfordert.

**Technisch:** Ausschließlich `adapter-in-gui`. Kein neuer Port, kein Use-Case.

---

### #81 – Enum-Werte statt deutscher Bezeichnungen

#### Problem

Die Status-ComboBox im Verlauf-Tab zeigt rohe Enum-Namen (`READY_FOR_AI`,
`FAILED_FINAL` etc.). Die Versuche-Tabelle im Detailbereich zeigt ebenfalls
Enum-Rohnamen in der Status-Spalte. Das ist für Endnutzer unlesbar.

#### Lösung

**Anzeige-Mapping:**

`ProcessingStatusPresentation` (existiert bereits aus #51) stellt die Mapping-Logik
bereit. Dieses Mapping wird für alle Statusanzeigen im Verlauf-Tab verbindlich genutzt.
**Alle acht Statuswerte der autoritativen Tabelle müssen abgedeckt sein:**

| Enum-Wert | Angezeigter Text |
|---|---|
| `SUCCESS` | „✓ Erfolgreich" |
| `FAILED_RETRYABLE` | „↻ Temporärer Fehler" |
| `FAILED_FINAL` | „× Dauerhaft fehlgeschlagen" |
| `SKIPPED_ALREADY_PROCESSED` | „≡ Bereits verarbeitet" |
| `SKIPPED_FINAL_FAILURE` | „⊘ Endgültig übersprungen" |
| `READY_FOR_AI` | „⟳ Wartet auf Verarbeitung" |
| `PROPOSAL_READY` | „◇ Vorschlag vorhanden" |
| `PROCESSING` | „▶ In Bearbeitung" |

**Status-ComboBox:**

- Erster Eintrag: „Alle Status" – GUI-intern als `Optional.empty()` bzw. `null`-Filter
  behandelt; kein Domain-Enum-Wert
- Weitere Einträge: alle acht Statuswerte mit Displaytext
- Intern wird für DB-Queries stets der Enum-Name verwendet
- `StringConverter<ProcessingStatus>` implementieren

**Versuche-Tabelle (Detailbereich):**

- Status-Spalte: `ProcessingStatusPresentation`-Mapping anwenden
- Kein Enum-Rohname darf für Endnutzer sichtbar sein

**Technisch:** Ausschließlich `adapter-in-gui`. Kein neuer Port, kein Use-Case.

---

### #83 – KI-Begründung bei SUCCESS-Versuch verwirrend leer

#### Problem

Im Detailbereich wird bei einem Versuch mit Status `SUCCESS` die
KI-Begründungs-TextArea leer angezeigt. Nutzer verstehen nicht, ob das
ein Fehler ist oder ob tatsächlich keine Begründung vorliegt.

#### Lösung

**Platzhalter über JavaFX `promptText` (kein echter Textinhalt):**

Bei leerem oder null `ai_reasoning` gilt:

```java
textArea.setText("");
textArea.setPromptText("Keine KI-Begründung für diesen Versuch gespeichert.");
```

Der `promptText` wird von JavaFX automatisch gedimmt dargestellt und ist
**nicht kopierbar, nicht speicherbar, nicht als Nutzdaten behandelbar**.
Kein Vermischen von Daten und UI-Platzhaltertext.

Die TextArea bleibt sichtbar – ein leeres Feld ohne Erklärung ist schlechter
als ein erklärender Platzhalter.

**Technisch:** Ausschließlich `adapter-in-gui`. Kein neuer Port, kein Use-Case,
keine DB-Änderung.

---

### #84 – Aktionsbuttons nach Laufende nicht sofort reaktiviert

#### Problem

Nach Abschluss eines Verarbeitungslaufs bleiben die Aktionsbuttons im Verlauf-Tab
(„Status zurücksetzen", „Eintrag löschen") dauerhaft deaktiviert.

#### Lösung

**Ereignisgetriebene Button-State-Neuberechnung:**

Der Button-State wird nach jedem Lauf-Terminierungsereignis neu berechnet –
unabhängig vom Grund der Terminierung:

- Erfolgreicher Laufabschluss
- Fehlerabbruch (Exception im Worker)
- Nutzerabbruch
- Leerlauf (keine Dateien zu verarbeiten)

Nach Terminierung wird, sofern eine Auswahl in der Verlauf-Tabelle besteht,
der zugehörige Aktionsbutton-State **ereignisgetrieben** aktiviert –
ohne dass der Nutzer die Auswahl erneuern oder den Tab wechseln muss.

**Code-Analyse erforderlich:** Claude Code analysiert den genauen Signal-Pfad
(Laufabschluss-Event → UI-Komponente) und korrigiert die fehlende
`Platform.runLater()`-Kopplung.

**Technisch:** Vermutlich `adapter-in-gui` und ggf. `bootstrap` (Bridge-Verdrahtung).
Kein neuer Port, kein Use-Case.

---

### #88 – FAILED_FINAL ohne Fehlerursache im Verlauf-Tab

#### Problem

Der Detailbereich zeigt bei `FAILED_FINAL`-, `FAILED_RETRYABLE`- und
`SKIPPED_FINAL_FAILURE`-Einträgen keine Fehlerursache an.
Der Nutzer sieht nur den Status-Icon.

#### Lösung

**Schema-/Code-Analyse als blockierender erster Schritt:**

Vor jeder weiteren Implementierung dokumentiert Claude Code verbindlich,
welcher Fall vorliegt:

**Fall A – geeignetes Fehlerfeld bereits vorhanden:**
`processing_attempt` enthält bereits ein nutzbares Fehlerfeld.
→ Keine Migration. GUI und Abfrage werden um die Anzeige erweitert.

**Fall B – kein geeignetes Fehlerfeld vorhanden:**
→ Flyway-Migration mit der **nächsten freien Versionsnummer** zum Zeitpunkt
der Implementierung. Fehlerdetails können nur für ab V3.1 erzeugte
Verarbeitungsversuche gespeichert werden. Bestehende Einträge bleiben
unverändert und zeigen den Platzhalter „Keine Fehlerdetails gespeichert."

**Fall C – Fehlerdetails werden bisher nur im Log gespeichert:**
→ Migration zwingend erforderlich. Zusätzlich muss der Fehlerpfad der
Verarbeitungslogik um Persistierung der Fehlerdetails erweitert werden.

**Domain-Modul-Einschränkung:**

`pdf-umbenenner-domain` bleibt unverändert, sofern die benötigten
Fehlerdetails ausschließlich über bestehende oder application-nahe
History-DTOs transportiert werden können.

Falls das fachliche Attempt-Modell im Domain-Modul liegt und für die
Anzeige erweitert werden muss, ist eine **minimale Domain-Erweiterung zulässig**.
Keine Änderung an der fachlichen Kernverarbeitung.

**Datenmodell (bei Migration – Fall B oder C):**

```sql
-- Versionsnummer = nächste freie Flyway-Version zum Zeitpunkt der Implementierung
ALTER TABLE processing_attempt ADD COLUMN failure_details TEXT;
```

`failure_details` enthält eine **nutzerverständliche, gekürzte Fehlerbeschreibung**.
Provider- oder Exception-Meldungen werden **nicht roh persistiert** –
gespeichert wird eine kontrolliert erzeugte Kurzmeldung aus bekannten
Fehlerkategorien oder eine bereinigte/gekürzte Message ohne Stacktrace,
API-Keys oder vollständige Provider-Rohantworten.

Die Begrenzung auf **1000 Zeichen wird spätestens vor Persistierung im
DB-Adapter erzwungen**: Längere Texte werden gekürzt und mit „…" markiert.
Falls bereits vorher ein zentrales Fehler-Mapping existiert, darf dort
gekürzt werden. Entscheidend: in die DB gelangen nur gekürzte, bereinigte
Fehlerdetails. Kein SQL-`CHECK`-Constraint (um Alt-/Importdaten nicht
zu blockieren).

**„Letzter Versuch" – Definition:**

Die angezeigte Fehlerursache stammt aus dem Versuch mit dem höchsten
`attempt_number`. Bei Gleichstand wird der mit dem jüngsten `ended_at` verwendet.

Die Sortierung wird im Rahmen der Code-Analyse gegen das vorhandene Schema
verifiziert. Falls `attempt_number` oder `ended_at` nicht existieren, wird
die technisch eindeutige Sortierung des Attempt-Verlaufs verwendet und
in der Implementierungsnotiz dokumentiert.

**Anzuzeigende Status:**

Fehlerursache wird angezeigt bei:
- `FAILED_FINAL`
- `FAILED_RETRYABLE`
- `SKIPPED_FINAL_FAILURE` (zeigt die letzte bekannte Fehlerursache des
  zugrundeliegenden fehlgeschlagenen Attempts – fachlich konsistent,
  da `SKIPPED_FINAL_FAILURE` direkte Folge eines endgültigen Fehlschlags ist)

Bei fehlendem `failure_details` (NULL oder leer): Platzhaltertext via `promptText`
analog zu #83.

**Technisch:** `adapter-in-gui` (Anzeige), ggf. `adapter-out-db`
(Abfrage-Erweiterung), ggf. Flyway-Migration, ggf. minimale Domain-Erweiterung.

---

### #91 – Lock-File relativer Pfad

#### Problem

Der Lock-Mechanismus nutzt einen konfigurierten oder Standard-Pfad für die
Lock-Datei. Bei relativem Pfad ist das Verzeichnis abhängig vom aktuellen
Arbeitsverzeichnis. Liegt die JAR unter `C:\Program Files`, ist das Verzeichnis
zudem nicht beschreibbar.

#### Lösung

**Verhalten abhängig vom Pfadtyp:**

**Absolut konfigurierter Pfad:**
Wird unverändert verwendet. Schlägt das Anlegen fehl, erfolgt **kein Fallback** –
der Nutzer hat den Speicherort explizit vorgegeben. Start bricht mit klarer
Fehlermeldung ab.

**Relativer oder nicht konfigurierter (Default-)Pfad – zweistufige Fallback-Strategie:**

1. **Primär:** Auflösung relativ zum Verzeichnis der JAR-Datei
   (`CodeSource.getLocation()`)
2. **Fallback:** Auflösung relativ zu `user.home`
3. **Abbruch:** Erst wenn auch `user.home` fehlschlägt

**Parent-Verzeichnisse** werden bei Bedarf automatisch angelegt
(`Files.createDirectories()`).

Der final verwendete **absolute Pfad wird beim Start geloggt** (INFO-Level):
```
Lock-Datei: C:\Users\Funny\Documents\pdf-umbenenner.lock
```

**Gilt für GUI- und Headless-Start.**

**Code-Analyse erforderlich:** Claude Code ermittelt die aktuelle
Lock-Implementierungslokation (`bootstrap` oder `adapter-out-db`).

---

## GUI-Features

### #82 – Verlauf-Tab: Live-Filter bei Suche

#### Problem

Die Suche im Verlauf-Tab wird nur durch expliziten Klick auf den Such-Button
ausgelöst. Das erfordert unnötige Interaktion bei jeder Suchanpassung.

#### Lösung

**Live-Filter mit Debounce und Generation-Counter:**

- Das Suchfeld erhält einen `ChangeListener` auf die `textProperty()`
- Bei jeder Texteingabe startet ein JavaFX-`Timeline`-Debounce-Timer (300 ms)
- Nach 300 ms ohne weitere Eingabe wird die DB-Abfrage auf einem Worker-Thread gestartet

**Race-Condition-Schutz via Generation-Counter:**

Jede gestartete Suchanfrage erhält eine aufsteigende Generations-ID (atomarer
`long`-Counter). Der Worker-Thread trägt seine Generations-ID ins Ergebnis.
Beim `Platform.runLater()`-Callback wird das Ergebnis nur in die UI übernommen,
wenn die Generations-ID noch aktuell ist – veraltete Worker-Ergebnisse
werden verworfen.

**Such-Button und Enter-Taste:**

- Klick auf Such-Button oder Enter im Suchfeld: Debounce-Timer sofort abgebrochen,
  Suche unverzüglich gestartet
- Barrierefreiheit: Such-Button bleibt erhalten

**Auswahlverhalten nach neuen Suchergebnissen:**

Nach jeder Übernahme neuer Suchergebnisse wird die Tabellenauswahl
**vollständig geleert**. Detailbereich und Aktionsbuttons werden entsprechend
zurückgesetzt. Das ist robuster als ein Abgleich der alten Auswahl gegen
die neue Ergebnisliste und vermeidet Wechselwirkungen mit #86.

**Leeres Suchfeld:** Zeigt alle Einträge (bis LIMIT 501).

**Technisch:** Ausschließlich `adapter-in-gui`. Die bestehende Suchabfrage via
`GuiHistoryOverviewPort` wird unverändert wiederverwendet.

---

### #86 – Mehrfachauswahl im Verlauf-Tab

#### Problem

Der Verlauf-Tab erlaubt nur Einzelauswahl. Bulk-Operationen sind nicht möglich.

#### Lösung

**Multi-Select-Modus:**

```java
tableView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
```

JavaFX stellt damit Strg+Klick und Shift+Klick automatisch bereit.

**Strg+A – Fokusabhängig:**

Strg+A selektiert alle sichtbaren Tabelleneinträge **nur, wenn die Verlauf-Tabelle
den Fokus besitzt**. Liegt der Fokus im Suchfeld, bleibt Strg+A die normale
Textauswahl im Suchfeld.

**Detailbereich bei Mehrfachauswahl:**

- Genau 1 Eintrag: Detailbereich wie bisher
- Mehrere Einträge: Platzhaltertext „X Einträge ausgewählt."

**Snapshot der fachlichen Schlüssel vor Worker-Thread-Start:**

Vor dem Start einer Bulk-Operation wird ein **unveränderlicher Snapshot der
fachlichen Schlüssel** erstellt, die die bestehenden Reset-/Delete-Use-Cases
erwarten (typischerweise Fingerprints, sofern das die vorhandene Use-Case-Signatur
erwartet). Der Worker-Thread arbeitet ausschließlich auf diesem Snapshot –
nie auf einer Live-`ObservableList`, die sich während der Operation ändern könnte.

**Aktionsbuttons bei Mehrfachauswahl:**

| Aktion | Verhalten |
|---|---|
| „Status zurücksetzen" | Aktiv bei ≥ 1 Auswahl; Bestätigungsdialog: „X Einträge zurücksetzen?" |
| „Eintrag löschen" | Aktiv bei ≥ 1 Auswahl; Bestätigungsdialog: „X Einträge unwiderruflich löschen?" |

**Bulk-Fehlerstrategie (Partial Success):**

Schlägt eine Operation bei einzelnen Einträgen fehl, werden die restlichen
trotzdem abgearbeitet. Nach Abschluss erscheint ein **kompakter**
Zusammenfassungsdialog:
> „X von Y Einträgen erfolgreich verarbeitet. Z Einträge konnten nicht
>  verarbeitet werden."

Detaillierte Einzelfehler werden geloggt, nicht in den Dialog gestopft.

**Ausführung:** Bulk-Operationen rufen die bestehenden Use-Cases
(`DefaultResetDocumentStatusUseCase`, `DefaultDeleteDocumentHistoryUseCase`)
sequenziell auf dem Worker-Thread auf. Keine neuen Use-Cases erforderlich.

**Sperren während Lauf:** Alle Aktions-Buttons deaktiviert während eines
aktiven Verarbeitungslaufs.

**Technisch:** Ausschließlich `adapter-in-gui`. Keine neuen Ports oder Use-Cases.

---

### #87 – Neue leere SQLite-Datenbank anlegen

#### Problem

Will der Nutzer mit einer frischen Datenbank starten, muss er die Datei
manuell löschen. Das ist umständlich und fehleranfällig.

#### Lösung

**Neuer Menüpunkt:**

`Datenbank → Neue Datenbank anlegen...`

(Nur aktiv wenn kein Verarbeitungslauf läuft.)

**Eigentümer des aktiven Datenbankkontexts:**

Der Runtime-Wechsel der aktiven Datenbank erfordert eine zentrale Komponente,
die den aktiven Datenbankkontext besitzt. Vor der Implementierung analysiert
Claude Code, ob eine solche Komponente bereits existiert.

- **Fall A – wechselbarer DB-Kontext vorhanden:** Vorhandene Komponente
  wird genutzt/erweitert.
- **Fall B – kein wechselbarer DB-Kontext vorhanden:** Es wird ein minimaler
  `ActiveDatabaseContextPort` eingeführt (Outbound-Port in `application`,
  Adapter in `bootstrap` oder `adapter-out-db`). Dieser Port ist die einzige
  Stelle, an der die aktive DB-Referenz umgestellt wird.

**Der DB-Wechsel darf nicht im JavaFX-Code versteckt werden.**
Der Use-Case `DefaultCreateNewDatabaseUseCase` orchestriert den Wechsel;
die physische Umstellung der Verbindung delegiert er über den Port.

**Ablauf (atomar aus Anwendungssicht):**

1. `FileChooser` öffnet (Filter: `*.sqlite`); Nutzer wählt Zieldatei
2. **Pfad-Sicherheitsprüfung:**
   Die aktive DB und die gewählte Zieldatei werden über **normalisierte,
   absolut aufgelöste Pfade** verglichen – kein Rohstring-Vergleich.
   Für existierende Dateien wird `toRealPath()` verwendet; für noch nicht
   existierende Dateien wird der Parent-Pfad real aufgelöst und der Dateiname
   normalisiert verglichen. Unter Windows erfolgt der Vergleich case-insensitive.
   Bei Übereinstimmung: klare Fehlermeldung, kein Überschreiben.
3. Existiert die Zieldatei (andere als aktive DB): Bestätigungsdialog
   „Die Datei existiert bereits. Überschreiben?"
4. **GUI-Sperre:** Während Anlage und Wechsel befindet sich die GUI in einem
   `DB-Busy`-Zustand. Alle DB-lesenden und DB-schreibenden Aktionen
   (Live-Suche, Bulk-Reset, Bulk-Delete, Verlauf-Refresh, erneuter
   Klick auf „Neue Datenbank anlegen") sind deaktiviert. Der Zustand
   wird nach Erfolg oder Fehler zuverlässig zurückgesetzt.
5. Neue SQLite-Datei wird als **temporäre Datei im Zielverzeichnis** erzeugt
6. Flyway führt alle verfügbaren Migrationsskripte gegen die temporäre Datei aus
   (`migrate()` auf neuesten Schema-Stand)
7. Neue DB-Verbindung wird **testweise geöffnet und geprüft** (gegen Temp-Datei).
   Der Verbindungstest prüft mindestens:
   - SQLite-Verbindung kann geöffnet werden
   - Flyway-Schema-History ist vorhanden
   - Eine einfache Leseabfrage gegen Schema-Metadaten ist erfolgreich
8. Erst nach erfolgreichem Test: temporäre Datei zur Zieldatei verschoben.
   Bei bereits existierender, bestätigter Zieldatei wird
   `Files.move(tempFile, targetFile, ATOMIC_MOVE, REPLACE_EXISTING)` verwendet,
   sofern vom Dateisystem unterstützt. Die vorhandene Zieldatei wird vorher
   **nicht separat gelöscht**. Wird die Kombination `ATOMIC_MOVE + REPLACE_EXISTING`
   nicht unterstützt, bricht der Vorgang mit klarer Fehlermeldung ab –
   kein unsicherer halb-atomarer Fallback.
9. Aktive DB-Referenz der Anwendung umgestellt (via `ActiveDatabaseContextPort`)
10. Verlauf-Tab neu geladen → zeigt „Noch keine Verarbeitungen vorhanden."
11. Statuszeile aktualisiert DB-Pfad
12. DB-Pfad im Konfigurationsmodell geändert → Konfig-Tab wechselt in Dirty-State
13. Statuszeile oder Meldungsbereich zeigt:
    „Neue Datenbank ist aktiv. Konfiguration speichern, damit diese DB
    beim nächsten Start verwendet wird."

**Fehlerfall ohne partielle Änderung:**

Schlägt ein Schritt (Anlegen, Flyway, Verbindungstest, Move) fehl, bleibt die
bisher aktive DB **vollständig unverändert in Betrieb**. Die temporäre Datei
wird gelöscht. Fehlerdialog mit konkreter Meldung.

**Headless:** Die Funktion ist ausschließlich GUI-seitig aufrufbar.
`adapter-in-cli` ist nicht betroffen.

**Architektur:**

| Komponente | Typ | Modul | Zweck |
|---|---|---|---|
| `CreateNewDatabaseUseCase` | Inbound-Port-Interface | `application` | Vertrag: `createNewDatabase(Path)` |
| `DefaultCreateNewDatabaseUseCase` | Use-Case-Impl. | `application` | Atomarer DB-Wechsel: Temp-Datei, Flyway, Test, Move, Kontext-Umstellung |
| `DatabaseCreationPort` | Outbound-Port | `application` | `createAndInitialize(Path tempFile)` |
| `ActiveDatabaseContextPort` | Outbound-Port | `application` | `switchActiveDatabase(Path newDbFile)` – Eigentümer des Laufzeitkontexts |
| `GuiCreateNewDatabasePort` | Bridge-Interface | `adapter-in-gui` | Brücke zum Use-Case |
| `SqliteDatabaseCreationAdapter` | Outbound-Adapter | `adapter-out-db` | SQLite-Temp-Datei erzeugen, Flyway migrate auf latest, Verbindung testen |
| `SqliteActiveDatabaseContextAdapter` | Outbound-Adapter | `bootstrap` oder `adapter-out-db` | Umschalten der aktiven DB-Referenz (Analyse erforderlich) |

---

### #32 – Mausrad-Zoom in PDF-Vorschau

#### Problem

Die PDF-Vorschau lässt sich nur über die Zoom-Buttons skalieren.
Ein Mausrad-Zoom fehlt.

#### Lösung

**Scroll-Event auf der PDF-Vorschau-Komponente:**

```java
scrollPane.addEventFilter(ScrollEvent.SCROLL, event -> {
    if (event.isControlDown()) {
        accumulateAndApplyZoomDelta(event.getDeltaY());
        event.consume(); // immer konsumieren bei Strg, kein paralleles Scrollen
    }
    // ohne Strg: normales Scrollen bleibt
});
```

**Bei gedrückter Strg-Taste werden ScrollEvents grundsätzlich konsumiert**,
damit kein paralleles Scrollen im ScrollPane erfolgt – auch wenn der Delta
zu klein für einen Zoomschritt ist.

**Delta-Akkumulation für Trackpad-Kompatibilität:**

Sehr kleine Trackpad-Deltas werden **intern akkumuliert**, bis die Mindestschwelle
für einen Zoomschritt erreicht ist. Kein Verwerfen: akkumulierte Deltas
ergeben bei genug Trackpad-Wischbewegung sauber einen Zoomschritt.
Als Orientierungswert gilt ±10 % je „Notch" eines Standard-Mausrads.

**Zoom-Verhalten:**

| Parameter | Wert |
|---|---|
| Auslöser | Strg + Mausrad |
| Schrittweite | Vorzeichenbasiert auf akkumuliertem `deltaY`, ca. 10 % je Notch |
| Minimum | 10 % |
| Maximum | 500 % |
| Zurücksetzen bei neuem PDF | Ja (Zoom auf Fit-to-Width) |

**Fit-to-Width-Modus:**

Nach manuellem Strg+Mausrad-Zoom verlässt die Vorschau den Fit-to-Width-Modus.
Fit-to-Width wird erst wieder aktiv, wenn ein neues PDF geladen oder der
Fit-to-Width-Button explizit erneut betätigt wird.

**Viewport-Stabilität:**

Beim Zoom bleibt die sichtbare Viewport-Mitte möglichst erhalten.

**Zoom-State-Konsistenz:**

Der Zoom-State wird über dieselbe Variable geführt, die auch die
Toolbar-Zoom-Buttons bedienen.

**Technisch:** Ausschließlich `adapter-in-gui`. Kein neuer Port, kein Use-Case.

---

## Architektur-Zusammenfassung

### Neue Inbound-Port-Interfaces und Use-Cases

| Komponente | Typ | Modul | Zweck | Issue |
|---|---|---|---|---|
| `CreateNewDatabaseUseCase` | Inbound-Port-Interface | `application` | Vertrag für DB-Anlage | #87 |
| `DefaultCreateNewDatabaseUseCase` | Use-Case-Impl. | `application` | Atomarer DB-Wechsel via Temp-Datei + Port-Delegation | #87 |

### Neue Outbound-Ports

| Komponente | Modul | Zweck | Issue |
|---|---|---|---|
| `DatabaseCreationPort` | `application` | Temp-Datei erzeugen, Flyway, Verbindungstest | #87 |
| `ActiveDatabaseContextPort` | `application` | `switchActiveDatabase(Path)` – Laufzeit-DB-Kontext | #87 |

### Neue Bridge-Interfaces (adapter-in-gui)

| Interface | Zweck | Issue |
|---|---|---|
| `GuiCreateNewDatabasePort` | Brücke zur DB-Anlage | #87 |

### Neue Adapter

| Adapter | Modul | Zweck | Issue |
|---|---|---|---|
| `SqliteDatabaseCreationAdapter` | `adapter-out-db` | SQLite-Temp-Datei, Flyway migrate auf latest, Test | #87 |
| `SqliteActiveDatabaseContextAdapter` | `bootstrap` oder `adapter-out-db` | Umschalten der aktiven DB-Referenz (Lokation via Code-Analyse) | #87 |

### Geänderte Komponenten (adapter-in-gui)

| Komponente | Änderung | Issues |
|---|---|---|
| `GuiHistoryTab` | Multi-Select + Schlüssel-Snapshot, Live-Filter + Generation-Counter + Auswahl leeren, Fehlerursache, Platzhalter via promptText, Tooltips, DB-Busy-Sperre | #82, #83, #86, #88, #77, #87 |
| `GuiConfigTab` | Dirty-State mit Baseline-Snapshot + loadingInProgress, Tab-Titel, Dialog, Kopplung mit #87 | #80 |
| `GuiTooltipTexts` | Neue Tooltip-Konstanten; TableColumn-Header via Column-Graphic-Pattern | #77 |
| Verlauf-Detailbereich | Enum-Displaytext (alle 8 Werte), Fehlerursache für FAILED/SKIPPED_FINAL | #81, #88 |
| Status-ComboBox | `StringConverter<ProcessingStatus>`, „Alle Status" als GUI-interner Null-Filter | #81 |
| PDF-Vorschau-Komponente | Delta-Akkumulation, Strg+Scroll konsumiert, Viewport-Stabilität, Fit-to-Width-Modus | #32 |
| Lauf-Abschluss-Signalkette | Ereignisgetriebene Button-State-Neuberechnung für alle Terminierungsgründe | #84 |

### Geänderte Komponenten (sonstige)

| Komponente | Modul | Änderung | Issue |
|---|---|---|---|
| Lock-File-Auflösung | `bootstrap` oder `adapter-out-db` | Absolut: direkt + Abbruch; Relativ: JAR-Dir → user.home → Abbruch; Parent-Dirs; Logging | #91 |

### Nicht geändert

- `pdf-umbenenner-domain` – keine Änderungen, außer ggf. minimale Erweiterung
  für #88 falls Attempt-Modell dort liegt (zulässig, keine Kernverarbeitungslogik)
- `pdf-umbenenner-adapter-in-cli` – keine neuen Funktionen
- Headless-Verarbeitungslogik – vollständig unberührt
- Kernverarbeitungslogik (PDF lesen → KI → umbenennen)

---

## Datenbankmigrationen

Flyway ist die einzige Schema-Evolutionsquelle.

### Potenzielles Migrationsskript (abhängig von Code-Analyse #88)

Vor der Implementierung von #88 dokumentiert Claude Code verbindlich,
ob ein Fehlerfeld bereits im Schema existiert (Fall A / B / C – siehe #88).

**Nur bei Fall B oder C:**

```sql
-- Fehlerdetails in processing_attempt ergänzen
-- Versionsnummer = nächste freie Flyway-Version zum Zeitpunkt der Implementierung
ALTER TABLE processing_attempt ADD COLUMN failure_details TEXT;
```

- `failure_details`: nutzerverständliche, gekürzte Fehlerbeschreibung;
  Begrenzung auf 1000 Zeichen **vor Persistierung im Adapter** erzwungen,
  Kürzung mit „…"; kein SQL-`CHECK`-Constraint
- Bestehende Zeilen erhalten automatisch `NULL` – kein Datenverlust
- Alte Einträge ohne Fehlerdetails zeigen `promptText`-Platzhalter in der GUI

---

## Definition of Done (V3.1 gesamt)

- [ ] Alle 11 Issues implementiert und einzeln getestet
- [ ] `mvn clean verify` grün (alle Module, kein `-DskipTests`)
- [ ] `mvn clean install -Drevision=3.1.0` – Build ohne Fehler
- [ ] Manueller GUI-Produkttest durchgeführt (Green build ≠ fertige Software)
- [ ] Keine Enum-Rohnamen in der GUI sichtbar (alle 8 Statuswerte mit Displaytext)
- [ ] Alle fehlenden Tooltips vorhanden; TableColumn-Header via Column-Graphic-Pattern
- [ ] Dirty-Indikator Konfig-Tab: kein programmgesteuertes Feuern, Baseline-Snapshot korrekt
- [ ] Live-Filter: 300 ms Debounce, Generation-Counter, Auswahl nach Suche geleert
- [ ] Mehrfachauswahl: Strg+A nur bei Tabellenfokus; Schlüssel-Snapshot; Partial-Success-Dialog
- [ ] `FAILED_FINAL`/`FAILED_RETRYABLE`/`SKIPPED_FINAL_FAILURE`: Fehlerursache sichtbar (oder Platzhalter)
- [ ] Leere `ai_reasoning`: `promptText`-Platzhalter (kein echter Text)
- [ ] Aktionsbuttons ereignisgetrieben reaktiviert nach allen Terminierungsgründen
- [ ] #87 Code-Analyse: DB-Kontext-Eigentümer dokumentiert (Fall A oder B)
- [ ] #87: Atomarer Ablauf via Temp-Datei; Pfadvergleich normalisiert + case-insensitive
- [ ] #87: Aktive DB bleibt bei Fehler unverändert; DB-Busy-Sperre korrekt zurückgesetzt
- [ ] #87: Flyway auf neuesten Stand; Hinweismeldung nach Wechsel
- [ ] Strg+Mausrad-Zoom: Delta-Akkumulation, immer konsumiert bei Strg, 10%–500%
- [ ] Lock-File: Absolut direkt; Relativ zweistufig; Parent-Dirs; Pfad geloggt
- [ ] Code-Kommentare auf Deutsch; Logging auf Deutsch
- [ ] JavaDoc auf allen neuen öffentlichen Ports, Use-Cases und Adapter-Methoden
- [ ] `betrieb.md` und `gui-bedienanleitung.md` auf V3.1-Stand gebracht
- [ ] Freigabedokument `freigabe-v3_1.md` erstellt

---

## Abnahmekriterien je Feature

### #77 Fehlende Tooltips
- [ ] Vollständige Bestandsaufnahme: Liste aller Elemente ohne Tooltip erstellt
- [ ] Alle identifizierten Elemente haben Tooltips (Anzahl aus Bestandsaufnahme)
- [ ] TableColumn-Header: Column-Graphic mit Label+Tooltip, kein Skin-/Lookup-Hack
- [ ] Column-Graphic: Sortierung, Header-Breite und CSS nicht sichtbar verschlechtert
- [ ] Neue Konstanten ausschließlich in `GuiTooltipTexts`, keine hartcodierten Strings

### #80 Dirty-Indikator Konfig-Tab
- [ ] Tab-Titel `* Konfiguration` nur nach echter Nutzeränderung gegenüber Baseline-Snapshot
- [ ] Programmgesteuertes Laden setzt kein Dirty-Flag (`loadingInProgress`-Schutz)
- [ ] Tab-Titel `Konfiguration` nach Speichern
- [ ] Bestätigungsdialog bei Laden neuer Konfig mit Dirty State
- [ ] DB-Pfad-Wechsel via #87 setzt Konfig-Tab dirty
- [ ] UX identisch zum Prompt-Tab (Sternchen, Dialog, Reset)

### #81 Enum-Bezeichnungen
- [ ] Status-ComboBox: „Alle Status" als erster Eintrag (GUI-interner Null-Filter)
- [ ] Status-ComboBox: alle 8 Statuswerte als Displaytext
- [ ] Versuche-Tabelle: alle 8 Statuswerte als Displaytext
- [ ] DB-Queries intern weiterhin mit Enum-Namen
- [ ] Kein Enum-Rohname für Endnutzer sichtbar

### #82 Live-Filter
- [ ] Suche startet nach 300 ms Tipp-Pause automatisch
- [ ] Generation-Counter: veraltete Worker-Ergebnisse werden verworfen
- [ ] Such-Button / Enter: sofortige Suche, Debounce abgebrochen
- [ ] Auswahl nach neuen Suchergebnissen vollständig geleert
- [ ] Leeres Suchfeld zeigt alle Einträge
- [ ] Worker-Thread, UI via `Platform.runLater()`

### #83 KI-Begründung leer
- [ ] `textArea.setPromptText(...)` bei leerem/null `ai_reasoning`
- [ ] `textArea.setText("")` – kein Platzhaltertext als echter Inhalt
- [ ] TextArea bleibt sichtbar

### #84 Buttons reaktivieren
- [ ] Aktionsbuttons während Lauf deaktiviert
- [ ] Reaktivierung ereignisgetrieben nach: Erfolg, Fehlerabbruch, Nutzerabbruch, Exception
- [ ] Keine manuellen Workarounds notwendig

### #86 Mehrfachauswahl
- [ ] `SelectionMode.MULTIPLE` aktiv
- [ ] Strg+A nur bei Tabellenfokus (kein Konflikt mit Suchfeld)
- [ ] Strg+Klick, Shift+Klick korrekt
- [ ] Detailbereich: „X Einträge ausgewählt." bei Mehrfachauswahl
- [ ] Schlüssel-Snapshot vor Worker-Thread-Start
- [ ] Bulk-Reset: Bestätigungsdialog + Partial-Success-Dialog
- [ ] Bulk-Delete: Bestätigungsdialog + Partial-Success-Dialog
- [ ] Aktionen während Lauf gesperrt

### #87 Neue Datenbank anlegen
- [ ] Code-Analyse: DB-Kontext-Eigentümer dokumentiert, Fall A oder B entschieden
- [ ] Menüpunkt vorhanden, nur außerhalb von Läufen aktiv
- [ ] Aktive DB über normalisierten Pfadvergleich (case-insensitive, toRealPath) erkannt
- [ ] Bestehende Fremddatei: Überschreiben-Bestätigung
- [ ] DB-Busy-Sperre während Anlage aktiv; nach Erfolg/Fehler zuverlässig zurückgesetzt
- [ ] Neue DB als Temp-Datei; Flyway auf neuesten Stand
- [ ] Verbindungstest: Verbindung öffnen, Flyway-History prüfen, Leseabfrage erfolgreich
- [ ] Move mit `ATOMIC_MOVE + REPLACE_EXISTING`; vorhandene Datei nicht vorher separat löschen
- [ ] Kein halb-atomarer Fallback bei nicht unterstützter Kombination
- [ ] Fehlerfall: Temp-Datei gelöscht, aktive DB unverändert, Fehlerdialog
- [ ] `ActiveDatabaseContextPort.switchActiveDatabase()` schaltet Referenz um
- [ ] Verlauf-Tab: „Noch keine Verarbeitungen vorhanden."
- [ ] Statuszeile aktualisiert DB-Pfad
- [ ] Konfig-Tab wechselt in Dirty-State
- [ ] Hinweismeldung: Konfiguration speichern nicht vergessen

### #88 Fehlerursache FAILED_FINAL
- [ ] Schema-/Code-Analyse: Fall A/B/C dokumentiert vor Implementierung
- [ ] Ggf. Flyway-Migration mit nächster freier Versionsnummer
- [ ] Sortierung für „letzter Versuch" gegen Schema verifiziert
- [ ] Detailbereich: `failure_details` bei `FAILED_FINAL`, `FAILED_RETRYABLE`, `SKIPPED_FINAL_FAILURE`
- [ ] NULL/leer: `promptText`-Platzhalter
- [ ] 1000-Zeichen-Grenze spätestens vor DB-Persistierung erzwungen, Kürzung mit „…"
- [ ] Keine rohen Provider-/Exception-Meldungen persistiert

### #91 Lock-File Pfad
- [ ] Absoluter Pfad: direkt verwendet, kein Fallback, Abbruch bei Fehler
- [ ] Relativer Pfad: erst JAR-Verzeichnis, dann `user.home`, dann Abbruch
- [ ] Parent-Verzeichnisse automatisch angelegt
- [ ] Absoluter Pfad beim Start geloggt (INFO)
- [ ] Gilt für GUI- und Headless-Start

### #32 Mausrad-Zoom
- [ ] Strg+Scroll: Event grundsätzlich konsumiert (kein paralleles Scrollen)
- [ ] Delta-Akkumulation für kleine Trackpad-Deltas
- [ ] Zoom 10%–500%, ca. 10 % je Notch
- [ ] Ohne Strg: normales Scrollen
- [ ] Viewport-Mitte beim Zoom möglichst stabil
- [ ] Fit-to-Width-Modus verlassen nach manuellem Zoom
- [ ] Zoom-Reset bei neuem PDF (Fit-to-Width)
- [ ] Zoom-State konsistent mit Toolbar-Zoom-Buttons
