# V3.0 – Infrastruktur, Transparenz und Bedienkomfort

**Status:** Freigegeben zur Implementierung
**Erstellt:** 2026-04-29
**Überarbeitet:** 2026-04-30 (nach ChatGPT-Review Runden 1, 2 und 3)
**Autor:** Marcus (mit Claude als Mentor)

---

## Ziel

V3.0 ist kein Wechsel der Kernfunktion, sondern ein gezielter Qualitätssprung
in drei Dimensionen:

1. **Infrastruktur** – saubere Versionierung, Flyway-DB-Migration,
   Jenkins-Optimierung und MSI-Stabilisierung schaffen ein solides technisches
   Fundament für alle weiteren Ausbaustufen.

2. **Transparenz** – der Historien-Tab macht die SQLite-Datenbank sichtbar;
   Fehlerstatus werden benutzerfreundlich erklärt; ein Summary-Banner liefert
   nach jedem Lauf sofort den Überblick.

3. **Bedienkomfort** – Tooltips, Statuszeile, Prompt-Editor und die Klärung der
   Fehlerstatus-Semantik reduzieren den täglichen Reibungsverlust spürbar.

---

## Einordnung

V2.9 ist der abgeschlossene Ausgangspunkt. Die fachliche Kernverarbeitung
(PDF lesen → KI benennen → Zieldatei kopieren) bleibt in V3.0 **vollständig
unverändert**. Hexagonale Architektur, Modulstruktur, headless-Betrieb und
`.properties`-Konfigurationswahrheit bleiben unangetastet.

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

---

## Scope

### In V3.0 enthalten

| # | Thema | Kategorie |
|---|---|---|
| #49 | Flyway für SQLite-DB-Migration | Infrastruktur |
| #65 | MSI-Installer Review & Optimierung | Infrastruktur |
| #67 | Konsistente Versionierung (POM / App / Jenkins) | Infrastruktur |
| #68 | Jenkins-Build-Optimierung | Infrastruktur |
| #51 | FAILED_RETRYABLE vs. FAILED_FINAL klären | Fachlich/UX |
| #7  | Historien-Tab (SQLite-DB-Ansicht) | GUI |
| #50 | Statuszeile | GUI |
| #66 | Tooltips | UX |
| #71 | Prompt-Editor in der GUI | GUI |
| #73 | Lauf-Statistik / Summary-Banner | GUI |

### Explizit nicht in V3.0

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

---

## Unverrückbare Leitplanken (unverändert gegenüber V2.x)

- 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

---

## Verbindliche Status-Mapping-Tabelle

Diese Tabelle ist die einzige autoritative Quelle für Status-Darstellung in der GUI.
**Die Statusnamen wurden gegen den Domain-Enum `ProcessingStatus`
und die SQLite-Persistierung geprüft und entsprechen 1:1 dem Code.**
Persistierung via `.name()` – gespeicherter String = Enum-Konstantenname.

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

**`PROCESSING` wird nicht als unvollständiger `processing_attempt` persistiert.**
`processing_attempt` wird erst nach vollständigem Abschluss eines Versuchs
geschrieben – `ended_at NOT NULL` bleibt daher immer erfüllbar.
`PROCESSING` tritt ausschließlich in `document_record.overall_status`
während eines laufenden Verarbeitungslaufs auf.

---

## Infrastruktur-Features

### #67 – Konsistente Versionierung

#### Problem

- Alle Maven-POMs: dauerhaft `0.0.1-SNAPSHOT`
- Anwendungsversion (`2.5.0`): manuell hardcoded als `app.version` im Packaging-POM
- Jenkins: MAJOR/MINOR als manuelle Parameter, werden nicht an Maven übergeben

#### Lösung: Maven CI-friendly Versioning

Im **Parent-POM** wird `${revision}` als zentrale Versions-Property eingeführt:

```xml
<properties>
    <revision>0.0.1-SNAPSHOT</revision>
</properties>
<version>${revision}</version>
```

In allen **Kind-Modulen** (nur die `<parent>`-Referenz):

```xml
<parent>
    <version>${revision}</version>
</parent>
```

**Wichtig:** Interne Modul-Abhängigkeiten zwischen Kind-Modulen verwenden
`${project.version}`, **nicht** `${revision}`:

```xml
<!-- korrekt für interne Modul-Abhängigkeiten: -->
<dependency>
    <artifactId>pdf-umbenenner-domain</artifactId>
    <version>${project.version}</version>
</dependency>
```

`flatten-maven-plugin` wird im **Parent-POM unter `<build><plugins>`** aktiviert
(nicht nur unter `<pluginManagement>` – sonst wird es nicht ausgeführt):

```xml
<build>
    <plugins>
        <plugin>
            <groupId>org.codehaus.mojo</groupId>
            <artifactId>flatten-maven-plugin</artifactId>
            <version>1.6.0</version>
            <configuration>
                <updatePomFile>true</updatePomFile>
                <flattenMode>resolveCiFriendliesOnly</flattenMode>
            </configuration>
            <executions>
                <execution>
                    <id>flatten</id>
                    <phase>process-resources</phase>
                    <goals><goal>flatten</goal></goals>
                </execution>
                <execution>
                    <id>flatten.clean</id>
                    <phase>clean</phase>
                    <goals><goal>clean</goal></goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>
```

#### Versionsnummer-Schema

```
MAJOR.MINOR.BUILD_NUMBER  (Beispiel: 3.0.42)
```

- MAJOR/MINOR: manuell per Jenkins-Parameter (wie bisher)
- BUILD_NUMBER: automatisch durch Jenkins
- Jenkins übergibt: `-Drevision=${EFFECTIVE_MAJOR}.${EFFECTIVE_MINOR}.${BUILD_NUMBER}`

#### Verifikation

