Spezifikation für V3.1 angelegt

This commit is contained in:
2026-05-05 10:59:57 +02:00
parent 406eac80e4
commit 9c49fc61c0
+924
View File
@@ -0,0 +1,924 @@
# 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