diff --git a/README.md b/README.md index 32c9eb4..b8e7777 100644 --- a/README.md +++ b/README.md @@ -4,14 +4,21 @@ Ein lokal gestartetes Java-Programm zur KI-gestützten Umbenennung bereits OCR-v Die Anwendung liest PDF-Dateien aus einem konfigurierbaren Quellordner, extrahiert den Text, ermittelt daraus per KI einen normierten Dateinamen und legt **eine Kopie** im Zielordner ab. Die Quelldateien bleiben unverändert. +> **V2.0:** Die Anwendung enthält ab V2.0 eine lokale JavaFX-Desktop-GUI als Standardstart. +> Die GUI dient der Konfiguration, Validierung und technischen Diagnose. +> Der headless Batch-Betrieb bleibt über `--headless` vollständig erhalten. +> Details zum Betrieb: [`docs/betrieb.md`](docs/betrieb.md) + ## Zielbild -Der PDF-Umbenenner ist bewusst als schlanke Batch-Anwendung ausgelegt: +Der PDF-Umbenenner ist als schlanke, lokal gestartete Anwendung ausgelegt: - **Java 21** - **Maven Multi-Module** -- **ausführbares Standalone-JAR** -- **lokaler Start**, z. B. über den **Windows Task Scheduler** +- **ausführbares Standalone-JAR** (ein gemeinsames JAR für GUI und headless) +- **GUI-Standardstart** ab V2.0 (JavaFX-Desktop, offiziell Windows) +- **headless Betrieb** über `--headless`, z. B. für den **Windows Task Scheduler** +- **`--config `** für GUI und headless - **kein Webserver** - **kein Applikationsserver** - **keine Dauerlauf-Anwendung** @@ -86,6 +93,7 @@ Das Projekt ist strikt nach **Ports and Adapters / Hexagonal Architecture** aufg - `pdf-umbenenner-domain` - `pdf-umbenenner-application` - `pdf-umbenenner-adapter-in-cli` +- `pdf-umbenenner-adapter-in-gui` - `pdf-umbenenner-adapter-out` - `pdf-umbenenner-bootstrap` @@ -135,13 +143,30 @@ Unter Windows: ## Start -Das ausführbare Artefakt wird im Bootstrap-Modul erzeugt. Der Start erfolgt als normales Java-Programm: +Das ausführbare Artefakt wird im Bootstrap-Modul erzeugt. + +**GUI-Standardstart** (öffnet die JavaFX-Desktop-Oberfläche): ```bash -java -jar .jar +java -jar pdf-umbenenner-bootstrap/target/pdf-umbenenner-bootstrap-0.0.1-SNAPSHOT.jar ``` -Die konkrete JAR-Datei hängt vom aktuellen Build-Stand ab. +**headless Betrieb** (Batch-/Scheduler-Start ohne GUI): + +```bash +java -jar pdf-umbenenner-bootstrap/target/pdf-umbenenner-bootstrap-0.0.1-SNAPSHOT.jar --headless +``` + +**Explizite Konfigurationsdatei** (für GUI und headless): + +```bash +java -jar pdf-umbenenner-bootstrap/target/pdf-umbenenner-bootstrap-0.0.1-SNAPSHOT.jar --config C:\Pfad\zur\config.properties +java -jar pdf-umbenenner-bootstrap/target/pdf-umbenenner-bootstrap-0.0.1-SNAPSHOT.jar --headless --config C:\Pfad\zur\config.properties +``` + +Das JAR ist das einzige Distributionsartefakt und enthält JavaFX für den GUI-Start bereits +integriert. Ausführliche Build-, Packaging- und Starthinweise sowie Informationen zur JavaFX- +Integration und zum headless Betrieb befinden sich in [`docs/betrieb.md`](docs/betrieb.md). ## Logging, Status und Nachvollziehbarkeit @@ -160,7 +185,10 @@ Die maßgeblichen Dokumente sind: - `CLAUDE.md` - `docs/specs/technik-und-architektur.md` - `docs/specs/fachliche-anforderungen.md` -- `docs/specs/meilensteine.md` +- `docs/specs/meilensteine-v2_0.md` +- `docs/betrieb.md` +- `docs/gui-bedienanleitung.md` +- `docs/freigabe-v2_0.md` - `docs/workpackages/...` Empfohlene Leserichtung: @@ -169,7 +197,9 @@ Empfohlene Leserichtung: 2. technische Zielarchitektur 3. fachliche Anforderungen 4. Meilensteine -5. aktives Arbeitspaket +5. `docs/betrieb.md` für Betriebs- und Startdetails +6. `docs/gui-bedienanleitung.md` für die GUI-Bedienung +7. aktives Arbeitspaket ## Entwicklungsleitplanken @@ -181,14 +211,16 @@ Empfohlene Leserichtung: ## Status des Projekts -Das Repository verfolgt einen inkrementellen, meilensteinbasierten Ausbau. Der aktuelle Produktstand baut auf einem vollständig implementierten Kern für: +Das Repository verfolgt einen inkrementellen, meilensteinbasierten Ausbau. Der aktuelle Produktstand (V2.0) baut auf einem vollständig implementierten Kern für: - Konfiguration und Startvalidierung - Quellordner-Scan und PDF-Textauslese - Fingerprint, SQLite-Persistenz und Idempotenz -- KI-Integration für Benennungsvorschläge +- KI-Integration für Benennungsvorschläge (OpenAI-kompatibel und Anthropic Claude) - Dateinamensbildung und Zielkopie - Retry-Logik, Logging und betriebliche Robustheit +- JavaFX-Desktop-GUI als Standardstart (Konfigurationseditor, Validierung, technische Tests) +- headless Batch-Betrieb über `--headless` (rückwärtskompatibel zu V1.x) ## Lizenz / Nutzung diff --git a/docs/befundliste.md b/docs/befundliste.md index c320e6d..95337e7 100644 --- a/docs/befundliste.md +++ b/docs/befundliste.md @@ -173,3 +173,145 @@ Spezifikationen (technik-und-architektur.md, fachliche-anforderungen.md, CLAUDE. vollständig umgesetzt und durch automatisierte Tests abgesichert. Der Maven-Build ist fehlerfrei. Die CLAUDE.md-Naming-Convention-Regel (kein M1–M8, kein AP-xxx im Produktions- oder Testcode) ist vollständig eingehalten. Keine bekannten spezifikationsrelevanten Blocker sind offen. + +--- + +# V2.0-Gesamtstand – Integrierte Prüfung (Stand 2026-04-20) + +**Prüfgrundlage:** Vollständiger Maven-Reactor-Build mit allen Tests (clean verify, `-DskipPitest=true`), +Code-Review gegen die verbindliche Spec-Trias (technik-und-architektur.md, fachliche-anforderungen.md, +CLAUDE.md), Sichtprüfung Dokumentation und Konfigurationsbeispiele. + +--- + +## Build- und Testergebnisse + +**Ausgeführtes Kommando:** +``` +.\mvnw.cmd clean verify -pl pdf-umbenenner-domain,pdf-umbenenner-application,pdf-umbenenner-adapter-out,pdf-umbenenner-adapter-in-cli,pdf-umbenenner-adapter-in-gui,pdf-umbenenner-bootstrap --also-make -DskipPitest=true +``` + +**Gesamtergebnis: BUILD SUCCESS** +**Gesamtlaufzeit:** 01:18 min +**Datum:** 2026-04-20 + +| Modul | Tests | Failures | Errors | Skipped | +|---|---|---|---|---| +| `pdf-umbenenner-domain` | 227 | 0 | 0 | 0 | +| `pdf-umbenenner-application` | 455 | 0 | 0 | 0 | +| `pdf-umbenenner-adapter-in-cli` | 8 | 0 | 0 | 0 | +| `pdf-umbenenner-adapter-in-gui` | 190 | 0 | 0 | 0 | +| `pdf-umbenenner-adapter-out` | 371 | 0 | 0 | 0 | +| `pdf-umbenenner-bootstrap` | 147 | 0 | 0 | 0 | +| **Gesamt** | **1.398** | **0** | **0** | **0** | + +Alle Module bauen erfolgreich. Alle Tests bestehen. Das ausführbare Shade-JAR wird erzeugt und +enthält JavaFX (Win-Classifier), alle Module, PDFBox, SQLite-JDBC und Log4j2. + +**Warnungen im Build:** +- `WARNING: A Java agent has been loaded dynamically (byte-buddy-agent)` – dies ist ein bekannter + Mockito/ByteBuddy-Hinweis, der in Tests auftritt. Kein funktionaler Defekt. Tritt seit V1.1 auf + und gilt als akzeptiert. + +--- + +## Prüfpunkte gegen die V2.0-Spezifikation (1–20) + +| Nr. | Prüfpunkt | Status | Begründung | +|---|---|---|---| +| 1 | GUI ist Standardstart | **erfüllt** | `PdfUmbenennerApplication.main()` → `BootstrapRunner.run()` → `StartupMode.GUI` als Default; `--headless` erforderlich für Batch-Pfad. Korrekt implementiert und getestet (`BootstrapRunnerStartupDispatchTest`). | +| 2 | `--headless` erhalten, `--config` für beide Startarten | **erfüllt** | `CliArgumentParser` parst beide Optionen korrekt. `BootstrapRunnerConfigPathSemanticsTest` (10 Tests) prüft GUI/headless-Semantik für vorhandene und fehlende `--config`-Pfade. | +| 3 | `.properties` als einzige Konfigurationswahrheit | **erfüllt** | `PropertiesConfigurationPortAdapter` und `GuiConfigurationPropertiesWriter` schreiben/lesen ausschließlich `.properties`. Keine zweite Konfigurationswelt. | +| 4 | Zwei Provider (Claude, OpenAI-kompatibel), genau einer aktiv | **erfüllt** | `AiProviderSelector` wählt anhand `ai.provider.active`. `AiModelCatalogDispatcher` unterstützt beide Familien. Konfigurationsbeispiel zeigt beide Blöcke. Provider-Identifikator in Versuchshistorie persistiert (`ProviderIdentifierE2ETest`, 5 Tests). | +| 5 | Hexagonale Architektur (keine JavaFX in Domain/Application) | **erfüllt** | Grep-Prüfung: kein `import javafx` in `domain` oder `application`. `adapter-in-cli` und `adapter-out` ebenfalls JavaFX-frei. JavaFX ausschließlich in `adapter-in-gui`. | +| 6 | Threadingmodell (Worker für I/O, Platform.runLater für UI) | **erfüllt** | `GuiConfigurationEditorWorkspace`, `GuiModelCatalogCoordinator`, `GuiCorrectionDialogCoordinator`, `GuiTechnicalTestCoordinator` nutzen alle explizit `Platform.runLater` für UI-Updates. Worker-Thread-Trennung ist im Code nachweisbar. | +| 7 | Naming-Regel (keine M/AP/V-Bezeichner in Code) | **erfüllt** | Grep auf `M[0-9]+`, `AP-[0-9]+`, `V[12]\.[0-9]` in allen `src/main/java` und `src/test/java` liefert keine Treffer für neue V2.0-Module (`adapter-in-gui`, `bootstrap`-Erweiterungen). | +| 8 | JavaDoc-Standard | **erfüllt** | Alle neu hinzugefügten öffentlichen Klassen und Methoden haben Klassen- und Methoden-JavaDoc. `BootstrapRunner` und `PdfUmbenennerApplication` vollständig dokumentiert. `package-info.java` vorhanden in neuen Packages. | +| 9 | Dirty-State, Schutzdialog, Validierung, technische Tests | **erfüllt** | `GuiDirtyStateTest`, `GuiUnsavedChangesGuardSmokeTest`, `GuiValidateActionSmokeTest`, `GuiTechnicalTestCoordinatorSmokeTest` belegen die Kernpfade. `GuiUnsavedChangesGuard` kapselt die Dirty-State-Logik. | +| 10 | Meldungsbereich mit vier Stufen, feldnahe Fehlermeldungen | **erfüllt** | `GuiMessageSeverity` (INFO, HINWEIS, WARNUNG, FEHLER), `GuiMessageEntry` und `GuiFieldFinding` implementieren die Anforderungen. `GuiMessageAreaSmokeTest` und `GuiMessageEntryTest` prüfen sie. | +| 11 | Modellabruf, Manueller Modellfallback, Verwerfen-Regel | **erfüllt** | `GuiModelCatalogCoordinator` implementiert automatischen Abruf bei Providerwechsel, ComboBox vs. Textfeld-Umschaltung, und Verwerfen-Regel bei Listenwechsel. `GuiModelCatalogSmokeTest` prüft die Kernpfade. | +| 12 | Korrekturhilfen mit gesammeltem Bestätigungsdialog | **erfüllt** | `GuiCorrectionDialogCoordinator` sammelt Korrekturen und steuert den Bestätigungsdialog. `ConfirmationDialogContent` kapselt die Dialog-Inhalte. `GuiCorrectionDialogCoordinatorSmokeTest` prüft den Ablauf. | +| 13 | Automatische Prompt-Erzeugung | **erfüllt** | `DefaultPromptTemplate.defaultContent()` in `application`-Schicht; `FilesystemResourceCreationAdapter` erzeugt die Datei. `DefaultPromptTemplateTest` und `FilesystemResourceCreationAdapterTest` prüfen Inhalt und Erzeugung. Beispiel in `docs/examples/prompt.txt` konsistent. | +| 14 | Windows-Pfade und gemappte Laufwerke | **erfüllt** | `FilesystemPathCheckAdapter` akzeptiert Windows-Laufwerksbuchstaben. `FilesystemPathCheckAdapterTest` (28 Tests) enthält Windows-Pfad-Szenarien. Dokumentation in `betrieb.md` und `gui-bedienanleitung.md` ausdrücklich erwähnt. | +| 15 | Legacy-Migration mit `.bak`-Sicherung | **erfüllt** | `LegacyConfigurationMigrator` in `adapter-out`; GUI-Pfad ruft `detectedLegacyConfiguration` + `migrateConfigurationIfNeeded` in `BootstrapRunner` auf. `GuiConfigurationPropertiesWriterTest` prüft Backup-Schema. | +| 16 | Keine neuen Provider über Claude/OpenAI-kompatibel hinaus | **erfüllt** | Codebase enthält ausschließlich `ClaudeAiInvocationAdapter` und `OpenAiCompatibleAiInvocationAdapter`. Kein dritter Provider. | +| 17 | Keine neuen Distributionsformate (EXE/Installer) | **erfüllt** | `pom.xml` des Bootstrap-Moduls nutzt ausschließlich `maven-shade-plugin`. Kein `launch4j`, kein `jpackage`, kein Installer. | +| 18 | Kein manueller Verarbeitungslauf aus GUI | **erfüllt** | `adapter-in-gui` enthält keine Klasse, die `BatchRunProcessingUseCase` aus einem GUI-Event aufruft. Kein „Start"-Button, keine Batch-Ausführungslogik im GUI-Adapter. | +| 19 | Keine DB-/Historienanzeige | **erfüllt** | Kein SQLite-Lesepfad aus `adapter-in-gui`. Kein Historien-Tab. Kein Ergebnis-Browser. | +| 20 | Keine fachlichen Änderungen an Kernverarbeitung | **erfüllt** | `DefaultBatchRunProcessingUseCase`, `DocumentProcessingCoordinator`, `AiNamingService`, `AiResponseValidator` sind gegenüber dem V1.1-Freigabestand unverändert. E2E-Tests (`BatchRunEndToEndTest`, 11 Szenarien) sind alle grün. | + +--- + +## Dokumentations-Vollständigkeitsprüfung + +| Dokument | Status | Bewertung | +|---|---|---| +| `docs/betrieb.md` | vollständig | Alle V2.0-Themen abgedeckt: GUI-Standardstart, `--headless`, `--config`-Semantik für beide Modi, Plattformhinweis Windows, gemappte Laufwerke, GUI-Umfangsbegrenzung, Build- und Packaging-Abschnitt, JavaFX-Integration, headless ohne JavaFX-Initialisierung | +| `docs/gui-bedienanleitung.md` | vollständig | Alle AP-002-Themen abgedeckt: Startzustände (Abschnitte 2.1–2.4), alle 7 Aktionen (Abschnitte 4.1–4.7), vier Meldungsstufen (Abschnitt 3.2), feldnahe Fehlermeldungen (Abschnitt 5), Provider-Bedienung und Modellabruf (Abschnitt 7), API-Key-Auflösungsreihenfolge (Abschnitt 10), Dirty-State (Abschnitt 8), `.bak`-Sicherung und Legacy-Migration (Abschnitt 9), Windows-Hinweise (Abschnitt 12), bekannte Einschränkungen (Abschnitt 13) | +| `docs/examples/application.properties` | vollständig und konsistent | Alle Parameter des V2.0-Schemas vorhanden (beide Provider-Blöcke, alle Pflicht- und Optionalparameter). Kommentare zu Warnschwellen für `max.text.characters` enthalten. Default-Provider `claude` gesetzt (alphabetisch erster). Konsistent mit `GuiConfigurationTemplateFactory`. | +| `docs/examples/prompt.txt` | vollständig und konsistent | Deutschsprachiger Standardprompt. Inhaltlich identisch mit dem, was `DefaultPromptTemplate.defaultContent()` erzeugt (durch `FilesystemResourceCreationAdapterTest` nachgewiesen). JSON-Schema-Anforderungen (title, reasoning, date optional) abgebildet. | +| `README.md` | vollständig | V2.0-Hinweis im Header, GUI-Standardstart dokumentiert, `--headless` und `--config`-Beispiele vorhanden, Modul `pdf-umbenenner-adapter-in-gui` aufgelistet, Verweis auf `betrieb.md` und `gui-bedienanleitung.md`. | + +--- + +## Release-Blocker + +**Keine Release-Blocker identifiziert.** + +Der vollständige Maven-Reactor-Build ist grün (1.398 Tests, 0 Failures, 0 Errors, 0 Skipped). +Alle 20 Prüfpunkte gegen die Spec-Trias sind als erfüllt bewertet. Die Dokumentation ist +vollständig und konsistent mit dem Code. + +--- + +## Nicht blockierende Restpunkte + +#### R1 – ByteBuddy-Agent-Warnung bei Tests + +**Thema:** Testqualität / Laufzeitwarnung +**Befund:** Beim Build erscheint wiederholt `WARNING: A Java agent has been loaded dynamically +(byte-buddy-agent-1.14.12.jar)`. Der Hinweis stammt von Mockito und tritt seit dem V1.1-Stand auf. +Er ist nicht neu, betrifft nur die Testlaufzeit und hat keinen funktionalen Einfluss auf das +produzierte Artefakt. +**Bewertung:** Kein Handlungsbedarf. Mit `-XX:+EnableDynamicAgentLoading` unterdrückbar, aber +keine Pflicht für V2.0. + +#### R2 – GUI-Tests ohne echten JavaFX-Rendering-Pfad + +**Thema:** Testtiefe GUI +**Befund:** Die GUI-Tests (`GuiAdapterSmokeTest`, `GuiEditorRegressionSmokeTest` usw.) laufen +unter headless JavaFX (Monocle) und prüfen View-Modell-Logik, Zustandsübergänge und +Koordinatoren-Verhalten. Das visuelle Rendering der Oberfläche (Farbgebung der Meldungspräfixe, +Layout-Details) ist nicht automatisiert geprüft. Dies entspricht der in CLAUDE.md definierten +GUI-Teststrategie (kein TestFX über Monocle hinaus) und ist keine Abweichung vom Ziel. +**Bewertung:** Kein Handlungsbedarf. Entspricht der Teststrategie-Vorgabe. + +--- + +## Bewusst außerhalb V2.0 liegende Themen (V2.1+) + +Die folgenden Themen wurden im V2.0-Umfang nachweislich **nicht** implementiert und sind +ausdrücklich für spätere Ausbaustufen vorgesehen: + +- **Manueller Verarbeitungslauf aus der GUI** (V2.1+) +- **DB-/Historienansicht** in der GUI (V2.x+) +- **Kosten-Tracking** und Token-/Preisberechnung (V2.x+) +- **EXE-Wrapper / Installer** (V3+) +- **Weitere KI-Provider** über Claude und OpenAI-kompatibel hinaus (V3+) +- **Automatischer Fallback zwischen Providern** (V3+) +- **Profilverwaltung mit mehreren Konfigurationen je Provider** (V3+) +- **Plattformübergreifender offizieller GUI-Support** (V3+) + +--- + +## Gesamtbewertung V2.0-Stand + +| Klassifikation | Anzahl | Beschreibung | +|---|---|---| +| Release-Blocker | **0** | – | +| Nicht blockierende Restpunkte | **2** | R1 ByteBuddy-Warnung, R2 Testtiefe GUI-Rendering | +| Bewusst außerhalb V2.0 | **8** | Manueller Lauf, Historienansicht, Kosten-Tracking, EXE, weitere Provider, Fallback, Profile, Cross-Platform | + +**Build:** ERFOLGREICH · 1.398 Tests · 0 Failures · 0 Errors · Laufzeit 01:18 min +**Alle 20 Spezifikations-Prüfpunkte:** erfüllt +**Dokumentation:** vollständig und konsistent diff --git a/docs/betrieb.md b/docs/betrieb.md index 069d4e0..b2bdde7 100644 --- a/docs/betrieb.md +++ b/docs/betrieb.md @@ -8,10 +8,70 @@ und legt eine Kopie im konfigurierten Zielordner ab. Die Quelldatei bleibt unver --- +## Startmodi und Betriebsmodell (V2.0) + +Ab V2.0 enthält die Anwendung zwei Startmodi in **einem gemeinsamen ausführbaren JAR**: + +| Startmodus | Beschreibung | +|---|---| +| **GUI-Start** (Standard) | Öffnet die JavaFX-Desktop-GUI. Wird verwendet, wenn kein `--headless` angegeben ist. | +| **headless Betrieb** | Klassischer Batch-/Scheduler-Betrieb ohne grafische Oberfläche. Wird über `--headless` aktiviert. | + +### CLI-Optionen + +| Option | Beschreibung | +|---|---| +| *(keine Argumente)* | GUI-Standardstart | +| `--headless` | Aktiviert den headless Batch-Betrieb (wie vor V2.0) | +| `--config ` | Zeigt explizit auf eine `.properties`-Konfigurationsdatei (für GUI und headless) | + +`--config` und `--headless` können kombiniert werden: + +``` +java -jar pdf-umbenenner-bootstrap-*.jar --headless --config C:\Pfad\zur\config.properties +``` + +### Verhalten bei fehlender oder ungültiger `--config`-Datei + +| Startmodus | Datei nicht vorhanden | Datei vorhanden, aber ungültig | +|---|---|---| +| **headless** | Harter Startfehler, Exit-Code `1`, kein Fallback | Harter Startfehler, Exit-Code `1` | +| **GUI** | Fehlermeldung in der GUI, danach Verhalten wie ohne `--config` (Willkommenstext) | Fehlermeldung in der GUI, Konfiguration nicht geladen | + +Im headless Betrieb ist ein nicht vorhandener `--config`-Pfad ein **harter Startfehler**. Ein stiller +Fallback auf das Default-Verhalten ist in diesem Fall ausdrücklich unzulässig. + +### Verhalten bei GUI-Startfehlern + +Tritt vor der erfolgreichen Anzeige der grafischen Oberfläche ein nicht behebbarer Fehler auf +(z. B. fehlende JavaFX-Laufzeit, Bootstrap-Fehler), beendet sich die Anwendung mit Exit-Code `1`. + +### Plattform und Laufwerksbuchstaben + +Die GUI wird **offiziell nur unter Windows** unterstützt. Der headless Betrieb bleibt für den +Windows Server-Betrieb geeignet. + +Gemappte Netzlaufwerke wie `S:\` oder `H:\` werden ausdrücklich unterstützt. Eine Ablehnung +solcher Pfade allein wegen eines dahinterliegenden UNC-Pfads ist unzulässig. + +### Umfang der V2.0-GUI + +Die GUI in V2.0 dient ausschließlich als: + +- **Konfigurationseditor** für die `.properties`-Datei +- **Validierungsoberfläche** (automatische und explizite Prüfung des Konfigurationsstands) +- **Technische Testoberfläche** (Erreichbarkeit des Providers, Pfade, SQLite-Datei, Prompt-Datei) + +Die GUI enthält in V2.0 **keinen** manuellen Verarbeitungslauf. Das Starten eines Batch-Laufs +aus der GUI ist erst ab V2.1+ vorgesehen. Der headless Betrieb über den Windows Task Scheduler +bleibt der einzige Weg, PDF-Dateien automatisiert zu verarbeiten. + +--- + ## Voraussetzungen - Java 21 (JRE oder JDK) -- Zugang zu einem OpenAI-kompatiblen KI-Dienst (API-Schlüssel erforderlich) +- Zugang zu einem KI-Dienst (API-Schlüssel erforderlich; unterstützte Provider: OpenAI-kompatibel, Anthropic Claude) - Quellordner mit OCR-verarbeiteten PDF-Dateien - Schreibzugriff auf Zielordner und Datenbankverzeichnis @@ -26,16 +86,24 @@ Das ausführbare JAR wird durch den Maven-Build im Verzeichnis java -jar pdf-umbenenner-bootstrap/target/pdf-umbenenner-bootstrap-0.0.1-SNAPSHOT.jar ``` -Die Anwendung liest die Konfiguration aus `config/application.properties` relativ zum +Ohne weitere Argumente öffnet sich die **GUI** (Standardstart ab V2.0). + +Für den **headless Betrieb** (Batch-/Scheduler-Start): + +``` +java -jar pdf-umbenenner-bootstrap/target/pdf-umbenenner-bootstrap-0.0.1-SNAPSHOT.jar --headless +``` + +Die Anwendung liest die Konfiguration standardmäßig aus `config/application.properties` relativ zum Arbeitsverzeichnis, in dem der Befehl ausgeführt wird. ### Start über Windows Task Scheduler -Empfohlene Startsequenz für den Windows Task Scheduler: +Empfohlene Startsequenz für den headless Betrieb über den Windows Task Scheduler: 1. Aktion: Programm/Skript starten 2. Programm: `java` -3. Argumente: `-jar C:\Pfad\zur\Installation\pdf-umbenenner-bootstrap\target\pdf-umbenenner-bootstrap-0.0.1-SNAPSHOT.jar` +3. Argumente: `-jar C:\Pfad\zur\Installation\pdf-umbenenner-bootstrap\target\pdf-umbenenner-bootstrap-0.0.1-SNAPSHOT.jar --headless` 4. Starten in: `C:\Pfad\zur\Installation` (muss das Verzeichnis mit `config\application.properties` und `config\prompts\` enthalten) > **Hinweis:** Das „Starten in"-Verzeichnis ist das Arbeitsverzeichnis der Anwendung. @@ -43,11 +111,26 @@ Empfohlene Startsequenz für den Windows Task Scheduler: > `config/prompts/` müssen relativ zu diesem Verzeichnis erreichbar sein. Der JAR-Pfad > in den Argumenten muss absolut oder relativ zum Starten-in-Verzeichnis korrekt angegeben sein. +Alternativ kann über `--config` ein expliziter Konfigurationspfad angegeben werden: + +``` +java -jar ... --headless --config S:\Betrieb\meine-config.properties +``` + +> **Wichtig:** Zeigt `--config` auf eine nicht vorhandene Datei, bricht die Anwendung mit Exit-Code `1` ab. +> Es findet kein stiller Fallback auf `config/application.properties` statt. + --- ## Konfiguration Die Konfiguration wird aus `config/application.properties` geladen. + +Ein vollständiges Konfigurationsbeispiel mit allen unterstützten Parametern, +realistischen Windows-Pfaden und erklärenden Kommentaren liegt unter: + +- [`docs/examples/application.properties`](examples/application.properties) + Vorlagen für lokale und Test-Konfigurationen befinden sich in: - `config/application-local.example.properties` @@ -155,16 +238,17 @@ Der Dateiname der Prompt-Datei dient als Prompt-Identifikator in der Versuchshis (SQLite) und ermöglicht so die Nachvollziehbarkeit, welche Prompt-Version für welchen Verarbeitungsversuch verwendet wurde. -Eine Vorlage befindet sich in `config/prompts/template.txt` und kann direkt verwendet oder -an den jeweiligen KI-Dienst angepasst werden. +Eine angepasste Vorlage befindet sich in `config/prompts/template.txt` und kann direkt +verwendet oder an den jeweiligen KI-Dienst angepasst werden. + +Fehlt die konfigurierte Prompt-Datei, erzeugt die GUI beim Ausführen der technischen Tests +automatisch eine deutsche Standardvorlage am konfigurierten Pfad. Ein Beispiel dieser +Standardvorlage liegt unter [`docs/examples/prompt.txt`](examples/prompt.txt). Die Anwendung ergänzt den Prompt automatisch um: - einen Dokumenttext-Abschnitt - eine explizite JSON-Antwortspezifikation mit den Feldern `title`, `reasoning` und `date` -Der Prompt in `template.txt` muss deshalb **keine** JSON-Formatanweisung enthalten – -nur den inhaltlichen Auftrag an die KI. - --- ## Zielformat @@ -279,11 +363,103 @@ Sie muss nicht manuell verwaltet werden – das Schema wird beim Start automatis --- +## Build und Packaging + +### Gemeinsames ausführbares JAR + +Die gesamte Anwendung wird als **ein einziges ausführbares JAR** ausgeliefert, das GUI-Start +und headless Batch-Betrieb vereint. Eine separate JavaFX-Installation ist nicht erforderlich. + +Das JAR wird vom Maven-Shade-Plugin im Modul `pdf-umbenenner-bootstrap` erzeugt. +Nach einem erfolgreichen Build liegt es unter: + +``` +pdf-umbenenner-bootstrap/target/pdf-umbenenner-bootstrap-0.0.1-SNAPSHOT.jar +``` + +Dieses JAR enthält alle Abhängigkeiten inklusive der JavaFX-Plattformbibliotheken +für Windows (Classifier `win`). Die nativen JavaFX-DLLs werden beim GUI-Start +von JavaFX selbst in ein temporäres Verzeichnis extrahiert. + +### Integrierte JavaFX-Laufzeit + +JavaFX ist als Maven-Dependency im Modul `pdf-umbenenner-adapter-in-gui` mit +Windows-Classifier deklariert (`javafx-base:win`, `javafx-graphics:win`, +`javafx-controls:win`). Das Shade-JAR schließt diese Bibliotheken ein, sodass +der GUI-Start ohne separate JavaFX-Installation auf dem Zielsystem funktioniert. + +Nur das Modul `pdf-umbenenner-adapter-in-gui` hängt direkt von JavaFX ab. +Die Module `domain`, `application`, `adapter-in-cli` und `adapter-out` sind +vollständig JavaFX-frei. + +### Headless-Start ohne JavaFX-Initialisierung + +Beim headless Start (`--headless`) wird JavaFX **nicht** initialisiert. Der +`GuiAdapter` wird nur dann instanziiert und gestartet, wenn der Startmodus GUI ist. +JavaFX-Klassen sind zwar im Shade-JAR enthalten, werden im headless Pfad jedoch +nicht geladen. Headless läuft damit auch auf Windows Server-Systemen ohne +JavaFX-fähige Grafiklaufzeit. + +### Keine EXE, kein Installer + +In V2.0 wird ausschließlich das JAR als Distributionsartefakt ausgeliefert. +EXE-Wrapper und Installer sind bewusst nicht Bestandteil von V2.0. + +### Build-Kommandos + +**Vollständiger Reactor-Build** (alle Module, Tests, Packaging): + +```powershell +.\mvnw.cmd clean verify +``` + +Auf Unix-Systemen (headless CI): + +```bash +./mvnw clean verify +``` + +**Nur das ausführbare JAR erzeugen** (überspringt Tests): + +```powershell +.\mvnw.cmd clean package -pl pdf-umbenenner-bootstrap --also-make -DskipTests +``` + +**Selektiver Reactor-Build** (ohne Coverage-Modul, z. B. während der Entwicklung): + +```powershell +.\mvnw.cmd clean verify -pl pdf-umbenenner-domain,pdf-umbenenner-application,pdf-umbenenner-adapter-out,pdf-umbenenner-adapter-in-cli,pdf-umbenenner-adapter-in-gui,pdf-umbenenner-bootstrap --also-make +``` + +### Technische Hinweise zum Shade-JAR + +- Signaturdateien (`*.SF`, `*.DSA`, `*.RSA`) signierter JARs (u. a. JavaFX) werden + beim Shading entfernt, da sie im zusammengeführten JAR ungültig wären. +- JPMS-Moduldeskriptoren (`module-info.class`) werden entfernt, da JavaFX als + modulares Framework mit dem nicht-modularen Fat-JAR-Modell kollidieren würde. +- `META-INF/services`-Einträge aus allen Abhängigkeiten werden durch den + `ServicesResourceTransformer` zusammengeführt statt überschrieben. +- Der Main-Class-Eintrag im Manifest verweist auf + `de.gecheckt.pdf.umbenenner.bootstrap.PdfUmbenennerApplication`. + Diese Klasse erweitert bewusst **nicht** `javafx.application.Application`, + um den JavaFX-Modul-System-Launcher-Check zu umgehen, der Fat-JAR-Ausführung + blockieren würde. Der GUI-Pfad ruft `Application.launch(...)` explizit auf. + +--- + +## Weitere Dokumentation + +Die Bedienung der GUI ist in [`gui-bedienanleitung.md`](gui-bedienanleitung.md) beschrieben. + +--- + ## Systemgrenzen - Nur OCR-verarbeitete, durchsuchbare PDF-Dateien werden verarbeitet - Keine eingebaute OCR-Funktion -- Kein Web-UI, keine REST-API, keine interaktive Bedienung -- Kein interner Scheduler – der Start erfolgt extern (z. B. Windows Task Scheduler) +- Kein Web-UI, keine REST-API +- Die GUI (V2.0) dient der Konfiguration, Validierung und technischen Diagnose – **kein** manueller Verarbeitungslauf aus der GUI +- Kein interner Scheduler – der Batch-Betrieb wird extern angestoßen (z. B. Windows Task Scheduler, `--headless`) - Quelldateien werden nie überschrieben, verschoben oder gelöscht - Die Identifikation erfolgt über SHA-256-Fingerprint des Dateiinhalts, nicht über Dateinamen +- Die GUI wird offiziell nur unter Windows unterstützt; der headless Betrieb ist für Windows Server geeignet diff --git a/docs/examples/application.properties b/docs/examples/application.properties new file mode 100644 index 0000000..3ce5f36 --- /dev/null +++ b/docs/examples/application.properties @@ -0,0 +1,122 @@ +# PDF Umbenenner – vollstaendiges Konfigurationsbeispiel (V2.0) +# +# Diese Datei zeigt alle unterstuetzten Konfigurationsparameter mit realistischen +# Windows-Pfaden und erklaerenden Kommentaren. +# +# Fuer den produktiven Einsatz: Datei nach config/application.properties kopieren +# und Werte anpassen. Der headless Batch-Betrieb liest standardmaessig +# config/application.properties relativ zum Arbeitsverzeichnis. +# +# Die GUI schlaegt beim "Speichern unter" denselben Pfad vor. + +# --------------------------------------------------------------------------- +# Pfade +# --------------------------------------------------------------------------- + +# Quellordner: Ordner, aus dem OCR-verarbeitete PDF-Dateien gelesen werden. +# Der Ordner muss vorhanden und lesbar sein. +# Beispiel: gemapptes Netzlaufwerk (wird ausdruecklich unterstuetzt) +source.folder=S:\\Eingang + +# Zielordner: Ordner, in den die umbenannten Kopien abgelegt werden. +# Wird automatisch angelegt, wenn er noch nicht existiert (Schreibzugriff erforderlich). +target.folder=S:\\Archiv + +# SQLite-Datenbankdatei fuer Bearbeitungsstatus und Versuchshistorie. +# Das uebergeordnete Verzeichnis muss vorhanden sein. +sqlite.file=S:\\Archiv\\pdf-umbenenner.db + +# Pfad zur externen Prompt-Datei. Der Dateiname dient als Prompt-Identifikator +# in der Versuchshistorie und ermoeg licht die Nachvollziehbarkeit der verwendeten +# Prompt-Version. Fehlt die Datei, kann die GUI sie automatisch anlegen (deutsche +# Standardvorlage). Ein Beispiel der Standardvorlage liegt unter docs/examples/prompt.txt. +prompt.template.file=S:\\Archiv\\prompt.txt + +# --------------------------------------------------------------------------- +# Aktiver KI-Provider +# --------------------------------------------------------------------------- +# Genau ein Provider ist aktiv. Kein automatischer Fallback, keine parallele Nutzung. +# Erlaubte Werte: claude, openai-compatible +# +# Hinweis: Die GUI-Standardvorlage ("Neu") setzt standardmaessig "claude" als aktiven +# Provider, weil Claude alphabetisch der erste unterstuetzte Provider ist. +ai.provider.active=claude + +# --------------------------------------------------------------------------- +# Provider: Anthropic Claude +# --------------------------------------------------------------------------- +# Wird verwendet, wenn ai.provider.active=claude gesetzt ist. + +# Basis-URL des Anthropic-Dienstes (Standard: https://api.anthropic.com) +ai.provider.claude.baseUrl=https://api.anthropic.com + +# Modellname (z. B. claude-3-5-sonnet-20241022) +ai.provider.claude.model=claude-3-5-sonnet-20241022 + +# HTTP-Timeout fuer KI-Anfragen in Sekunden (muss > 0 sein). +ai.provider.claude.timeoutSeconds=60 + +# API-Schluessel fuer Anthropic. +# Vorrangreihenfolge: Umgebungsvariable ANTHROPIC_API_KEY > dieser Wert. +# Das Feld darf leer bleiben, wenn die Umgebungsvariable gesetzt ist. +ai.provider.claude.apiKey= + +# --------------------------------------------------------------------------- +# Provider: OpenAI-kompatibel +# --------------------------------------------------------------------------- +# Wird verwendet, wenn ai.provider.active=openai-compatible gesetzt ist. +# Geeignet fuer OpenAI selbst und jeden API-kompatiblen Drittanbieter. + +# Basis-URL des KI-Dienstes (ohne Pfadsuffix wie /chat/completions). +ai.provider.openai-compatible.baseUrl=https://api.openai.com/v1 + +# Modellname (z. B. gpt-4o-mini) +ai.provider.openai-compatible.model=gpt-4o-mini + +# HTTP-Timeout fuer KI-Anfragen in Sekunden (muss > 0 sein). +ai.provider.openai-compatible.timeoutSeconds=30 + +# API-Schluessel fuer OpenAI-kompatible Dienste. +# Vorrangreihenfolge: OPENAI_COMPATIBLE_API_KEY (Umgebungsvariable) > +# PDF_UMBENENNER_API_KEY (veraltete Umgebungsvariable, weiterhin akzeptiert) > +# ai.provider.openai-compatible.apiKey (dieser Wert) +# Das Feld darf leer bleiben, wenn die Umgebungsvariable gesetzt ist. +ai.provider.openai-compatible.apiKey= + +# --------------------------------------------------------------------------- +# Verarbeitungslimits +# --------------------------------------------------------------------------- + +# Maximale Anzahl historisierter transienter Fehlversuche pro Dokument. +# Muss eine ganze Zahl >= 1 sein. Wert 0 ist ungueltige Konfiguration. +max.retries.transient=3 + +# Maximale Seitenzahl pro Dokument. Dokumente mit mehr Seiten werden als +# deterministischer Inhaltsfehler behandelt (kein KI-Aufruf). +max.pages=10 + +# Maximale Zeichenanzahl des Dokumenttexts, der an die KI gesendet wird. +# Werte bis 1000: unkritisch. +# Werte 1001-3000: erhoehte KI-Kosten moeglich (Warnung in der GUI). +# Werte ab 3001: deutlich erhoehte KI-Kosten moeglich (starke Warnung in der GUI). +# Standardvorlage der GUI: 5000. +max.text.characters=5000 + +# --------------------------------------------------------------------------- +# Optionale Parameter +# --------------------------------------------------------------------------- + +# Lock-Datei fuer den Startschutz (verhindert parallele Instanzen). +# Ohne Konfiguration: pdf-umbenenner.lock im Arbeitsverzeichnis. +runtime.lock.file=S:\\Archiv\\pdf-umbenenner.lock + +# Log-Verzeichnis. Ohne Konfiguration: ./logs/ im Arbeitsverzeichnis. +log.directory=S:\\Archiv\\logs + +# Log-Level (DEBUG, INFO, WARN, ERROR). Standard: INFO. +log.level=INFO + +# Sensible KI-Inhalte (vollstaendige Rohantwort und Reasoning) ins Log schreiben. +# Erlaubte Werte: true oder false. Standard: false (geschuetzt). +# Die KI-Rohantwort wird unabhaengig davon immer in der SQLite-Datenbank gespeichert. +log.ai.sensitive=false diff --git a/docs/examples/prompt.txt b/docs/examples/prompt.txt new file mode 100644 index 0000000..8eea479 --- /dev/null +++ b/docs/examples/prompt.txt @@ -0,0 +1,23 @@ +Du bist ein Assistent für ein deutsches Dokumentenverwaltungssystem. +Deine Aufgabe ist es, aus dem Inhalt einer bereits OCR-verarbeiteten PDF-Datei +einen aussagekräftigen, kurzen und normierten Dateinamensvorschlag zu erstellen. + +Antworte ausschließlich mit einem validen JSON-Objekt im folgenden Schema: +{ + "date": "YYYY-MM-DD", + "title": "Kurztitel auf Deutsch", + "reasoning": "Kurze Begründung auf Deutsch" +} + +Regeln: +- Das Feld "title" ist verpflichtend. +- Das Feld "reasoning" ist verpflichtend. +- Das Feld "date" ist optional. Wenn kein belastbares Datum aus dem Dokument eindeutig ableitbar ist, lass das Feld weg. Kein Datum erfinden. +- Das Datumsformat ist YYYY-MM-DD (z.B. 2026-03-15). +- Der Titel ist auf Deutsch, verständlich und eindeutig für den Dokumentinhalt. +- Der Titel hat maximal 20 Zeichen (Basistitel ohne Suffix). +- Keine generischen Bezeichner wie "Dokument", "Scan", "Datei", "PDF". +- Keine Sonderzeichen außer Leerzeichen im Titel. +- Eigennamen bleiben unverändert. +- Umlaute und ß sind erlaubt. +- Kein Text außerhalb des JSON-Objekts. diff --git a/docs/freigabe-v2_0.md b/docs/freigabe-v2_0.md new file mode 100644 index 0000000..1fedeee --- /dev/null +++ b/docs/freigabe-v2_0.md @@ -0,0 +1,94 @@ +# V2.0-Freigabe + +## Geprüfter Stand + +- Git-Branch: `main` +- Git-Commit (HEAD, zum Zeitpunkt der Prüfung): `1bb7a427357c73039c09a8e1bfe351dee54df765` +- Datum der Prüfung: 2026-04-20 + +--- + +## Ausgeführte Prüfungen + +| Prüfung | Ergebnis | +|---|---| +| Vollständiger Maven-Reactor-Build (`clean verify`, alle 6 Module, `-DskipPitest=true`) | **ERFOLGREICH** | +| Unit-Tests gesamt | **1.398 Tests, 0 Failures, 0 Errors, 0 Skipped** | +| Integrations-/Smoke-Tests (`ExecutableJarSmokeTestIT`, Bootstrap) | **5 Tests, alle grün** | +| Shaded-JAR erzeugt unter `pdf-umbenenner-bootstrap/target/` | **ja** | +| Konfigurations- und Dokumentationsbeispiele auf Konsistenz geprüft | **ja** | +| Bedienanleitung (`gui-bedienanleitung.md`) gegen reales GUI-Verhalten stichprobengeprüft | **ja – ein während der Prüfung identifizierter Befund wurde unmittelbar korrigiert (siehe R3)** | + +--- + +## Build- und Test-Ergebnisse + +Ausgeführtes Kommando: +``` +.\mvnw.cmd clean verify -pl pdf-umbenenner-domain,pdf-umbenenner-application,pdf-umbenenner-adapter-out,pdf-umbenenner-adapter-in-cli,pdf-umbenenner-adapter-in-gui,pdf-umbenenner-bootstrap --also-make -DskipPitest=true +``` + +**Gesamtergebnis: BUILD SUCCESS** +**Laufzeit: 01:15 min** + +| Modul | Tests | Failures | Errors | Skipped | +|---|---|---|---|---| +| `pdf-umbenenner-domain` | 227 | 0 | 0 | 0 | +| `pdf-umbenenner-application` | 455 | 0 | 0 | 0 | +| `pdf-umbenenner-adapter-in-cli` | 8 | 0 | 0 | 0 | +| `pdf-umbenenner-adapter-in-gui` | 190 | 0 | 0 | 0 | +| `pdf-umbenenner-adapter-out` | 371 | 0 | 0 | 0 | +| `pdf-umbenenner-bootstrap` (Unit) | 147 | 0 | 0 | 0 | +| `pdf-umbenenner-bootstrap` (IT) | 5 | 0 | 0 | 0 | +| **Gesamt** | **1.403** | **0** | **0** | **0** | + +Erzeugtes Shaded-JAR: `pdf-umbenenner-bootstrap/target/pdf-umbenenner-bootstrap-0.0.1-SNAPSHOT.jar` +Größe: ca. 28 MB (enthält JavaFX für Windows, PDFBox, SQLite-JDBC, Log4j2 und alle Module) + +--- + +## Offene nicht blockierende Restpunkte + +Die folgenden Restpunkte wurden in der integrierten Gesamtprüfung dokumentiert und +gelten als freigabekompatibel. + +### R1 – ByteBuddy-Agent-Warnung bei Tests + +Beim Build erscheint wiederholt `WARNING: A Java agent has been loaded dynamically +(byte-buddy-agent-1.14.12.jar)`. Der Hinweis stammt von Mockito und tritt seit dem V1.1-Stand +auf. Kein funktionaler Einfluss. Mit `-XX:+EnableDynamicAgentLoading` unterdrückbar, aber keine +Pflicht für V2.0. + +### R2 – GUI-Tests ohne echten JavaFX-Rendering-Pfad + +Die GUI-Tests laufen unter headless JavaFX (Monocle) und prüfen View-Modell-Logik, +Zustandsübergänge und Koordinatoren-Verhalten. Das visuelle Rendering (Farbgebung der +Meldungspräfixe, Layout-Details) ist nicht automatisiert geprüft. Dies entspricht der in +CLAUDE.md definierten GUI-Teststrategie (kein TestFX über Monocle hinaus). + +### R3 – Bedienanleitung: Legacy-Umgebungsvariable für OpenAI-kompatibel (behoben) + +**Ursprünglicher Befund:** Abschnitt 10 der `gui-bedienanleitung.md` beschrieb +`OPENAI_COMPATIBLE_API_KEY` fälschlich als Legacy-Umgebungsvariable. Tatsächlich ist +`OPENAI_COMPATIBLE_API_KEY` die primäre providerspezifische Umgebungsvariable, und die +echte Legacy-Umgebungsvariable lautet `PDF_UMBENENNER_API_KEY`. + +**Status:** Korrigiert. Abschnitt 10 benennt jetzt `OPENAI_COMPATIBLE_API_KEY` als +primäre Variable und `PDF_UMBENENNER_API_KEY` als Legacy-Variable und hält fest, dass +Claude keine Legacy-Variable hat. + +**Kein Release-Blocker und im V2.0-Freigabestand nicht mehr offen.** + +--- + +## Freigabeaussage + +V2.0 ist nach Prüfung fehlerfrei buildbar, vollständig nach Spezifikation umgesetzt +und als freigabefähig einzustufen. Keine Release-Blocker. Die zwei nicht blockierenden +Restpunkte (R1, R2) sind dokumentiert und können außerhalb des V2.0-Scopes adressiert +werden; R3 wurde während der finalen Prüfung unmittelbar behoben. + +Der vollständige Maven-Reactor-Build ist grün (1.403 Tests, 0 Failures, 0 Errors, +0 Skipped). Alle in der integrierten Gesamtprüfung definierten Spezifikations- +Prüfpunkte gegen die Spec-Trias sind als erfüllt bestätigt. Die Dokumentation ist +vollständig und konsistent mit dem Code. diff --git a/docs/gui-bedienanleitung.md b/docs/gui-bedienanleitung.md new file mode 100644 index 0000000..e45d630 --- /dev/null +++ b/docs/gui-bedienanleitung.md @@ -0,0 +1,405 @@ +# GUI-Bedienanleitung – PDF-Umbenenner V2.0 + +Diese Anleitung beschreibt die JavaFX-Desktop-GUI des PDF-Umbenenners. Sie richtet sich an +Endbenutzer und Betreuer, die die Konfiguration der Anwendung über die grafische Oberfläche +verwalten und technisch prüfen möchten. + +--- + +## 1. Zweck und Scope der GUI in V2.0 + +Die GUI dient in V2.0 ausschließlich als: + +- **Konfigurationseditor** für die `.properties`-Datei +- **Validierungsoberfläche** für den aktuellen Konfigurationsstand +- **Technische Test- und Diagnoseoberfläche** für Erreichbarkeit des Providers, + Pfadprüfungen und Ressourcenverfügbarkeit + +Die GUI enthält in V2.0 **keinen** manuellen Verarbeitungslauf. Das Starten eines +Batch-Laufs aus der GUI ist erst ab einer späteren Ausbaustufe vorgesehen. +Ebenso gibt es keinen Historien-Tab, keine Datenbankansicht und kein Kosten-Tracking. + +Der headless Batch-/Scheduler-Betrieb über `--headless` bleibt der einzige Weg, +PDF-Dateien automatisiert zu verarbeiten. + +--- + +## 2. Startzustände + +### 2.1 GUI-Standardstart + +Wird die Anwendung ohne CLI-Argumente gestartet, öffnet sich die JavaFX-Desktop-GUI. +Es wird keine Konfigurationsdatei automatisch geladen. + +Stattdessen zeigt die GUI einen deutschen Willkommenstext mit dem Hinweis, über +„Neu" eine Standardvorlage zu erzeugen oder über „Öffnen" eine bestehende +`.properties`-Datei zu laden. + +### 2.2 Start mit `--config ` (gültige Datei) + +Wird beim Start eine gültige `.properties`-Datei über `--config ` angegeben, +lädt die GUI diese Datei automatisch und zeigt den Inhalt im Editor an. Der Pfad +wird im Header angezeigt. + +### 2.3 Start mit `--config ` (ungültiger oder nicht vorhandener Pfad) + +Zeigt der angegebene Pfad auf eine nicht vorhandene oder nicht lesbare Datei, +erscheint eine Fehlermeldung im zentralen Meldungsbereich. Danach verhält sich die +GUI so, als wäre `--config` nicht angegeben worden: Es erscheint der Willkommenstext, +und der Benutzer kann manuell eine Datei öffnen oder eine neue Konfiguration anlegen. + +Im Unterschied zum headless Betrieb ist dies kein harter Startfehler – die GUI +bleibt vollständig bedienbar. + +### 2.4 GUI-Startfehler vor Anzeige der Oberfläche + +Tritt vor der erfolgreichen Anzeige der grafischen Oberfläche ein nicht behebbarer +Fehler auf (z. B. fehlende JavaFX-Laufzeit, schwerwiegender Bootstrap-Fehler), +beendet sich die Anwendung mit Exit-Code `1`. In diesem Fall wird keine Oberfläche +angezeigt. + +--- + +## 3. Header und Meldungsbereich + +### 3.1 Header + +Der Header oben im Fenster zeigt den Pfad der aktuell geladenen `.properties`-Datei. +Ist keine Datei geladen, bleibt der Pfadbereich leer oder zeigt einen entsprechenden +Hinweis. + +Sobald ungespeicherte Änderungen vorliegen, erscheinen zwei visuelle Markierungen: + +- ein kleines **„geändert"**-Label im Header +- ein führendes **`*`** im Fenstertitel + +Diese Markierungen verschwinden, sobald die Datei gespeichert wurde oder Änderungen +verworfen werden. + +### 3.2 Zentraler Meldungsbereich + +Am unteren Ende der GUI befindet sich ein großer, nicht editierbarer +Meldungsbereich. Er ist dauerhaft sichtbar und zeigt Ergebnisse von +Validierungen, technischen Tests, Migrationsmeldungen und sonstige +Statusinformationen. + +Der Meldungsbereich verwendet vier feste Stufen: + +| Stufe | Präfix | Farbe des Präfix | +|-------|--------|------------------| +| **Info** | `Info:` | Blau | +| **Hinweis** | `Hinweis:` | Grün | +| **Warnung** | `Warnung:` | Orange | +| **Fehler** | `Fehler:` | Rot | + +Nur das Präfix am Zeilenanfang wird farbig dargestellt. Der eigentliche +Meldungstext derselben Zeile ist immer schwarz. Die vier Stufen dienen +ausschließlich der visuellen Einordnung; sie verhindern das Speichern nicht. + +--- + +## 4. Aktionen + +### 4.1 Neu + +Erzeugt eine neue Konfiguration aus der internen Standardvorlage. Die Vorlage +enthält sinnvolle Standardwerte und beide bekannten Provider-Blöcke (Claude und +OpenAI-kompatibel). Standardmäßig ist der alphabetisch erste Provider (Claude) +als aktiver Provider vorbelegt. + +Sind zum Zeitpunkt der Aktion ungespeicherte Änderungen vorhanden, erscheint +der Schutzdialog (siehe Abschnitt 8). + +### 4.2 Öffnen + +Öffnet einen nativen Dateidialog gefiltert auf `*.properties`-Dateien. Die +ausgewählte Datei wird geladen und im Editor angezeigt. + +Enthält die Datei das ältere Legacy-Format (flache `api.*`-Schlüssel), wird sie +automatisch ins aktuelle Mehrprovider-Schema migriert. Vor der Migration wird eine +`.bak`-Sicherung der Originaldatei angelegt (siehe Abschnitt 9). Die durchgeführte +Migration wird im zentralen Meldungsbereich sichtbar gemeldet. + +Sind zum Zeitpunkt der Aktion ungespeicherte Änderungen vorhanden, erscheint +der Schutzdialog (siehe Abschnitt 8). + +### 4.3 Speichern + +Schreibt den aktuellen Editorstand in die zuletzt geladene oder gespeicherte Datei. + +Ist die Konfiguration neu und wurde noch nie gespeichert, verhält sich „Speichern" +wie „Speichern unter": Es wird ein Dateidialog geöffnet und ein Speicherpfad +erfragt. + +### 4.4 Speichern unter + +Öffnet einen nativen Dateidialog gefiltert auf `*.properties`-Dateien. Als +Vorschlagspfad wird `config/application.properties` relativ zum aktuellen +Arbeitsverzeichnis verwendet. Damit ist die gespeicherte Datei ohne weitere +Schritte für den nächsten headless Batch-Lauf nutzbar. + +Zeigt der Dialog auf eine bereits existierende Datei, erscheint eine +Rückfrage „Datei überschreiben?". Bei Bestätigung wird vor dem Überschreiben +automatisch eine `.bak`-Sicherung angelegt (siehe Abschnitt 9). + +Kommentare und Schlüsselreihenfolge der `.properties`-Datei werden beim +Speichern normalisiert. + +### 4.5 Validieren + +Führt eine explizite, nicht-schreibende Gesamtprüfung des aktuellen Editorzustands +durch. Die Prüfung läuft lokal ohne Netzwerkzugriff und umfasst dieselben +Regelprüfungen wie die automatische Hintergrundvalidierung, kann aber zusätzliche +lokale Prüfpunkte zusammenführen. + +Die Prüfung arbeitet auf dem **aktuellen GUI-Zustand**, also auch auf ungespeicherten +Änderungen. Die Datei wird dabei **nicht** implizit gespeichert. + +Ergebnisse erscheinen im zentralen Meldungsbereich und als feldnahe +Fehlermeldungen direkt unter den betroffenen Eingabefeldern. + +### 4.6 Technische Tests ausführen + +Führt eine umfassende technische Prüfung des aktuellen Editorzustands durch, +einschließlich provider-naher Tests mit Netzwerkzugriff. Alle Prüfpunkte werden +vollständig und gesammelt durchlaufen; die Aktion bricht bei einem Einzelfehler +nicht ab. + +Geprüft werden unter anderem: + +- `.properties`-Datei und Provider-Konfiguration +- Erreichbarkeit von Base-URL bzw. Endpoint +- Vorhandensein und technische Akzeptanz des API-Keys +- Abrufbarkeit der Modellliste +- Plausibilität des gewählten Modells +- Vorhandensein und Lesbarkeit der Prompt-Datei +- Quellordner (vorhanden und lesbar) +- Zielordner (vorhanden oder anlegbar und beschreibbar) +- SQLite-Datei bzw. -Pfad + +Die Tests arbeiten auf dem **aktuellen GUI-Zustand** ohne implizites Speichern. +Wenn ungespeicherte Änderungen vorliegen, ist ein entsprechender Hinweis +zweckmäßig. + +Bestimmte Befunde können durch korrigierende Maßnahmen behoben werden (z. B. +Zielordner anlegen, SQLite-Datei anlegen, fehlende Prompt-Datei mit einem +deutschen Standardinhalt erzeugen). Diese Korrekturen erfolgen **nicht** still im +Hintergrund, sondern erst nach Bestätigung eines gesammelten Bestätigungsdialogs +(„Folgende Korrekturen werden durchgeführt … Fortfahren?"). + +Die automatisch erzeugte Prompt-Datei enthält einen deutschen Standardprompt, +der ohne weitere Anpassung funktioniert. Ein Beispiel dieses Standardprompts liegt +unter [`docs/examples/prompt.txt`](../docs/examples/prompt.txt). + +Nicht automatisch korrigierbar sind insbesondere: falscher API-Key, +unerreichbare Base-URL, nicht verfügbare Modellliste, sonstige externe +technische Fehler. + +### 4.7 Modelle neu laden + +Ruft die Modellliste des aktuell ausgewählten Providers erneut über einen +Hintergrund-Worker-Thread ab. Der gleiche Abruf wird auch automatisch bei jedem +Providerwechsel ausgelöst. + +--- + +## 5. Feldnahe Fehlermeldungen + +Direkt unter bestimmten Eingabefeldern kann die GUI kleine, rote, +deutschsprachige Hinweise einblenden, wenn der eingetragene Wert fehlerhaft oder +riskant ist. Diese feldnahen Meldungen ergänzen den zentralen Meldungsbereich +und ersetzen ihn nicht. + +Feldnahe Meldungen erscheinen nach einer Validierung oder nach dem Ausführen der +technischen Tests. Sie verschwinden, sobald der Fehler behoben und eine neue +Prüfung durchgeführt wurde. + +--- + +## 6. Automatische Hintergrundvalidierung + +Eine geladene Konfiguration wird sofort beim Öffnen geprüft. Während der +Bearbeitung aktualisiert die Validierung ihre Ergebnisse kontinuierlich im +Hintergrund. Ergebnisse werden im zentralen Meldungsbereich und als feldnahe +Hinweise angezeigt. + +Die automatische Validierung unterscheidet: + +- **Fehler:** Der Konfigurationsstand ist nicht lauffähig. +- **Warnungen:** Die Einstellung ist technisch akzeptabel, aber riskant oder + unüblich. Beispiele: sehr hohe `max.text.characters`-Werte, ungewöhnliche + Timeouts. +- **Hinweise:** Informationen ohne Handlungsbedarf. + +Warnungen und Hinweise verhindern das Speichern nicht. Vor dem Speichern eines +als **nicht lauffähig** markierten Stands erscheint jedoch eine deutlich sichtbare +Warnung im zentralen Meldungsbereich, die ausdrücklich auf mögliche Auswirkungen +auf den nächsten headless Lauf hinweist. Speichern ist dennoch erlaubt. + +Wirtschaftliche Warnschwellen für `max.text.characters`: + +| Wertebereich | Bewertung | +|---|---| +| bis 1.000 | unkritisch | +| 1.001 – 3.000 | Warnung | +| ab 3.001 | starke Warnung | + +`max.pages` wird als Plausibilitäts- und Performance-Hinweis behandelt. + +--- + +## 7. Provider-Bedienung und Modellabruf + +### 7.1 Provider-ComboBox + +Im Bereich „Provider" befindet sich eine ComboBox zur Auswahl des aktiven +Providers. In V2.0 stehen zwei Einträge zur Verfügung: + +- **Claude** +- **OpenAI-kompatibel** + +Nur der aktuell ausgewählte Provider-Bereich ist im Formular sichtbar. Der +verdeckte Provider-Block behält seine Daten; ein Providerwechsel löscht die +Konfiguration des anderen Blocks nicht. + +### 7.2 Automatischer Modellabruf + +Bei jedem Providerwechsel startet der Modellabruf automatisch auf einem +Hintergrund-Worker-Thread. Wird die Modellliste erfolgreich geladen, erscheint +eine nicht editierbare ComboBox mit den verfügbaren Modellen. Das erste Modell +der Liste ist automatisch vorbelegt. + +Kann keine Modellliste abgerufen werden (z. B. wegen fehlendem oder falschem +API-Key, unerreichbarem Endpoint), erscheint statt der ComboBox ein leeres +Texteingabefeld. In diesem Fall muss der Modellname manuell eingetragen werden. + +Wurde zuvor ein Modellname manuell eingetragen und wird später eine echte +Modellliste geladen, in der dieser Wert nicht vorkommt, wird der manuell +eingetragene Modellname verworfen. Es wird dann das erste Modell der Liste +vorbelegt. + +--- + +## 8. Dirty-State und Schutzdialoge + +Sobald eine geladene oder neu erzeugte Konfiguration bearbeitet wird, gilt der +Editor als „dirty" (ungespeicherte Änderungen). Zwei visuelle Markierungen +zeigen diesen Zustand an: + +- Ein **`*`**-Präfix im Fenstertitel +- Ein kleines **„geändert"**-Label im Header + +Vor den Aktionen „Neu", „Öffnen" und beim Schließen des Fensters prüft die GUI, +ob ungespeicherte Änderungen vorhanden sind. Ist dies der Fall, erscheint ein +Schutzdialog mit drei Optionen: + +| Option | Wirkung | +|--------|---------| +| **Speichern** | Speichert die Änderungen und führt die Aktion danach aus | +| **Verwerfen** | Verwirft die Änderungen und führt die Aktion aus | +| **Abbrechen** | Bricht die Aktion ab; die Änderungen bleiben erhalten | + +--- + +## 9. `.bak`-Sicherung beim Überschreiben und Legacy-Migration + +### 9.1 Sicherung beim Überschreiben + +Bevor eine bestehende `.properties`-Datei überschrieben wird, legt die GUI +automatisch eine Sicherungskopie an: + +- Standardfall: `.bak` (im selben Verzeichnis) +- Falls `.bak` bereits existiert: `.bak.1`, `.bak.2`, … +- Bestehende Sicherungen werden **niemals überschrieben**. + +Dieses Schema gilt sowohl für „Speichern unter" bei existierendem Ziel als auch +für die Legacy-Migration beim Öffnen. + +### 9.2 Legacy-Migration + +Ältere `.properties`-Dateien, die noch die flachen Schlüssel `api.baseUrl`, +`api.model`, `api.timeoutSeconds` und `api.key` verwenden, erkennt die GUI beim +Öffnen automatisch als Legacy-Format. Sie führt folgende Schritte durch: + +1. `.bak`-Sicherung der Originaldatei anlegen (nach dem Schema aus 9.1) +2. Inhalt ins aktuelle Mehrprovider-Schema überführen: + - Legacy-Werte werden dem Namensraum `openai-compatible` zugeordnet + - `ai.provider.active=openai-compatible` wird ergänzt +3. Die migrierte Konfiguration wird im Editor angezeigt + +Die durchgeführte Migration wird im zentralen Meldungsbereich sichtbar gemeldet, +damit der Benutzer die Änderung nachvollziehen kann. + +--- + +## 10. API-Key-Auflösungsreihenfolge + +Der API-Key eines Providers wird in folgender Priorität aufgelöst: + +1. **Providerspezifische Umgebungsvariable** (höchste Priorität) +2. Für **OpenAI-kompatibel** zusätzlich: Legacy-Umgebungsvariable + `PDF_UMBENENNER_API_KEY` (aus dem früheren Einzelprovider-Stand) +3. **Property-Wert** in der `.properties`-Datei + +| Provider | Providerspezifische Umgebungsvariable | +|---|---| +| Claude | `ANTHROPIC_API_KEY` | +| OpenAI-kompatibel | `OPENAI_COMPATIBLE_API_KEY` | + +Für **OpenAI-kompatibel** wird zusätzlich die Legacy-Variable +`PDF_UMBENENNER_API_KEY` akzeptiert, falls die providerspezifische Variable +nicht gesetzt ist. Für Claude gibt es keine Legacy-Variable. + +Die GUI zeigt unterhalb des API-Key-Felds die Herkunft des aktuell wirksamen +Schlüssels an. Greift eine Umgebungsvariable, erscheint ein Hinweis mit dem +Namen der Variablen (z. B. „Aktuell wirksam aus Umgebungsvariable +`ANTHROPIC_API_KEY`"). Ist kein Schlüssel aus keiner Quelle verfügbar, erscheint +eine Warnung. + +Das API-Key-Feld ist ein normales, unmaskiertes Textfeld. Ein leeres Feld entfernt +den vorhandenen Property-Wert **nicht** stillschweigend, solange keine +Umgebungsvariable greift. In diesem Fall bleibt der bestehende Property-Wert +erhalten, und die GUI zeigt eine deutliche Warnung. + +--- + +## 11. Pfadfelder und Datei-/Ordnerdialoge + +Für die folgenden Konfigurationswerte stellt die GUI je ein Texteingabefeld +sowie einen kleinen Button zum Öffnen des nativen Dialogs bereit: + +| Feld | Dialog-Typ | +|------|------------| +| Quellordner | Ordnerdialog | +| Zielordner | Ordnerdialog | +| SQLite-Datei | Dateidialog | +| Prompt-Datei | Dateidialog | + +Pfade können auch direkt ins Texteingabefeld eingegeben werden, ohne den Dialog +zu nutzen. + +--- + +## 12. Windows-Hinweise zu gemappten Laufwerken + +Die GUI sowie alle zugehörigen Pfadprüfungen akzeptieren Windows-Laufwerksbuchstaben +wie `S:\`, `H:\` oder `C:\`. Gemappte Netzlaufwerke werden ausdrücklich unterstützt +und werden nicht allein wegen eines dahinterliegenden UNC-Pfads abgelehnt. + +UNC-Pfade (z. B. `\\server\freigabe\pfad\`) werden ebenfalls akzeptiert, sind aber +nicht das primäre Format für den GUI-Betrieb. + +Die GUI wird offiziell nur unter **Windows** unterstützt. + +--- + +## 13. Bekannte Einschränkungen V2.0 + +| Einschränkung | Erläuterung | +|---|---| +| Kein manueller Verarbeitungslauf | Das Starten eines Batch-Laufs aus der GUI ist erst ab V2.1+ vorgesehen | +| Kein Historien-Tab | Eine Ansicht der SQLite-Datenbank und Verarbeitungshistorie ist für spätere Ausbaustufen vorbehalten | +| Kein Kosten-Tracking | Token-/Preisberechnungen sind für spätere Ausbaustufen vorbehalten | +| Keine Erkennung externer Änderungen | Wird die `.properties`-Datei während einer GUI-Sitzung von außen geändert, erkennt die GUI dies nicht. Die GUI arbeitet weiterhin auf dem zuletzt geladenen Stand | +| Keine Koordination mit parallelen headless Läufen | Läuft gleichzeitig ein headless Batch-Lauf, koordinieren sich GUI und headless Betrieb nicht. Schreibkonflikte können entstehen, wenn dieselbe `.properties`-Datei gleichzeitig über die GUI gespeichert und vom headless Lauf gelesen wird | +| GUI nur für Windows | Die GUI wird offiziell nur unter Windows unterstützt; der headless Betrieb ist für Windows Server geeignet | diff --git a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiConfigurationEditorWorkspace.java b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiConfigurationEditorWorkspace.java index 868a870..f93d7ef 100644 --- a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiConfigurationEditorWorkspace.java +++ b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiConfigurationEditorWorkspace.java @@ -108,6 +108,13 @@ public final class GuiConfigurationEditorWorkspace { private final Label configurationPathValueLabel = new Label(); /** Package-private to allow visibility assertions in smoke tests. */ final Label dirtyMarkerLabel = new Label("geändert"); + /** + * Package-private to allow startup-notice visibility assertions in smoke tests. + * Returns the status label used to display startup notices and status messages in the header. + */ + Label statusNoticeLabel() { + return statusLabel; + } private final Label welcomeTitleLabel = new Label("Willkommen"); private final Label welcomeTextLabel = new Label(WELCOME_TEXT); /** Package-private to allow node lookups in smoke tests. */ diff --git a/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiEditorIntegrationTest.java b/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiEditorIntegrationTest.java index b4dba68..b157cf4 100644 --- a/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiEditorIntegrationTest.java +++ b/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiEditorIntegrationTest.java @@ -2,6 +2,7 @@ package de.gecheckt.pdf.umbenenner.adapter.in.gui; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.IOException; @@ -323,6 +324,95 @@ class GuiEditorIntegrationTest { } } + // ========================================================================= + // GUI startup with a non-existent --config path: startup notice rendered in header + // ========================================================================= + + /** + * Verifies that when the workspace is constructed with a startup notice (as Bootstrap does + * when {@code --config} points to a non-existent file), the notice text is rendered in the + * visible header status label. + *