Nach `mvn clean install -Drevision=3.0.1` muss geprüft werden:
- Kein installiertes POM in `~/.m2` enthält noch `${revision}` unaufgelöst
- `java -jar pdf-ki-renamer-3.0.1.jar` zeigt in der Statuszeile exakt `V3.0.1`

#### MANIFEST.MF

Das Shade-Plugin im Bootstrap-Modul befüllt `Implementation-Version`.
Es muss sichergestellt werden, dass der Manifest-Transformer die Einträge
korrekt in das finale Fat-JAR überträgt:

```xml
<manifestEntries>
    <Implementation-Version>${revision}</Implementation-Version>
    <Implementation-Title>PDF KI Renamer</Implementation-Title>
</manifestEntries>
```

Laufzeit-Auslesen mit Fallback:

```java
String version = getClass().getPackage().getImplementationVersion();
if (version == null) {
    version = "dev"; // Fallback für IDE-Start und ungepackten Betrieb
}
```

#### `pdf-umbenenner-packaging`

```xml
<appVersion>${revision}</appVersion>
```

---

### #68 – Jenkins-Build-Optimierung

#### Aktueller Stand

- `mvn clean verify` ohne `-Drevision` → POM bleibt `0.0.1-SNAPSHOT`
- JAR-Archivierung fragil
- MSI-Build nicht im CI (Linux-Container kann kein Windows-MSI bauen)

#### Verbesserungen

**1. Versionsübergabe an Maven:**

```groovy
stage('Maven Build') {
    steps {
        sh "mvn clean verify -Drevision=${env.EFFECTIVE_MAJOR}.${env.EFFECTIVE_MINOR}.${env.BUILD_NUMBER}"
    }
}
```

**2. Robuste Shade-JAR-Archivierung (exakt ein JAR erzwingen, Bash explizit):**

```groovy
stage('Archive JAR') {
    steps {
        sh '''#!/usr/bin/env bash
set -euo pipefail
mapfile -t JARS < <(find pdf-umbenenner-bootstrap/target \
    -maxdepth 1 -name "pdf-umbenenner-bootstrap-*.jar" \
    ! -name "*-sources.jar" ! -name "*-javadoc.jar")
test "${#JARS[@]}" -eq 1 \
    || { echo "FEHLER: Erwartet genau 1 Shade-JAR, gefunden: ${#JARS[@]}"; exit 1; }
JAR_NAME="pdf-ki-renamer-${EFFECTIVE_MAJOR}.${EFFECTIVE_MINOR}.${BUILD_NUMBER}.jar"
cp "${JARS[0]}" "$JAR_NAME"
'''
        archiveArtifacts artifacts: 'pdf-ki-renamer-*.jar', fingerprint: true
    }
}
```

**3. MSI als manuell dokumentieren:**

Im Jenkinsfile-Kommentar und in `betrieb.md`:
> MSI-Build ist Windows-only (`jpackage + WiX Toolset 3.x`). Jenkins läuft im
> Linux-Container auf Synology NAS und kann kein MSI erzeugen. Der MSI-Build
> wird bewusst manuell auf der Windows-Entwicklungsmaschine ausgeführt:
> `.\mvnw.cmd clean package -P release -pl pdf-umbenenner-packaging --also-make -DskipTests`

#### Unverändert

MAJOR/MINOR-Persistenz in State-Datei, PIT/JaCoCo/JUnit-Berichte,
Artefakt-Ablage nach `/builds/`.

---

### #49 – Flyway für SQLite-DB-Migration

#### Motivation

Die manuelle `evolveTableColumns()`-Logik via `DatabaseMetaData.getColumns()`
ist schwer nachvollziehbar und fehleranfällig. Flyway ersetzt sie durch
versionierte, reproduzierbare Migrationsskripte.

#### Abhängigkeiten

```xml
<dependency>
    <groupId>org.flywaydb</groupId>
    <artifactId>flyway-core</artifactId>
    <!-- Version zentral im Parent-POM pinnen -->
</dependency>
```

Der SQLite-JDBC-Treiber (`org.xerial:sqlite-jdbc`) ist bereits im Projekt
vorhanden und bleibt unverändert. Flyway nutzt ihn über die DataSource.
URL-Format: `jdbc:sqlite:<pfad-zur-datei>`.

#### Migrationsstrategie: Ein einziges Basisskript

`V1__initial_schema.sql` enthält das **vollständige Zielschema** aller Spalten
und Constraints. Es gibt keine V2 oder V3. Für neue DBs läuft V1 vollständig.
Für bestehende V2.9-DBs wird nach vollständiger Schema-Prüfung auf V1 baselined.

Ablage: `pdf-umbenenner-adapter-out/src/main/resources/db/migration/`

**`V1__initial_schema.sql`:**

```sql
CREATE TABLE document_record (
    id                          INTEGER PRIMARY KEY AUTOINCREMENT,
    fingerprint                 TEXT    NOT NULL,
    last_known_source_locator   TEXT    NOT NULL,
    last_known_source_file_name TEXT    NOT NULL,
    overall_status              TEXT    NOT NULL,
    content_error_count         INTEGER NOT NULL DEFAULT 0,
    transient_error_count       INTEGER NOT NULL DEFAULT 0,
    last_failure_instant        TEXT,
    last_success_instant        TEXT,
    created_at                  TEXT    NOT NULL,
    updated_at                  TEXT    NOT NULL,
    last_target_path            TEXT,
    last_target_file_name       TEXT,
    CONSTRAINT uq_document_record_fingerprint UNIQUE (fingerprint)
);

CREATE TABLE processing_attempt (
    id                     INTEGER PRIMARY KEY AUTOINCREMENT,
    fingerprint            TEXT    NOT NULL,
    run_id                 TEXT    NOT NULL,
    attempt_number         INTEGER NOT NULL,
    started_at             TEXT    NOT NULL,
    ended_at               TEXT    NOT NULL,
    status                 TEXT    NOT NULL,
    failure_class          TEXT,
    failure_message        TEXT,
    retryable              INTEGER NOT NULL DEFAULT 0,
    model_name             TEXT,
    prompt_identifier      TEXT,
    processed_page_count   INTEGER,
    sent_character_count   INTEGER,
    ai_raw_response        TEXT,
    ai_reasoning           TEXT,
    resolved_date          TEXT,
    date_source            TEXT,
    validated_title        TEXT,
    final_target_file_name TEXT,
    ai_provider            TEXT,
    CONSTRAINT fk_processing_attempt_fingerprint
        FOREIGN KEY (fingerprint) REFERENCES document_record (fingerprint),
    CONSTRAINT uq_processing_attempt_fingerprint_number
        UNIQUE (fingerprint, attempt_number)
);

CREATE INDEX idx_processing_attempt_fingerprint
    ON processing_attempt (fingerprint);

CREATE INDEX idx_processing_attempt_run_id
    ON processing_attempt (run_id);

CREATE INDEX idx_document_record_overall_status
    ON document_record (overall_status);
```

