diff --git a/docs/specs/V3_1_-_Spezifikation.md b/docs/specs/V3_1_-_Spezifikation.md new file mode 100644 index 0000000..1299a05 --- /dev/null +++ b/docs/specs/V3_1_-_Spezifikation.md @@ -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` 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`, „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