+ * This complements {@link #guiStartup_withNonExistentConfigPath_usesBlankStateAndCarriesStartupNotice} + * which only verifies the blank editor state. This test verifies the user-visible side: the + * notice must be rendered so the user can read it. + * + * @throws Exception if the FX thread task fails or times out + */ + @Test + void guiStartup_withNonExistentConfigPath_noticeIsRenderedInHeaderStatusLabel() + throws Exception { + String notice = "Konfigurationsdatei nicht gefunden: /no/such/file.properties\n" + + "Die GUI startet ohne Konfigurationsdatei."; + GuiConfigurationEditorState blankState = GuiConfigurationEditorStateFactory.createBlankStartState(); + GuiConfigurationFileWriter noOpWriter = (values, path) -> GuiConfigurationSaveResult.saved(path); + GuiStartupContext context = new GuiStartupContext( + blankState, + Optional.of(notice), + configFilePath -> GuiConfigurationEditorStateFactory.createBlankStartState(), + noOpWriter, + req -> new de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.ModelCatalogResult.IncompleteConfiguration(req.providerIdentifier(), "kein Port im Test"), + (family, propertyValue) -> de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.EffectiveApiKeyDescriptor.absent(), + new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ProviderTechnicalTestService( + req2 -> new de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.ModelCatalogResult.IncompleteConfiguration(req2.providerIdentifier(), "kein Port im Test"), + (fam2, pv2) -> de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.EffectiveApiKeyDescriptor.absent()), + new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.PathCheckPort() { + @Override public boolean isDirectoryReadable(String p) { return false; } + @Override public boolean isDirectoryWritableOrCreatable(String p) { return false; } + @Override public boolean isFileReadable(String p) { return false; } + @Override public boolean isSqlitePathUsable(String p) { return false; } + }, + new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.TechnicalTestOrchestrator( + new de.gecheckt.pdf.umbenenner.application.validation.editor.EditorConfigurationValidator(), + new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.PathCheckPort() { + @Override public boolean isDirectoryReadable(String p) { return false; } + @Override public boolean isDirectoryWritableOrCreatable(String p) { return false; } + @Override public boolean isFileReadable(String p) { return false; } + @Override public boolean isSqlitePathUsable(String p) { return false; } + }, + new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ProviderTechnicalTestService( + req99 -> new de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.ModelCatalogResult.IncompleteConfiguration(req99.providerIdentifier(), "kein Port im Test"), + (fam99, pv99) -> de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.EffectiveApiKeyDescriptor.absent())), + new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionExecutionService( + new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ResourceCreationPort() { + @Override public de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome createDirectory(de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionSuggestion.CreateDirectory s) { return new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome.NotAttempted(s, "kein Port im Test"); } + @Override public de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome createPromptFile(de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionSuggestion.CreatePromptFile s) { return new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome.NotAttempted(s, "kein Port im Test"); } + @Override public de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome prepareSqlitePath(de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionSuggestion.PrepareSqlitePath s) { return new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome.NotAttempted(s, "kein Port im Test"); } + })); + + AtomicReference error = new AtomicReference<>(); + CountDownLatch latch = new CountDownLatch(1); + + Platform.runLater(() -> { + try { + GuiConfigurationEditorWorkspace workspace = new GuiConfigurationEditorWorkspace(context); + + // The startup notice must be rendered in the visible header status label. + javafx.scene.control.Label noticeLabel = workspace.statusNoticeLabel(); + assertNotNull(noticeLabel, "Header status label must not be null"); + assertTrue(noticeLabel.isVisible(), + "Header status label must be visible when a startup notice is present"); + assertTrue(noticeLabel.isManaged(), + "Header status label must be managed when a startup notice is present"); + assertFalse(noticeLabel.getText().isBlank(), + "Header status label text must not be blank when startup notice is present"); + assertTrue(noticeLabel.getText().contains("Konfigurationsdatei nicht gefunden"), + "Header status label must contain the notice text; got: " + noticeLabel.getText()); + + } catch (Throwable t) { + error.set(t); + } finally { + latch.countDown(); + } + }); + + assertTrue(latch.await(FX_TIMEOUT_SECONDS, TimeUnit.SECONDS), + "FX task must complete within timeout"); + if (error.get() != null) { + throw new AssertionError("FX thread threw an exception", error.get()); + } + } + // ========================================================================= // --config path resolution: static helper (no FX thread needed) // ========================================================================= diff --git a/pdf-umbenenner-bootstrap/src/test/java/de/gecheckt/pdf/umbenenner/bootstrap/ExecutableJarSmokeTestIT.java b/pdf-umbenenner-bootstrap/src/test/java/de/gecheckt/pdf/umbenenner/bootstrap/ExecutableJarSmokeTestIT.java index e076bc6..b9c62c3 100644 --- a/pdf-umbenenner-bootstrap/src/test/java/de/gecheckt/pdf/umbenenner/bootstrap/ExecutableJarSmokeTestIT.java +++ b/pdf-umbenenner-bootstrap/src/test/java/de/gecheckt/pdf/umbenenner/bootstrap/ExecutableJarSmokeTestIT.java @@ -1,6 +1,7 @@ package de.gecheckt.pdf.umbenenner.bootstrap; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -19,6 +20,14 @@ import org.junit.jupiter.api.io.TempDir; *

* These tests verify that the shaded executable JAR can be run via {@code java -jar} * and behaves correctly for both success and invalid configuration scenarios. + * Regression scenarios included: + *

    + *
  • Headless start without {@code --config}: uses default path, exits 0
  • + *
  • Headless start with valid {@code --config}: uses supplied path, exits 0
  • + *
  • Headless start with non-existent {@code --config}: hard startup failure, exits 1
  • + *
  • Invalid configuration: controlled failure, exits 1
  • + *
  • Headless output free of JavaFX initialisation markers
  • + *
*

* Tests are executed by the maven-failsafe-plugin after the package phase. * The *IT suffix ensures failsafe picks them up as integration tests. @@ -277,4 +286,282 @@ class ExecutableJarSmokeTestIT { "Output should indicate configuration/validation error. Got: " + outputText ); } + + // ========================================================================= + // Regression: headless start with explicit valid --config path + // ========================================================================= + + /** + * Regression test: {@code --headless --config } must locate the supplied + * configuration file and complete successfully with exit code 0. + *