#### Differenzierte Startstrategie

Die Flyway-Initialisierung in `SqliteSchemaInitializationAdapter` unterscheidet
drei Fälle. **`baselineOnMigrate(true)` wird ausschließlich in Fall 2 gesetzt.**

```
Fall 1: DB-Datei existiert nicht, oder existiert aber enthält
        keine fachlichen Tabellen und keine Flyway-History-Tabelle
        („leer" = keine Tabellen, nicht nur Dateigröße 0 Byte)
→ Flyway mit baselineOnMigrate=false
→ V1__initial_schema.sql wird ausgeführt → vollständige neue DB

Fall 2: DB existiert, enthält fachliche Tabellen, aber keine Flyway-History-Tabelle
        (bestehende V2.9-DB)
→ Vollständige Schema-Prüfung gegen V1-Zielschema (siehe Checkliste unten)
→ Bei vollständig konformem Schema:
    Backup erstellen (datierte Kopie der .sqlite-Datei)
    Flyway mit baselineOnMigrate=true, baselineVersion="1"
    migrate() → nur zukünftige Migrationen ab V2 werden ausgeführt
→ Bei nicht konformem Schema:
    Start abbrechen mit klarer Fehlermeldung, kein stilles Weiterlaufen

Fall 3: DB existiert, Flyway-History-Tabelle vorhanden (regulärer Folgestart)
→ Flyway mit baselineOnMigrate=false → migrate() (idempotent)
```

#### Vollständige Schema-Prüfcheckliste für Fall 2

Da nach Baseline V1 **nicht mehr ausgeführt wird**, muss die bestehende DB
**vollständig** dem V1-Zielschema entsprechen. Geprüft wird via `DatabaseMetaData`:

**Tabellen:** `document_record` und `processing_attempt` müssen existieren.

**Alle Spalten `document_record`:**
`id`, `fingerprint`, `last_known_source_locator`, `last_known_source_file_name`,
`overall_status`, `content_error_count`, `transient_error_count`,
`last_failure_instant`, `last_success_instant`, `created_at`, `updated_at`,
`last_target_path`, `last_target_file_name`

**Alle Spalten `processing_attempt`:**
`id`, `fingerprint`, `run_id`, `attempt_number`, `started_at`, `ended_at`,
`status`, `failure_class`, `failure_message`, `retryable`,
`model_name`, `prompt_identifier`, `processed_page_count`, `sent_character_count`,
`ai_raw_response`, `ai_reasoning`, `resolved_date`, `date_source`,
`validated_title`, `final_target_file_name`, `ai_provider`

**Constraints (soweit per Metadata prüfbar):**
- `UNIQUE` auf `document_record.fingerprint`
- `UNIQUE` auf `(processing_attempt.fingerprint, processing_attempt.attempt_number)`
- Foreign Key von `processing_attempt.fingerprint` auf `document_record.fingerprint`

**Indizes:**
`idx_processing_attempt_fingerprint`, `idx_processing_attempt_run_id`,
`idx_document_record_overall_status`

Fehlt ein Element: Start mit Fehlermeldung abbrechen.

#### PRAGMA foreign_keys – Connection-sicheres Setzen

SQLite setzt `PRAGMA foreign_keys` pro Connection, nicht global.
Konfiguration via DataSource-Initializer:

```java
SQLiteConfig config = new SQLiteConfig();
config.enforceForeignKeys(true);
SQLiteDataSource ds = new SQLiteDataSource(config);
```

Ein einmaliges Statement vor dem Flyway-Aufruf ist **nicht ausreichend**.

#### Gleichzeitige Starts / Concurrency

Flyway unterstützt bei SQLite keine gleichzeitigen Migrationen.
Parallele Starts werden über einen einfachen Lock-Mechanismus erkannt
(z. B. Lock-Datei neben der SQLite-Datei oder OS-Dateilock).
Bei aktivem Lock bricht der zweite Start mit einer verständlichen Meldung ab
(„Anwendung läuft bereits"). Korruption der DB ist in keinem Fall akzeptabel.

#### Architektur

- Integration in `SqliteSchemaInitializationAdapter.initializeSchema()`
- `PersistenceSchemaInitializationPort`-Vertrag bleibt unverändert
- `evolveTableColumns()` wird vollständig entfernt

---

### #65 – MSI-Installer Review & Optimierung

#### Drei kritische Baustellen (vor Release zwingend)

**1. Icon-Platzhalter ersetzen**
`src/main/packaging/icon.ico` ist ein 1×1-Pixel-Platzhalter.
Ersetzen durch echtes Anwendungsicon (`.ico`, mindestens 256×256 px).

**2. `addModules`-Liste aktualisieren**
Die Java-Modulliste ist veraltet. Neue Bibliotheken (PDFViewFX,
jai-imageio-jpeg2000, PDFBox) müssen erfasst werden:

```
jdeps --print-module-deps --ignore-missing-deps \
      pdf-umbenenner-bootstrap-<version>.jar
```

Das Ergebnis muss **manuell plausibilisiert** werden: Nach dem MSI-Build
wird die installierte Anwendung **ohne Entwicklungs-JDK** gestartet und
folgende Szenarien durchlaufen:

- GUI-Start
- PDF laden und rendern
- Verarbeitungslauf mit PDFBox-Textextraktion
- Historien-Tab öffnen

Nur eine erfolgreiche Laufzeit-Prüfung ohne JDK bestätigt die Modulliste.

**3. `app.version` ersetzen**
Nach #67: `<appVersion>${revision}</appVersion>`

#### Weitere Punkte

- `winUpgradeUuid` mit stabilem GUID einmalig generieren und dauerhaft pflegen
- Desktop-Shortcut verifizieren (`winShortcut=true` + `winShortcutPrompt=false`)
- MSI-Build bleibt bewusst manuell (Linux-CI kann kein MSI bauen)

#### Installationsstruktur nach MSI-Installation

```
<InstallDir>\
    PDF-KI-Renamer.bat          (headless-Start via Task Scheduler)
    PDF-KI-Renamer-GUI.bat      (GUI-Start)
    PDF KI Renamer\
        PDF-KI-Renamer.exe
        application.example.properties
        (alle weiteren App-Dateien)
```

Beide BAT-Dateien sind Bestandteil des MSI-Installers und referenzieren
`%~dp0PDF KI Renamer\PDF-KI-Renamer.exe`. Sie müssen auch bei
**Installationspfaden mit Leerzeichen** korrekt funktionieren.

#### Hinweis zu Prompt- und Konfigurationspfaden unter MSI

In `betrieb.md` ist folgender Hinweis aufzunehmen:
> Für den MSI-Betrieb (Startmenü, Task Scheduler) wird **empfohlen**,
> `prompt.template.file` und `sqlite.path` als **absolute Pfade** zu konfigurieren
> oder auf `%APPDATA%`/`%ProgramData%`-Verzeichnisse zu zeigen.
> Relative Pfade beziehen sich auf das Arbeitsverzeichnis, das je nach Startart
> variiert (siehe Tabelle in #71).

#### Testmatrix

- [ ] Neuinstallation auf sauberer Windows-Umgebung ohne vorinstalliertes Java
- [ ] Installation in Pfad **mit Leerzeichen**
- [ ] Upgrade von installiertem V2.x-MSI auf V3.0-MSI (kein manuelles Deinstallieren)
- [ ] GUI-Start über Startmenü-Eintrag
- [ ] Headless-Start über `PDF-KI-Renamer.bat` im Task Scheduler
- [ ] Desktop-Shortcut vorhanden oder Einschränkung in `betrieb.md` dokumentiert
- [ ] App-Version `3.0.x` im Windows-Installer sichtbar („Programme und Features")
- [ ] Deinstallation sauber – ProgramData-Konfiguration bleibt erhalten
- [ ] SmartScreen-Warnung erwartet und in `betrieb.md` dokumentiert
- [ ] BAT-Dateien funktionieren bei Installationspfad mit Leerzeichen
- [ ] Anwendungsstart ohne Entwicklungs-JDK erfolgreich (Modullisten-Verifikation)

---

## Fachliche Features

### #51 – FAILED_RETRYABLE vs. FAILED_FINAL

Semantik und GUI-Darstellung gemäß verbindlicher Status-Mapping-Tabelle.

Die interne Status-Unterscheidung bleibt vollständig erhalten.
Kein Umbau an Domain oder Persistenz.

#### Detailbereich bei FAILED_FINAL

> „Diese Datei kann nicht verarbeitet werden. Mögliche Ursachen:
> kein lesbarer Text (z. B. gescanntes Foto ohne OCR), Passwortschutz
> oder beschädigte Datei.
> Sie können den Status manuell zurücksetzen, wenn Sie die Ursache
> behoben haben."

„Erneut verarbeiten" und „Status zurücksetzen" bleiben für beide
Fehlerstatus verfügbar.

---

## GUI-Features

### #7 – Historien-Tab

#### Ziel

Neuer Tab „Verlauf" zeigt alle jemals verarbeiteten Dokumente mit Status
und Verarbeitungsdetails aus der SQLite-Datenbank.

#### Layout

```
[ Konfiguration | Verarbeitungslauf | Verlauf | Prompt ]

[ Suchfeld / Filter          ] [ Status-Filter ▾ ] [ Aktualisieren ]

[ Dokumentenliste (~55%)     | Detailbereich (~45%)  ]
[                            |   Dokument-Info        ]
[                            |   Versuche-Tabelle     ]
[                            |   KI-Begründung        ]

[ Statuszeile ]
```

#### Dokumentenliste (linke Tabelle)

| Spalte | Quelle |
|---|---|
| Status-Icon | `document_record.overall_status` |
| Quelldateiname | `document_record.last_known_source_file_name` |
| Zieldateiname | `document_record.last_target_file_name` |
| Quellpfad | `document_record.last_known_source_locator` |
| Letzter Versuch | `document_record.updated_at` |
| Anzahl Versuche | `COUNT(processing_attempt)` |

**Sortierung:** Standard absteigend nach `updated_at`,
Tie-Breaker: `fingerprint ASC` (stabil und reproduzierbar).

**Initiales Limit (kein echtes Paging in V3.0):**
- `LIMIT 501` in der Query – 501 um „mehr vorhanden" sauber zu erkennen
- Bei mehr als 500 Treffern: „Weitere Einträge vorhanden – Filter verwenden"
- Bei leerer DB: „Noch keine Verarbeitungen vorhanden."
- Lange Dateinamen/Pfade: Ellipsis in der Tabelle, Tooltip zeigt vollen Text

**Filter (DB-seitig, kein GUI-seitiger Vollscan):**
- Freitextsuche über Quelldateiname und Zieldateiname
- Case-insensitiv (`COLLATE NOCASE` oder `LOWER()`)
- Sonderzeichen `%` und `_` in der Sucheingabe werden escaped
- Status-ComboBox: alle / einzelner Status

**`READY_FOR_AI`, `PROPOSAL_READY` und `PROCESSING` werden im Summary-Banner
nicht gezählt** – das Banner erscheint nach Laufabschluss und zeigt
ausschließlich abgeschlossene Ergebnisse.

#### Detailbereich (rechte Seite)

**Dokument-Info:** Fingerprint (12 Zeichen), Quelldateiname, Quellpfad,
Status (Icon + Text), Erstellt/Aktualisiert.

**Versuche-Tabelle:**

| Spalte | Quelle |
|---|---|
| # | `processing_attempt.attempt_number` |
| Datum | `processing_attempt.ended_at` |
| Status | `processing_attempt.status` |
| Provider | `processing_attempt.ai_provider` |
| Modell | `processing_attempt.model_name` |
| Vorgeschlagener Name | `processing_attempt.final_target_file_name` |

**KI-Begründung:** `processing_attempt.ai_reasoning` als nicht editierbare TextArea.

#### Aktionen

| Aktion | Beschreibung |
|---|---|
| **Status zurücksetzen** | Setzt definierte Felder zurück (siehe unten). **Versuche bleiben vollständig erhalten.** Bestätigungsdialog. |
| **Eintrag löschen** | Löscht Stammsatz und alle Versuche vollständig (destruktiv, nicht rückgängig). Bestätigungsdialog mit explizitem Hinweis. |

**`DefaultResetDocumentStatusUseCase` – feldgenaue Definition:**

Folgende Felder werden aktualisiert:

| Feld | Neuer Wert |
|---|---|
| `overall_status` | `READY_FOR_AI` |
| `content_error_count` | `0` |
| `transient_error_count` | `0` |
| `last_failure_instant` | `null` |

Folgende Felder werden **nicht** geändert:

| Feld | Begründung |
|---|---|
| `created_at` | Historisches Datum, immer unverändert |
| `last_success_instant` | Historische Information, bleibt erhalten |
| `last_target_path` / `last_target_file_name` | Historische Information, bleibt erhalten |
| `processing_attempt`-Einträge | Verlaufshistorie, bleibt vollständig erhalten |

Nach dem Reset muss das Dokument beim nächsten Lauf tatsächlich als
verarbeitbar gelten. `READY_FOR_AI` ist der einzige Trigger für die
Verarbeitungslogik. Alte Versuche dürfen die Wiederverarbeitung nicht blockieren.

**`DefaultDeleteDocumentHistoryUseCase` – Löschreihenfolge:**

Da der Foreign Key von `processing_attempt.fingerprint` auf
`document_record.fingerprint` **ohne `ON DELETE CASCADE`** definiert ist und
`PRAGMA foreign_keys` aktiv ist, muss innerhalb einer Transaktion
in folgender Reihenfolge gelöscht werden:

1. Alle `processing_attempt`-Einträge zum Fingerprint
2. Den `document_record`-Stammsatz zum Fingerprint

**Sperren während aktivem Verarbeitungslauf:**
Beide Aktionen sind während eines laufenden Verarbeitungslaufs **deaktiviert**.
Hinweis: „Aktion während Verarbeitungslauf nicht möglich."

#### Architektur

Das bestehende Muster des Projekts:

```
GUI-Adapter → Gui...Port (Bridge-Interface in adapter-in-gui)
                   ↑ implementiert von Bootstrap
              Default...UseCase (in application)
                   ↓
              Outbound-Ports → Adapter
```

Neue Komponenten:

| Komponente | Typ | Modul |
|---|---|---|
| `GuiHistoryOverviewPort` | Bridge-Interface | `adapter-in-gui` |
| `GuiHistoryDetailsPort` | Bridge-Interface | `adapter-in-gui` |
| `GuiResetDocumentStatusPort` | Bridge-Interface | `adapter-in-gui` |
| `GuiDeleteDocumentHistoryPort` | Bridge-Interface | `adapter-in-gui` |
| `DefaultHistoryOverviewUseCase` | Use-Case-Impl. | `application` |
| `DefaultHistoryDetailsUseCase` | Use-Case-Impl. | `application` |
| `DefaultResetDocumentStatusUseCase` | Use-Case-Impl. | `application` |
| `DefaultDeleteDocumentHistoryUseCase` | Use-Case-Impl. | `application` |

Der GUI-Adapter kennt ausschließlich `Gui...Port`-Interfaces – niemals direkt
`DocumentRecordRepository` oder `ProcessingAttemptRepository`.

**Threading:** Lesende DB-Zugriffe auf Worker-Thread, UI via `Platform.runLater()`.
Kein Echtzeit-Tailing – manueller „Aktualisieren"-Button.

---

### #50 – Statuszeile

Permanente Statuszeile am unteren Rand der Anwendung (unterhalb aller Tabs):

```
[ V3.0.42 ]  [ Provider: Claude · claude-opus-4-7 (Beispiel) ]  [ config/application.properties ]
```

| Segment | Inhalt | Quelle |
|---|---|---|
| Links | `V{version}` | `MANIFEST.MF` via `getImplementationVersion()`, Fallback: `dev` |
| Mitte | Aktiver Provider und Modell | Geladene Konfiguration |
| Rechts | Konfigurationsdateipfad | Aktueller Ladezustand |

- Permanent sichtbar auf allen Tabs
- Aktualisierung bei Laden/Speichern neuer Konfiguration
- Kein Profil geladen: „Kein Profil geladen"
- Version immer sichtbar, auch ohne geladene Konfiguration
- Voraussetzung: #67 muss umgesetzt sein

---

### #66 – Tooltips

JavaFX `Tooltip` auf allen Buttons, Feldern und Status-Icons.
Farbe ist niemals das einzige Unterscheidungsmerkmal.
Bei der Umsetzung wird eine UI-Checkliste pro Tab geführt.

**Konfigurationstab:**

| Element | Tooltip |
|---|---|
| Quellordner | „Ordner mit den zu verarbeitenden PDF-Dateien. Inhalt wird nicht verändert." |
| Zielordner | „Ordner für die umbenannten Kopien." |
| SQLite-Datei | „Datenbank für Verarbeitungsergebnisse und Datei-Historie." |
| Prompt-Datei | „Externe Textdatei mit den KI-Anweisungen." |
| Provider-ComboBox | „Der KI-Dienst, der die Dateinamen generiert." |
| Modell-Feld | „Das konkrete Sprachmodell des gewählten Providers." |
| max.text.characters | „Maximale Zeichenzahl aus dem PDF-Text. Höhere Werte = mehr Kontext, höhere Kosten." |
| max.pages | „Maximale Seitenzahl, die aus einem PDF gelesen wird." |
| max.title.length | „Maximale Länge des Dateinamens in Zeichen (ohne Datum und Erweiterung). Gültig: 10–120." |

**Tab „Verarbeitungslauf":** Alle Status-Icons gemäß Status-Mapping-Tabelle.

| Element | Tooltip |
|---|---|
| „Dateiname übernehmen" | „Benennt die Zieldatei um und aktualisiert die Datenbank. Nicht rückgängig zu machen." |
| „Zurücksetzen auf KI-Vorschlag" | „Stellt den KI-generierten Namen wieder her, ohne zu speichern." |

**Toolbar:**

| Button | Tooltip |
|---|---|
| Neu | „Neue Konfiguration erstellen." |
| Öffnen | „Bestehende Konfigurationsdatei (.properties) öffnen." |
| Speichern | „Aktuelle Konfiguration speichern." |
| Speichern unter | „Konfiguration unter neuem Dateipfad speichern." |
| Validieren | „Aktuelle Eingaben auf Vollständigkeit und Korrektheit prüfen." |
| Technische Tests | „Dateipfade, Datenbankverbindung und KI-Erreichbarkeit prüfen." |

Technisch: `element.setTooltip(new Tooltip("Text"))` –
rein im Modul `pdf-umbenenner-adapter-in-gui`.

---

### #71 – Prompt-Editor in der GUI

#### Ziel

Eigener Tab „Prompt" zum Lesen, Bearbeiten und Speichern der
KI-Prompt-Datei – kein externer Editor notwendig.

#### Bestehende Infrastruktur

| Komponente | Stand |
|---|---|
| `PromptPort.loadPrompt()` | ✅ existiert |
| `ResourceCreationPort.createPromptFile()` | ✅ existiert (nur Default-Template) |
| Port zum Speichern beliebigen Inhalts | ❌ fehlt – neu erstellen |

#### Neuer Port

`PromptPort` wird um eine Schreibmethode erweitert:

```java
PromptSaveResult savePrompt(String content);
```

`savePrompt(String content)` speichert immer in die aktuell über die
geladene Konfiguration aufgelöste Prompt-Datei. Der Pfad wird im Use-Case
aus der Konfiguration bestimmt und dem Adapter intern bereitgestellt –
er ist **nicht** Teil der Port-Signatur.

Implementierung: `FilesystemPromptPortAdapter` im Modul `pdf-umbenenner-adapter-out`.

#### Architektur

| Komponente | Typ | Modul |
|---|---|---|
| `GuiPromptEditorPort` | Bridge-Interface | `adapter-in-gui` |
| `DefaultPromptEditorUseCase` | Use-Case-Impl. | `application` |

Der GUI-Adapter greift **nicht** direkt auf `PromptPort` oder das Dateisystem zu.

#### Verhalten

- **Tab-Öffnen:** Inhalt der konfigurierten Prompt-Datei laden (Worker-Thread)
- **Keine Datei konfiguriert/vorhanden:** Hinweistext +
  „Standard-Prompt erstellen"-Button (ruft `createPromptFile()` auf)
- **Bearbeitung** in `TextArea`
- **Dirty State:** Tab-Titel mit Asterisk (`Prompt *`)
- **„Speichern":** schreibt atomar (temp-Datei **im selben Verzeichnis** wie
  Zieldatei → `ATOMIC_MOVE`), Encoding: **UTF-8**,
  Zeilenenden: unverändert übernommen (keine Normalisierung)
- **„Auf Standard zurücksetzen":** befüllt TextArea mit Default, **ohne** zu speichern
- **Tab-Wechsel/Schließen mit Dirty State:** Bestätigungsdialog „Änderungen verwerfen?"

#### Fehlerfälle

| Fehlerfall | Verhalten |
|---|---|
| Konfigurierter Ordner nicht vorhanden | Fehlermeldung im Tab, kein Absturz |
| Datei schreibgeschützt | Fehlermeldung nach Speicherversuch |
| Zielordner während Bearbeitung gelöscht | Fehlermeldung beim Speichern |
| `ATOMIC_MOVE` schlägt fehl | Speichervorgang abbrechen, Fehlermeldung – kein stiller Fallback auf Teilüberschreibung |
| Leerer Prompt beim Speichern | Bestätigungsdialog: „Wirklich leere Prompt-Datei speichern?" |
| Extern geänderte Datei | Last-write-wins (kein automatisches Reload) |

#### Pfadauflösung (Verhalten beibehalten, dokumentieren)

Der Prompt-Pfad wird relativ zum **Arbeitsverzeichnis** aufgelöst – wie bisher.
`betrieb.md` muss folgende Tabelle enthalten:

| Startart | Arbeitsverzeichnis |
|---|---|
| IDE | Projektwurzel |
| Fat-JAR per Konsole | Aktuelles Terminal-Verzeichnis |
| MSI / Startmenü | Installationsverzeichnis |
| Task Scheduler headless | Explizit gesetztes „Start in"-Verzeichnis empfohlen |

**Empfehlung für MSI-Betrieb** (in `betrieb.md`): Für MSI-Betrieb über
Startmenü oder Task Scheduler werden **absolute Pfade** für
`prompt.template.file` und `sqlite.path` empfohlen, idealerweise auf
`%APPDATA%`- oder `%ProgramData%`-Verzeichnisse.

Langfristig (V3.x): Pfadauflösung relativ zur geladenen `.properties`-Datei.

#### Threading

Laden und Speichern auf Worker-Thread, UI via `Platform.runLater()`.

---

### #73 – Lauf-Statistik / Summary-Banner

Banner erscheint nach Laufabschluss, unterhalb des Fortschrittsbalkens,
oberhalb der Ergebnistabelle:

```
✓ 14 erfolgreich  ·  ↻ 1 wird wiederholt  ·  × 2 fehlgeschlagen  ·  ≡ 3 übersprungen  ·  ⊘ 1 endgültig übersprungen
```

- Nur Kategorien mit Anzahl > 0 (inkl. `⊘ endgültig übersprungen` wenn > 0)
- `READY_FOR_AI`, `PROPOSAL_READY` und `PROCESSING` werden **nicht gezählt**
- Bei vollständig sauberem Lauf: `✓ 17 erfolgreich`
- Verschwindet beim Start des nächsten Laufs
- Kein Banner bei Anwendungsstart oder Tab-Wechsel ohne Lauf
- Icon + Text tragen die Information, nicht nur Farbe
- Reine GUI-Logik, kein neuer Port; Aggregation aus bestehender Ergebnisliste

---

## Architektur-Zusammenfassung

### Neue Ports / Use-Cases

| Komponente | Modul | Zweck |
|---|---|---|
| `PromptPort.savePrompt(String)` | `application` | Prompt-Inhalt speichern (#71) |
| `DefaultPromptEditorUseCase` | `application` | Prompt-Editor-Logik (#71) |
| `DefaultHistoryOverviewUseCase` | `application` | Dokumentenliste Historien-Tab (#7) |
| `DefaultHistoryDetailsUseCase` | `application` | Detailansicht Historien-Tab (#7) |
| `DefaultResetDocumentStatusUseCase` | `application` | Feldgenauer Status-Reset, Versuche bleiben (#7) |
| `DefaultDeleteDocumentHistoryUseCase` | `application` | Transaktionales Löschen in korrekter Reihenfolge (#7) |

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

| Interface | Zweck |
|---|---|
| `GuiPromptEditorPort` | Brücke zum Prompt-Editor-Use-Case (#71) |
| `GuiHistoryOverviewPort` | Brücke zur Dokumentenliste (#7) |
| `GuiHistoryDetailsPort` | Brücke zur Detailansicht (#7) |
| `GuiResetDocumentStatusPort` | Brücke zum Status-Reset (#7) |
| `GuiDeleteDocumentHistoryPort` | Brücke zum vollständigen Löschen (#7) |

### Geänderte Adapter

| Adapter | Modul | Änderung |
|---|---|---|
| `FilesystemPromptPortAdapter` | `adapter-out` | Neue Methode `savePrompt()` (#71) |
| `SqliteSchemaInitializationAdapter` | `adapter-out` | Ersetzt durch Flyway-Integration (#49) |

### Nicht geändert

- `pdf-umbenenner-domain` – keine Änderungen
- `pdf-umbenenner-adapter-in-cli` – keine Änderungen
- `pdf-umbenenner-bootstrap` – nur MANIFEST.MF-Ergänzung (#67) + neue Bridge-Verdrahtung
- Headless-Betrieb – vollständig unberührt

---

## Definition of Done (V3.0 gesamt)

- [ ] Alle 10 Issues implementiert und einzeln getestet
- [ ] `mvn clean verify` grün (alle Module, kein `-DskipTests`)
- [ ] `mvn clean install -Drevision=3.0.1` – kein unaufgelöstes `${revision}` in installierten POMs
- [ ] `java -jar pdf-ki-renamer-3.0.1.jar` zeigt in Statuszeile exakt `V3.0.1`
- [ ] Bestehende V2.9-DB via Flyway-Baseline korrekt übernommen – kein Datenverlust
- [ ] Historien-Tab zeigt korrekte Daten aus einer V2.9-DB
- [ ] Versionsnummer konsistent: MANIFEST.MF, Statuszeile, MSI und JAR-Dateiname
- [ ] Jenkins-Build läuft mit `-Drevision`-Übergabe durch; Bash-Skript schlägt bei 0 oder >1 JAR ab
- [ ] MSI-Testmatrix vollständig abgearbeitet inkl. Anwendungsstart ohne JDK
- [ ] Tooltips vollständig auf allen spezifizierten Elementen (UI-Checkliste pro Tab)
- [ ] Prompt-Editor: alle Szenarien inkl. Fehlerfälle getestet
- [ ] `FAILED_RETRYABLE` und `FAILED_FINAL` eindeutig durch Icon, Farbe und Text unterschieden
- [ ] „Status zurücksetzen" setzt feldgenau zurück, löscht keine Versuche, Datei wird wieder verarbeitet
- [ ] „Eintrag löschen" löscht in korrekter Reihenfolge (Attempts vor Record)
- [ ] Destruktive Aktionen während Lauf gesperrt
- [ ] Notwendige Code-Kommentare auf Deutsch; Logging auf Deutsch
- [ ] JavaDoc auf allen neuen öffentlichen Ports, Use-Cases, DTOs und Adapter-Methoden
- [ ] `betrieb.md` enthält Prompt-Pfadauflösungs-Tabelle je Startart + MSI-Absolute-Pfad-Empfehlung
- [ ] `betrieb.md` und `gui-bedienanleitung.md` auf V3.0-Stand gebracht
- [ ] Freigabedokument `freigabe-v3_0.md` erstellt
- [ ] Manueller GUI-Produkttest durchgeführt (Green build ≠ fertige Software)

---

## Abnahmekriterien je Feature

### #49 Flyway
- [ ] Neue SQLite-DB vollständig per Flyway aus V1 aufgebaut
- [ ] „Leer" korrekt erkannt: keine Tabellen vorhanden (nicht nur Dateigröße 0 Byte)
- [ ] Bestehende V2.9-DB: vollständige V1-Schema-Prüfcheckliste durchlaufen
- [ ] `baselineOnMigrate=true` ausschließlich in Fall 2 gesetzt
- [ ] Start mit nicht konformem Schema bricht mit Fehlermeldung ab
- [ ] PRAGMA foreign_keys via DataSource-Initializer (nicht einmaliges Statement)
- [ ] `evolveTableColumns()` vollständig entfernt
- [ ] Lock-Mechanismus verhindert korrupte DB bei parallelem Start

### #65 MSI
- [ ] Echtes Icon (kein 1×1-Platzhalter)
- [ ] `addModules`-Liste per `jdeps` aktualisiert und per Laufzeit-Test ohne JDK verifiziert
- [ ] Version entspricht V3.0-Stand (`${revision}`)
- [ ] `winUpgradeUuid` gesetzt
- [ ] Installationsstruktur korrekt: BAT-Dateien neben App-Verzeichnis
- [ ] BAT-Dateien funktionieren bei Pfad mit Leerzeichen
- [ ] Vollständige Testmatrix abgearbeitet
- [ ] `betrieb.md` mit MSI-Hinweisen zu ProgramData, absoluten Pfaden und SmartScreen

### #67 Versionierung
- [ ] `${revision}` im Parent-POM eingeführt
- [ ] `flatten-maven-plugin` in `<build><plugins>` des Parent-POM (nicht nur pluginManagement)
- [ ] `flatten-maven-plugin` mit `resolveCiFriendliesOnly` und `flatten:clean`
- [ ] Interne Modul-Abhängigkeiten verwenden `${project.version}`
- [ ] MANIFEST.MF enthält `Implementation-Version` korrekt im Fat-JAR
- [ ] Fallback `"dev"` bei null implementiert
- [ ] `app.version` im Packaging-Modul nutzt `${revision}`

### #68 Jenkins
- [ ] Maven-Build übergibt `-Drevision=MAJOR.MINOR.BUILD_NUMBER`
- [ ] Bash explizit erzwungen (`#!/usr/bin/env bash`, `set -euo pipefail`)
- [ ] `mapfile`-Prüfung: Build bricht ab bei 0 oder >1 JAR
- [ ] MSI-Build als manuell dokumentiert

### #51 FAILED_RETRYABLE vs. FAILED_FINAL
- [ ] Icons (↻ vs. ×), Farben (Orange vs. Rot) und Tooltips eindeutig unterschiedlich
- [ ] Detailbereich zeigt erweiterte Erklärung bei `FAILED_FINAL`
- [ ] Farbe ist nicht das einzige Unterscheidungsmerkmal

### #7 Historien-Tab
- [ ] Tab „Verlauf" sichtbar und navigierbar
- [ ] `LIMIT 501`-Technik, Hinweis bei mehr als 500 Treffern
- [ ] Hinweistext bei leerer DB
- [ ] Sortierung mit stabilem Tie-Breaker
- [ ] Suche case-insensitiv, Sonderzeichen escaped
- [ ] Detailbereich zeigt Versuche und KI-Begründung
- [ ] „Status zurücksetzen" setzt feldgenau zurück (Status, Zähler, last_failure_instant)
- [ ] „Status zurücksetzen" löscht keine Versuche; Datei wird beim nächsten Lauf verarbeitet
- [ ] „Eintrag löschen": Attempts vor Record, innerhalb Transaktion, mit Bestätigungsdialog
- [ ] Beide Aktionen während Lauf deaktiviert

### #50 Statuszeile
- [ ] Version aus MANIFEST.MF, Fallback `dev`
- [ ] Provider und Modell aus geladener Konfiguration
- [ ] Konfigurationspfad korrekt
- [ ] Aktualisierung bei Laden/Speichern neuer Konfiguration
- [ ] Kein Absturz wenn keine Konfiguration geladen

### #66 Tooltips
- [ ] UI-Checkliste pro Tab geführt und abgearbeitet
- [ ] Alle spezifizierten Elemente haben Tooltips
- [ ] Farbe ist nicht einziges Unterscheidungsmerkmal

### #71 Prompt-Editor
- [ ] Laden beim Tab-Öffnen (Worker-Thread)
- [ ] Dirty State korrekt angezeigt
- [ ] Speichern atomar (temp im selben Verzeichnis + `ATOMIC_MOVE`), Encoding UTF-8
- [ ] Kein stiller Fallback bei fehlgeschlagenem `ATOMIC_MOVE`
- [ ] „Auf Standard zurücksetzen" ohne automatisches Speichern
- [ ] Bestätigungsdialog bei Tab-Wechsel mit Dirty State
- [ ] Alle Fehlerfälle laut Spezifikation getestet
- [ ] `betrieb.md` enthält Pfadauflösungs-Tabelle + MSI-Empfehlung für absolute Pfade

### #73 Summary-Banner
- [ ] Korrekte Zählwerte nach Lauf inkl. `⊘ endgültig übersprungen`
- [ ] `READY_FOR_AI`, `PROPOSAL_READY`, `PROCESSING` werden nicht gezählt
- [ ] Nur Kategorien > 0 angezeigt
- [ ] Verschwindet beim nächsten Laufstart
- [ ] Icon + Text tragen Information, nicht nur Farbe