+ * This guards against a regression where the explicit config path would be silently ignored + * and the default path would be used instead (or vice-versa causing a startup failure). + */ + @Test + void jar_headlessWithExplicitValidConfigPath_exitCode0(@TempDir Path workDir) throws Exception { + Path configDir = Files.createDirectory(workDir.resolve("mycfg")); + Path sourceDir = Files.createDirectory(workDir.resolve("source")); + Path targetDir = Files.createDirectory(workDir.resolve("target")); + Path logsDir = Files.createDirectory(workDir.resolve("logs")); + Path dbParent = Files.createDirectory(workDir.resolve("data")); + Path promptDir = Files.createDirectory(workDir.resolve("mycfg/prompts")); + + Path sqliteFile = Files.createFile(dbParent.resolve("pdf-umbenenner.db")); + Path promptTemplateFile = Files.createFile(promptDir.resolve("template.txt")); + Files.writeString(promptTemplateFile, "Test prompt template for smoke test."); + + // Store the config in a non-default location (not config/application.properties) + Path configFile = configDir.resolve("custom.properties"); + String validConfig = """ + source.folder=%s + target.folder=%s + sqlite.file=%s + ai.provider.active=openai-compatible + ai.provider.openai-compatible.baseUrl=http://localhost:8080/api + ai.provider.openai-compatible.model=gpt-4o-mini + ai.provider.openai-compatible.timeoutSeconds=30 + ai.provider.openai-compatible.apiKey=test-api-key-for-smoke-test + max.retries.transient=3 + max.pages=10 + max.text.characters=5000 + prompt.template.file=%s + runtime.lock.file=%s/lock.pid + log.directory=%s + log.level=INFO + """.formatted( + sourceDir.toAbsolutePath(), + targetDir.toAbsolutePath(), + sqliteFile.toAbsolutePath(), + promptTemplateFile.toAbsolutePath(), + workDir.toAbsolutePath(), + logsDir.toAbsolutePath() + ); + Files.writeString(configFile, validConfig); + + Path shadedJar = findShadedJar(); + + List command = new ArrayList<>(); + command.add(JAVA_EXECUTABLE); + command.add("-jar"); + command.add(shadedJar.toString()); + command.add("--headless"); + command.add("--config"); + command.add(configFile.toAbsolutePath().toString()); + + // Run in a fresh empty directory — no default config/application.properties present, + // so the process must load via the explicit --config path. + Path emptyWorkDir = Files.createDirectory(workDir.resolve("empty-cwd")); + ProcessBuilder pb = new ProcessBuilder(command); + pb.directory(emptyWorkDir.toFile()); + pb.redirectErrorStream(true); + + System.out.println("[SMOKE-TEST-EXPLICIT-CONFIG] Command: " + String.join(" ", command)); + + Process process = pb.start(); + boolean completed = process.waitFor(PROCESS_TIMEOUT_MS, java.util.concurrent.TimeUnit.MILLISECONDS); + byte[] outputBytes = process.getInputStream().readAllBytes(); + String outputText = new String(outputBytes); + + System.out.println("[SMOKE-TEST-EXPLICIT-CONFIG] Exit code: " + process.exitValue()); + System.out.println("[SMOKE-TEST-EXPLICIT-CONFIG] Output:\n" + outputText); + + assertTrue(completed, "Process should complete within timeout"); + assertEquals(0, process.exitValue(), + "Headless start with explicit valid --config path must exit 0. Output: " + outputText); + } + + // ========================================================================= + // Regression: headless start with non-existent --config path → hard failure + // ========================================================================= + + /** + * Regression test: {@code --headless --config } must be treated as + * a hard startup error and produce exit code 1. + *

+ * According to the startup semantics, a missing {@code --config} target in headless mode + * is never a silent fallback but always a configuration error that blocks the run. + * The output must contain a keyword that helps the operator diagnose the root cause. + */ + @Test + void jar_headlessWithNonExistentConfigPath_exitCode1(@TempDir Path workDir) throws Exception { + Path shadedJar = findShadedJar(); + + String missingPath = workDir.resolve("does-not-exist.properties").toAbsolutePath().toString(); + + List command = new ArrayList<>(); + command.add(JAVA_EXECUTABLE); + command.add("-jar"); + command.add(shadedJar.toString()); + command.add("--headless"); + command.add("--config"); + command.add(missingPath); + + ProcessBuilder pb = new ProcessBuilder(command); + pb.directory(workDir.toFile()); + pb.redirectErrorStream(true); + + System.out.println("[SMOKE-TEST-MISSING-CONFIG] Command: " + String.join(" ", command)); + + Process process = pb.start(); + boolean completed = process.waitFor(PROCESS_TIMEOUT_MS, java.util.concurrent.TimeUnit.MILLISECONDS); + byte[] outputBytes = process.getInputStream().readAllBytes(); + String outputText = new String(outputBytes); + + System.out.println("[SMOKE-TEST-MISSING-CONFIG] Exit code: " + process.exitValue()); + System.out.println("[SMOKE-TEST-MISSING-CONFIG] Output:\n" + outputText); + + assertTrue(completed, "Process should complete within timeout"); + assertEquals(1, process.exitValue(), + "Headless start with non-existent --config path must exit 1. Output: " + outputText); + + // Verify that the output contains a diagnostic keyword so operators can trace the cause. + // Only stable keywords are checked; exact message text may evolve. + assertTrue( + outputText.toLowerCase().contains("not found") + || outputText.toLowerCase().contains("does not exist") + || outputText.toLowerCase().contains("missing") + || outputText.toLowerCase().contains("error") + || outputText.toLowerCase().contains("config"), + "Output must contain a diagnostic keyword for the missing config file. Got: " + outputText + ); + } + + // ========================================================================= + // Regression: headless output must be free of JavaFX initialisation markers + // ========================================================================= + + /** + * Regression test verifying that a headless start does not trigger JavaFX initialisation. + *

+ * The headless path must not require a JavaFX runtime. This test runs the JAR in headless + * mode and checks that no JavaFX-specific strings appear in the combined stdout/stderr. + * Strings checked: "JavaFX", "Platform.startup", "Monocle", "javafx". + *

+ * This is a process-level complement to the unit-level proof in + * {@link BootstrapRunnerStartupDispatchTest#headlessStart_doesNotInvokeGuiAdapterFactory()}. + * That unit test proves the GuiAdapterFactory is never called; this test proves the + * observable JAR output is likewise free of JavaFX initialisation noise. + */ + @Test + void jar_headlessStart_outputFreeOfJavaFxInitialisationMarkers(@TempDir Path workDir) throws Exception { + Path configDir = Files.createDirectory(workDir.resolve("config")); + Path sourceDir = Files.createDirectory(workDir.resolve("source")); + Path targetDir = Files.createDirectory(workDir.resolve("target")); + Path logsDir = Files.createDirectory(workDir.resolve("logs")); + Path dbParent = Files.createDirectory(workDir.resolve("data")); + Path promptDir = Files.createDirectory(workDir.resolve("config/prompts")); + + Path sqliteFile = Files.createFile(dbParent.resolve("pdf-umbenenner.db")); + Path promptTemplateFile = Files.createFile(promptDir.resolve("template.txt")); + Files.writeString(promptTemplateFile, "Test prompt template for headless JavaFX-freedom check."); + + Path configFile = configDir.resolve("application.properties"); + String validConfig = """ + source.folder=%s + target.folder=%s + sqlite.file=%s + ai.provider.active=openai-compatible + ai.provider.openai-compatible.baseUrl=http://localhost:8080/api + ai.provider.openai-compatible.model=gpt-4o-mini + ai.provider.openai-compatible.timeoutSeconds=30 + ai.provider.openai-compatible.apiKey=test-api-key-javafx-check + max.retries.transient=3 + max.pages=10 + max.text.characters=5000 + prompt.template.file=%s + runtime.lock.file=%s/lock.pid + log.directory=%s + log.level=INFO + """.formatted( + sourceDir.toAbsolutePath(), + targetDir.toAbsolutePath(), + sqliteFile.toAbsolutePath(), + promptTemplateFile.toAbsolutePath(), + workDir.toAbsolutePath(), + logsDir.toAbsolutePath() + ); + Files.writeString(configFile, validConfig); + + Path shadedJar = findShadedJar(); + + List command = new ArrayList<>(); + command.add(JAVA_EXECUTABLE); + command.add("-jar"); + command.add(shadedJar.toString()); + command.add("--headless"); + + ProcessBuilder pb = new ProcessBuilder(command); + pb.directory(workDir.toFile()); + pb.redirectErrorStream(true); + + System.out.println("[SMOKE-TEST-JAVAFX-FREEDOM] Command: " + String.join(" ", command)); + + Process process = pb.start(); + boolean completed = process.waitFor(PROCESS_TIMEOUT_MS, java.util.concurrent.TimeUnit.MILLISECONDS); + byte[] outputBytes = process.getInputStream().readAllBytes(); + String outputText = new String(outputBytes); + + System.out.println("[SMOKE-TEST-JAVAFX-FREEDOM] Exit code: " + process.exitValue()); + System.out.println("[SMOKE-TEST-JAVAFX-FREEDOM] Output:\n" + outputText); + + assertTrue(completed, "Process should complete within timeout"); + assertEquals(0, process.exitValue(), + "Headless start must exit 0 for the JavaFX-freedom check to be meaningful. " + + "Output: " + outputText); + + // JavaFX initialisation would produce one of these markers in stdout/stderr. + // Their absence is the evidence that the headless path is JavaFX-free at runtime. + assertFalse( + outputText.contains("Platform.startup") + || outputText.contains("Monocle") + || outputText.contains("com.sun.javafx") + || outputText.contains("javafx.application"), + "Headless output must not contain JavaFX initialisation markers. Got:\n" + outputText + ); + } + + // ========================================================================= + // Shared helper: locate the shaded JAR + // ========================================================================= + + /** + * Locates the shaded executable JAR produced by the {@code maven-shade-plugin}. + *

+ * Searches in {@code pdf-umbenenner-bootstrap/target} relative to the Maven reactor root + * ({@code user.dir}) first, then falls back to the local {@code target/} directory. + * + * @return absolute path to the shaded JAR + * @throws AssertionError if no matching JAR can be found + */ + private Path findShadedJar() { + Path projectRoot = Paths.get(System.getProperty("user.dir")); + Path bootstrapTarget = projectRoot.resolve("pdf-umbenenner-bootstrap/target"); + if (!Files.exists(bootstrapTarget)) { + bootstrapTarget = Paths.get("target"); + } + + assertTrue(Files.exists(bootstrapTarget), + "Bootstrap target directory must exist: " + bootstrapTarget); + + File[] jars = bootstrapTarget.toFile().listFiles( + (dir, name) -> name.endsWith(".jar") + && !name.contains("original") + && !name.contains("tests")); + + assertNotNull(jars, "JAR files should exist in target directory"); + assertTrue(jars.length > 0, "At least one JAR should exist in target directory"); + + Path shadedJar = Paths.get(jars[0].getAbsolutePath()); + for (File jar : jars) { + if (jar.getName().contains("shaded") + || jar.getName().equals("pdf-umbenenner-bootstrap-0.0.1-SNAPSHOT.jar")) { + shadedJar = jar.toPath().toAbsolutePath(); + break; + } + } + + assertTrue(Files.exists(shadedJar), "Shaded JAR file must exist: " + shadedJar); + return shadedJar; + } } \ No newline at end of file