From 791499169fa9d11eec7f944c516cea531e3ffcd5 Mon Sep 17 00:00:00 2001 From: Marcus van Elst Date: Wed, 6 May 2026 12:11:39 +0200 Subject: [PATCH] =?UTF-8?q?Spezifikation=20f=C3=BCr=20V3.2=20hinzugef?= =?UTF-8?q?=C3=BCgt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/specs/V3_2_-_Spezifikation.md | 1262 ++++++++++++++++++++++++++++ 1 file changed, 1262 insertions(+) create mode 100644 docs/specs/V3_2_-_Spezifikation.md diff --git a/docs/specs/V3_2_-_Spezifikation.md b/docs/specs/V3_2_-_Spezifikation.md new file mode 100644 index 0000000..7ebdb69 --- /dev/null +++ b/docs/specs/V3_2_-_Spezifikation.md @@ -0,0 +1,1262 @@ +# V3.2 – Automatischer Scheduler + +**Status:** Zur Implementierung freigegeben +**Erstellt:** 2026-05-06 +**Überarbeitet:** 2026-05-06 (nach ChatGPT-Review Runden 1, 2 und 3; #74 herausgelöst) +**Autor:** Marcus (mit Claude als Mentor) + +--- + +## Ziel + +V3.2 bringt **#22 – Automatischer Scheduler**: Die Anwendung überwacht den +Quellordner dauerhaft im Hintergrund und startet die Verarbeitungspipeline +automatisch, sobald neue PDF-Dateien erkannt werden. Das ist der Übergang +vom manuellen Batch-Tool zur autonomen Dauerläufer-Anwendung. + +V3.2 ist eine **reine Scheduler-Veranstaltung**. Token- und Kosten-Tracking +(#74) wurde aus V3.2 herausgelöst und bekommt eine eigene saubere +Spezifikation in V3.x – inklusive Modell-Preistabelle, Persistenz-Strategie +und EUR-Währung. + +--- + +## Einordnung + +V3.1 ist der abgeschlossene Ausgangspunkt. Hexagonale Architektur, +Modulstruktur, headless-Betrieb, `.properties`-Konfigurationswahrheit +und Flyway-DB-Evolution bleiben als Grundprinzipien vollständig erhalten. + +### Kontrollierte Architekturausnahme (CLAUDE.md-Öffnung) + +CLAUDE.md enthält heute die Vorgaben **„keine Dauerlauf-Anwendung"** und +**„kein interner Scheduler"**. Diese Einschränkungen werden für V3.2 +**bewusst und kontrolliert aufgehoben**. Die Aufhebung ist begrenzt auf: + +- Das neue Modul `adapter-in-scheduler` mit `ScheduledExecutorService`-Polling +- Den neuen GUI-Tab „Scheduler" als Steuerungsoberfläche + +CLAUDE.md wird im Rahmen von V3.2 entsprechend aktualisiert. Die Aktualisierung +muss explizit enthalten: + +- Scheduler nur im GUI-Modus in V3.2 +- Kein Headless-Daemon +- Kein WatchService +- Kein #74 in V3.2 +- Keine DB-Migration in V3.2 +- Scheduler-Modul darf gemischte technische Treiber-/Adapter-Rolle haben +- Kein JavaFX im Scheduler-Modul +- Headless ignoriert Scheduler-Properties vollständig + +### Bootstrap-Refactoring (Voraussetzung für #22) + +Jeder Verarbeitungslauf führt heute die vollständige Bootstrap-Sequenz aus. +Für einen Scheduler der alle N Sekunden tickt ist das inakzeptabel teuer +und fragil. `BootstrapRunner` wird um eine saubere **Init/Run-Trennung** +erweitert. Details im gleichnamigen Abschnitt. + +### Headless-Betrieb + +Der bestehende headless-Pfad (`--headless`, `SchedulerBatchCommand`) bleibt +vollständig erhalten und unverändert. Der neue Scheduler läuft in V3.2 +**ausschließlich im GUI-Modus**. + +**Hinweis zum Begriff:** `SchedulerBatchCommand` bezeichnet den +**bestehenden** Headless-Batch-Command (für Windows Task Scheduler-Aufrufe). +Er ist **nicht** der neue automatische Scheduler. Der Name ist historisch +gewachsen und wird in V3.2 nicht geändert, um den Headless-Pfad +unverändert zu lassen. + +**Headless ignoriert Scheduler-Properties:** +Im Headless-Modus werden `scheduler.enabled` und `scheduler.interval.seconds` +**weder gelesen noch validiert**. Ungültige Scheduler-Properties (z.B. +`scheduler.enabled=maybe`) beeinflussen Headless-Exit-Code und -Verhalten +nicht. + +### Datenbankschema + +V3.2 enthält **keine Flyway-Migration**. Das DB-Schema bleibt unverändert +auf V1. Es entstehen keine neuen Spalten und keine neuen Tabellen. + +Flyway wird im Rahmen von `initializeApplicationContext()` weiterhin wie +bisher initialisiert/validiert; Flyway-Fehler verhindern die Erzeugung +des `ApplicationRunContext`, lassen aber die GUI-Shell starten. + +### Neues Maven-Modul + +`pdf-umbenenner-adapter-in-scheduler` + +--- + +## Scope + +### In V3.2 enthalten + +| # | Thema | Kategorie | +|---|---|---| +| #22 | Automatischer Scheduler / Quellordner-Überwachung | Hauptfeature | + +### Explizit nicht in V3.2 + +- **Token- und Kosten-Tracking (#74)** → V3.x als eigenes durchdachtes Feature +- Headless-Daemon-Betrieb des Schedulers (`--watch`-Flag) → V3.x +- Java WatchService (ereignisgesteuerte Ordnerüberwachung) → V3.x +- Windows-Service-Integration (WinSW o.ä.) → V3.x +- Modell-Preistabelle, EUR-Währung, Cache-Tokens → V3.x (siehe #74) +- Modell-Filterung (z.B. OpenAI-Snapshots ausblenden) → V3.x +- Dark Mode (#70) → V3.x +- F1-Hilfe (#69) → V3.x +- Log-Viewer in der GUI (#72) → V3.x +- Excel-Export (#75) → V3.x +- Automatische Update-Prüfung (#76) → V3.x +- PDF-Viewer Render-DPI (#23) → V3.x +- Neue KI-Provider, Architekturbrüche +- Änderung der fachlichen Kernverarbeitung +- Stabilitätsprüfung für noch kopierte PDFs (bestehende Retry-Semantik bleibt) + +--- + +## Unverrückbare Leitplanken + +- 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 +- **Kein JavaFX im neuen Modul `adapter-in-scheduler`** +- JavaDoc auf allen neuen öffentlichen Ports, Use-Cases, DTOs und öffentlichen + Adapter-Methoden; private Implementierungsdetails erhalten deutsche Kommentare + nur bei fachlicher Nicht-Offensichtlichkeit +- Notwendige Code-Kommentare auf Deutsch; Logging auf Deutsch +- Flyway ist die einzige Schema-Evolutionsquelle (in V3.2 keine Migration) + +--- + +## Bootstrap-Refactoring: Init/Run-Trennung und Fehlertoleranz + +### Problem + +Jeder GUI- oder Headless-Lauf führt heute die vollständige Bootstrap-Sequenz +erneut aus. Für einen Scheduler der alle N Sekunden tickt ist das teuer +und fragil. Außerdem fehlt heute eine saubere Trennung zwischen +„GUI bedienbar" und „Verarbeitungslauf möglich" – ein Konfigurationsfehler +beim Programmstart führt heute zu einem nicht reparierbaren Zustand. + +### Lösung: Zwei Kontexte + +V3.2 unterscheidet konzeptionell zwei Kontexte: + +**`GuiShellContext` – wird immer aufgebaut, soweit die GUI selbst geladen werden kann:** +- Erlaubt Konfiguration anzeigen, bearbeiten und speichern +- Enthält keine vollständig verdrahtete Verarbeitungspipeline +- Reine GUI-Infrastruktur (Tabs, Dialoge, Konfig-Tab) + +**`ApplicationRunContext` – wird nur bei gültiger Laufkonfiguration erzeugt:** +- Legacy-Migration prüfen und ggf. durchführen +- `.properties` laden und validieren (strukturelle Werte) +- Flyway-Schema initialisieren +- Adapter- und Use-Case-Objektgraph vollständig aufbauen +- `BatchRunTrigger`-Implementierung erzeugen +- Wird im Speicher gehalten und für alle Folgeläufe wiederverwendet + +### Aufbaureihenfolge beim Programmstart + +``` +1. GuiShellContext aufbauen → GUI startet, Konfig-Tab bedienbar +2. ApplicationRunContext-Erzeugung versuchen + → Erfolg: manuelle Läufe und Scheduler-Autostart möglich + → Fehler: GUI-Shell zeigt Reparatur-Banner; manuelle Läufe und + Scheduler deaktiviert; Konfig-Tab editierbar +``` + +### Fehlerbehandlung beim Init + +Scheitert die Erzeugung des `ApplicationRunContext` (ungültige strukturelle +Konfiguration, DB nicht erreichbar, Flyway-Fehler, ungültiger API-Key etc.): + +- GUI-Shell startet trotzdem +- Im Hauptfenster wird ein **Reparatur-Banner** eingeblendet: + ``` + ⚠ Anwendung ist nicht laufbereit – Konfiguration fehlerhaft. + Details: + Bitte Konfiguration korrigieren und Anwendung neu starten. + ``` +- Manueller Starten-Button im Batch-Tab: deaktiviert (Tooltip erklärt) +- Scheduler-Tab: deaktiviert (Tooltip erklärt) +- Konfig-Tab: vollständig editierbar – User kann Konfiguration reparieren +- Nach Konfig-Speichern: Hinweis dass ein Neustart erforderlich ist + +**Strukturelle Werte (DB-Pfad, Provider-URL, API-Key, Quellordner) +wirken erst nach Neustart der Anwendung.** Ein „Live-Reload" des +`ApplicationRunContext` ist nicht in V3.2. + +### Pro-Lauf-Phase + +`executeRun(ApplicationRunContext)` (pro Lauf, manuell oder Tick): +- Run-Lock nicht-blockierend erwerben (`RunLockPort.tryAcquire()`) +- Falls Lock nicht verfügbar: sofort abbrechen (kein Warten, kein Queuing) +- `BatchRunProcessingUseCase.execute(BatchRunContext)` aufrufen +- Run-Lock freigeben (in `finally`-Block) + +### Code-Analyse erforderlich + +Vor der Implementierung analysiert Claude Code den aktuellen +`BootstrapRunner`-Quellcode und dokumentiert verbindlich: +- Welche Initialisierungsschritte heute mit der Laufausführung vermengt sind +- Den konkreten Schnitt zwischen `GuiShellContext`, `ApplicationRunContext` + und `executeRun` +- Ob die Kontexte als neue Typen eingeführt werden oder bestehende + Kapselungen genutzt werden können + +**Wichtig:** Der bestehende GUI-Aufrufpfad +(`GuiBatchRunCoordinator` → `GuiBatchRunLauncher` → +`BootstrapRunner::launchGuiBatchRun`) bleibt für manuelle Läufe vollständig +funktionsfähig. Das Refactoring darf keinen bestehenden Test brechen. + +--- + +## #22 – Automatischer Scheduler + +### Fachliche Beschreibung + +Die Anwendung überwacht den konfigurierten Quellordner in regelmäßigen +Abständen (Polling) auf neue, unverarbeitete PDF-Dateien. Wird der +`BatchRunProcessingUseCase` getriggert und findet keine Kandidaten, ist +das ein gültiger No-op-Lauf – kein Fehlerstatus, kein neuer +`processing_attempt`-Eintrag. Findet er Kandidaten, läuft die bestehende +Verarbeitungspipeline vollständig durch. Der Nutzer steuert den Scheduler +ausschließlich über den neuen GUI-Tab „Scheduler". + +**Während aktivem Scheduler:** +- Manuelle Läufe sind verboten (Manuell-Starten-Button deaktiviert) +- Konfigurationsänderungen sind verboten (Konfig-Tab read-only, OS-Lock auf `.properties`) +- Scheduler-Intervall im Scheduler-Tab ist nicht editierbar + +**Mentales Modell:** Scheduler an = Autopilot. Während des Autopilot-Betriebs +sind manuelle Läufe und Konfigurationsänderungen gesperrt. + +**Fehlerklassifikation im Lauf:** + +| Fehlerart | Ergebnis | +|---|---| +| Quellordner nicht erreichbar (vor Kandidatenscan) | Whole-run failure; kein `processing_attempt`; `BatchRunTriggerResult.Failed`; WARN-Log; Scheduler läuft weiter | +| Einzelne PDF nicht lesbar (z.B. noch im Kopiervorgang) | Per-document failure; bestehende Retry-Semantik; ggf. `FAILED_RETRYABLE` | +| API-Fehler bei einzelner Datei | Per-document failure; bestehende Attempt-Persistenz | +| Technischer Fehler des gesamten Laufs | Whole-run failure; `BatchRunTriggerResult.Failed`; WARN-Log; Scheduler läuft weiter | + +**Unvollständige PDFs (noch im Kopiervorgang):** Für V3.2 keine +zusätzliche Stabilitätsprüfung. Bekannte Einschränkung: temporär +unvollständige Dateien können `FAILED_RETRYABLE` erzeugen, werden aber +beim nächsten Tick erneut versucht. + +### Technische Entscheidungen + +| Aspekt | Entscheidung | Begründung | +|---|---|---| +| Überwachungsmechanismus | Polling via `ScheduledExecutorService` | Deterministisch; zuverlässiger auf Windows-Netzlaufwerken als WatchService | +| Ausführungsstrategie | `scheduleWithFixedDelay` | Wartet N Sekunden nach Laufende; verhindert Tick-Stapelung | +| **Initial Delay** | **Null (erster Tick sofort nach Start)** | Sofortiges Nutzer-Feedback bei manuellem Start; bei Autostart ebenso schnell | +| WatchService | Nicht in V3.2 | Auf Windows-Netzlaufwerken unzuverlässig | +| Modularisierung | Eigenes Modul `adapter-in-scheduler` | Saubere Trennung; kein JavaFX | +| Headless-Daemon | Nicht in V3.2 | Bewusste Scope-Begrenzung | +| Laufkollision | Skip via nicht-blockierendem RunLock | `RunLockPort.tryAcquire()`; kein `check-then-act` | +| Manuelle Läufe bei aktivem Scheduler | Verboten | Vermeidet Nicht-Determinismus | +| Lauf-Trigger | Neutrales `BatchRunTrigger`-Interface mit Result-Objekt | Keine Direktaufrufe von `BootstrapRunner` oder `GuiBatchRunCoordinator` aus dem Scheduler-Modul | +| **Daemon-Thread** | **Non-Daemon** | Saubere Shutdown-Garantie; passt zu kontrolliertem App-Schließen | +| Config-Schutz | OS-Lock auf `.properties` während Scheduler läuft oder Lauf aktiv | Bestmöglicher Schreibschutz; nicht als absolute Garantie formuliert | +| Lock-Mechanismus | `tryLock()` als Retry-Schleife mit Deadline | `FileChannel.tryLock()` hat keinen Timeout-Parameter; Deadline wird selbst gebaut | +| GUI-Aktualisierung | Polling per JavaFX `Timeline` (1 Hz), zentral beim GUI-Aufbau gestartet | Aktualisiert Scheduler-Tab, Batch-Tab und Konfig-Tab unabhängig vom aktiven Tab | + +### Konfiguration (`.properties`) + +```properties +# Scheduler aktiv (true/false); Default: false; nur im GUI-Modus relevant +scheduler.enabled=false + +# Wartezeit in Sekunden nach Ende des letzten Laufs bis zum nächsten Tick; Minimum: 30 +scheduler.interval.seconds=180 +``` + +#### Validierung beim Laden (nur GUI-Modus) + +Im **GUI-Modus** validiert ein neuer **Scheduler-Settings-Validator** +diese Properties: + +| Property | Fehlend | Leer | Ungültig | Unter Minimum / negativ | +|---|---|---|---|---| +| `scheduler.enabled` | Default `false` | Default `false` | Scheduler-Settings-Fehler; Anzeige in GUI | n/a | +| `scheduler.interval.seconds` | Default `180` | Default `180` | Scheduler-Settings-Fehler; Anzeige in GUI | Scheduler-Settings-Fehler | + +**Wichtig – die Begriffsbildung ist bewusst:** + +`Scheduler-Settings-Fehler` ist **nicht** das gleiche wie ein +`ApplicationRunContext`-Fehler. Ein Scheduler-Settings-Fehler: +- blockiert nur Scheduler-Autostart und Scheduler-Start-Button +- blockiert **nicht** die GUI +- blockiert **nicht** manuelle Läufe +- blockiert **nicht** den Headless-Lauf +- wird im Scheduler-Tab und/oder als Startup-Hinweis angezeigt + +**Headless-Modus** liest, validiert und verwendet diese Properties nicht. + +#### Autostart-Verhalten (nur GUI-Modus) + +`scheduler.enabled=false` ist der Default. Startet der Nutzer den Scheduler +in der GUI, schreibt die Anwendung `scheduler.enabled=true` in `.properties`. +Beim nächsten Programmstart startet der Scheduler automatisch, **wenn**: + +- `scheduler.enabled=true` +- Scheduler-Properties valide +- `ApplicationRunContext` erfolgreich erzeugt + +Scheitert eine dieser Bedingungen, **startet der Scheduler nicht**, aber +`scheduler.enabled` bleibt `true`. Der Status erhält ein zusätzliches Flag +`autostartFailed=true`. Die GUI zeigt einen klaren Reparaturpfad +(siehe Abschnitt „Autostart-Fehlerzustand"). + +### Properties-Persistierung – `SchedulerSettingsPort` + +**Neuer Outbound-Port (Modul `application`):** + +```java +public interface SchedulerSettingsPort { + SchedulerSettings loadSettings(); + void saveEnabled(boolean enabled) throws SchedulerSettingsWriteException; + void saveIntervalSeconds(int seconds) throws SchedulerSettingsWriteException; +} +``` + +Die GUI ruft den Use-Case auf, der Use-Case delegiert an den Port. Kein +direktes `.properties`-Schreiben aus der GUI. + +**Format-Erhalt-Anforderung:** + +Der `SchedulerSettingsPort`-Adapter aktualisiert nur die zwei +Scheduler-Keys und erhält alle übrigen Zeilen, Kommentare und unbekannten +Properties unverändert. Reihenfolge und Zeilenenden bleiben stabil. +Bei Schreibfehler darf die Datei nicht in einem korrupten Zustand +zurückbleiben (atomarer Schreibvorgang über Temp-Datei). + +### Config-Datei-Sperre und Schreibmechanismus + +Solange der Scheduler läuft **oder** ein Lauf (manuell oder Tick) aktiv +ist, hält die Anwendung einen exklusiven OS-Lock auf die `.properties`-Datei +via `FileChannel.tryLock()` (Windows: `LockFileEx`). + +**Realistische Formulierung des OS-Locks:** + +Der Lock ist ein **bestmöglicher OS-Level-Schreibschutz**. Die GUI +betrachtet die Konfiguration während Lock als gesperrt. Externe +Schreibversuche durch Texteditoren scheitern in den unterstützten +Windows-Testfällen oder erzeugen einen klaren Konflikt. Eine absolute +Garantie gegen alle externen Schreibstrategien (Delete-Rename, +Editor-spezifische Save-Strategien etc.) wird nicht beansprucht. + +**Variante B aus Review – Lock-Adapter besitzt Schreibfähigkeit:** + +Der Lock-Adapter hält den `FileChannel` und stellt eigene +Schreiboperationen bereit. Während des Locks darf nur dieser Adapter +schreiben. + +**Neuer Outbound-Port `ConfigurationFileLockPort` (Modul `application`):** + +```java +public interface ConfigurationFileLockPort { + void acquireLock() throws ConfigurationFileLockException; + void releaseLock(); + boolean isLocked(); +} +``` + +**Implementierung `FileChannelConfigurationAccessAdapter`** (implementiert +sowohl `ConfigurationFileLockPort` als auch `SchedulerSettingsPort`, +Modul `adapter-in-scheduler` oder `bootstrap`): + +- Kennt den `.properties`-Pfad via Konstruktor-Injektion aus `ApplicationRunContext` +- `acquireLock()` ruft intern eine **Retry-Schleife** auf: + ``` + deadline = now + lockTimeoutMillis + while (now < deadline) { + try { + fileLock = channel.tryLock(); + if (fileLock != null) return; + } catch (OverlappingFileLockException e) { + // weiter probieren + } + sleep(100ms); + } + throw new ConfigurationFileLockException("..."); + ``` +- `FileChannel.lock()` (blockierend) wird **nicht** verwendet +- Ausgeführt im Worker-Thread, niemals auf JavaFX Application Thread +- Bei Lock-Konflikt (extern gesperrt, Netzlaufwerk hängt): deutsche + Fehlermeldung, `ConfigurationFileLockException` wird geworfen +- `releaseLock()` ist idempotent +- Der Adapter teilt sich den `FileChannel` zwischen Lock- und + Schreiboperation, damit Settings auch während aktivem Lock geschrieben + werden können + +**Lebenszyklus:** + +| Phase | Lock-Status | Wer setzt/freigibt | +|---|---|---| +| App-Start, kein Lauf, Scheduler aus | kein Lock | – | +| Manueller Lauf läuft | Lock aktiv (für Dauer des Laufs) | `GuiBatchRunCoordinator` | +| Manueller Lauf beendet | Lock freigegeben | `GuiBatchRunCoordinator` (in `finally`) | +| Scheduler gestartet | Lock aktiv (für gesamte Scheduler-Laufzeit) | `DefaultSchedulerControlUseCase.start()` | +| Scheduler-Tick läuft | Lock weiterhin aktiv | – (bleibt aktiv) | +| Scheduler gestoppt | Lock freigegeben | `DefaultSchedulerControlUseCase.stop()` | + +**Wichtig:** Der Config-Lock bei manuellen Läufen wird im +`GuiBatchRunCoordinator` gesetzt, **nicht** in `BootstrapRunner.executeRun()`. +Damit bleibt der Headless-Pfad vollständig unverändert. + +**Manueller Lauf bei extern gesperrter Config-Datei:** +Kann der Lock nicht erworben werden, startet der manuelle Lauf nicht. +GUI zeigt deutsche Meldung: +``` +Konfigurationsdatei ist gesperrt. Lauf wurde nicht gestartet. +``` + +**GUI-Verhalten während Lock (Konfig-Tab):** + +Ein Read-Only-Banner wird eingeblendet: + +``` +⚠ Konfiguration gesperrt – Scheduler läuft (oder Lauf aktiv). + Scheduler beenden bzw. Lauf abwarten um Änderungen vorzunehmen. +``` + +Zusätzlich ist der **Speichern-Button hart deaktiviert**. +Alle Eingabefelder im Konfig-Tab sind nicht editierbar +(`setEditable(false)` / `setDisable(true)`), bleiben aber sichtbar. + +### Dirty-State vor Scheduler-Start + +Hat der Konfig-Tab beim Klick auf „Scheduler starten" ungespeicherte +Änderungen (Dirty-State), ist der Start-Button **hart deaktiviert**. +Tooltip: „Bitte Konfiguration speichern oder Änderungen verwerfen." + +Diese Regel verhindert, dass der Scheduler mit alten Werten startet +während der Nutzer geänderte Werte vor sich sieht. + +### Manuelle Läufe bei aktivem Scheduler + +Der Manuell-Starten-Button im Batch-Tab ist bei aktivem Scheduler +(`SchedulerStatus.state ∉ {STOPPED}` oder `state == STARTING`) +**hart deaktiviert**. +Tooltip: „Manuelle Läufe sind während aktivem Scheduler nicht möglich." + +### Autostart-Fehlerzustand + +Wenn `scheduler.enabled=true`, der Autostart aber fehlschlägt +(Scheduler-Settings-Fehler oder `ApplicationRunContext`-Fehler): + +`SchedulerStatus` enthält: +- `state = STOPPED` +- `autostartFailed = true` +- `lastError = ` + +GUI im Scheduler-Tab: + +``` +⚠ Autostart fehlgeschlagen – Scheduler ist nicht aktiv. + Grund: + + [ Scheduler starten ] [ Autostart deaktivieren ] +``` + +| Button | Verhalten | +|---|---| +| „Scheduler starten" | Aktiv wenn `ApplicationRunContext` vorhanden und Konfig-Tab nicht dirty; setzt `autostartFailed=false` bei Erfolg | +| „Autostart deaktivieren" | Schreibt `scheduler.enabled=false` über `SchedulerSettingsPort`; setzt `autostartFailed=false` | + +So bleibt der Zustand jederzeit reparierbar. + +### App-Schließen bei aktivem Lauf oder Scheduler + +Beim Klick auf „Schließen" prüft die Anwendung: + +| Zustand | Verhalten | +|---|---| +| Kein Lauf aktiv, Scheduler `STOPPED` | Schließen sofort möglich | +| Manueller Lauf läuft | Hinweisdialog (siehe unten) | +| Scheduler aktiv (egal ob Tick aktiv oder nicht) | Hinweisdialog (siehe unten) | + +**Hinweisdialog:** + +``` +┌──────────────────────────────────────────┐ +│ Anwendung kann nicht beendet werden │ +├──────────────────────────────────────────┤ +│ Ein Lauf ist aktiv oder der Scheduler │ +│ läuft. Bitte beende den Scheduler bzw. │ +│ warte auf das Ende des Laufs. │ +│ │ +│ [ OK ] │ +└──────────────────────────────────────────┘ +``` + +Der Dialog hat **nur einen OK-Button**. Die Anwendung wird **nicht** +beendet. Der Nutzer behält die Kontrolle. + +### Neues Maven-Modul `pdf-umbenenner-adapter-in-scheduler` + +Das neue Modul wird in der Parent-`pom.xml` als Submodul registriert. + +**Architektonische Einordnung:** + +Das Modul ist ein **technischer Scheduler-Adapter mit gemischter +Treiber-/Infrastrukturrolle**. Es enthält keine fachliche Logik und kein +JavaFX. Die Vermischung ist begrenzt auf: + +- Inbound-Treiber-Rolle: `ScheduledExecutorService` ruft bei jedem Tick + `BatchRunTrigger.triggerRun()` auf +- Outbound-Adapter-Rollen: implementiert `SchedulerPort`, + `ConfigurationFileLockPort` und `SchedulerSettingsPort` + +**Verbindliche Maven-Konfiguration:** + +| Plugin / Abhängigkeit | Einstellung | Begründung | +|---|---|---| +| PIT Mutation Testing | **Explizit deaktiviert** | PIT läuft ausschließlich auf `domain` und `application` | +| Shade-Plugin | **Nicht aktiv** | Nur im Distributions-Modul | +| `flatten-maven-plugin` | **Aktiv** | CI-friendly `${revision}` | +| JavaFX-Abhängigkeiten | **Keine** | Kein JavaFX in `adapter-in-scheduler` | +| Checkstyle / weitere Quality-Plugins | Konsistent mit anderen Adapter-Modulen | + +Claude Code prüft bei der Implementierung alle durch die Parent-`pom.xml` +vererbten Plugins und dokumentiert verbindlich, welche explizit deaktiviert +oder aktiviert werden müssen. + +### Architektur + +#### Neue Komponenten (Übersicht) + +| Komponente | Typ | Modul | Zweck | +|---|---|---|---| +| `SchedulerControlUseCase` | Inbound-Port-Interface | `application` | `start()`, `stop()`, `getStatus(): SchedulerStatus` | +| `DefaultSchedulerControlUseCase` | Use-Case-Impl. | `application` | Scheduler-Zustand; Delegation; Lock-Lifecycle; Settings-Persistierung | +| `SchedulerPort` | Outbound-Port | `application` | `startScheduler(SchedulerConfig, BatchRunTrigger)`, `stopScheduler()` | +| `BatchRunTrigger` | Funktionales Interface | `application` | `triggerRun(): BatchRunTriggerResult` | +| `BatchRunTriggerResult` | Sealed Interface | `application` | `Started`, `SkippedBusy`, `Failed` | +| `SchedulerConfig` | Value Object | `application` | `intervalSeconds: int` | +| `SchedulerSettings` | DTO | `application` | `enabled: boolean`, `intervalSeconds: int` | +| `SchedulerStatus` | Immutable Value Object | `application` | `state`, `lastRunEndedAt`, `lastRunSummary`, `nextTickAt`, `lastError`, `autostartFailed` | +| `SchedulerState` | Enum | `application` | `STOPPED`, `STARTING`, `RUNNING_IDLE`, `RUNNING_BATCH_ACTIVE`, `STOPPING_BATCH_ACTIVE` | +| `ConfigurationFileLockPort` | Outbound-Port | `application` | `acquireLock()`, `releaseLock()`, `isLocked()` | +| `SchedulerSettingsPort` | Outbound-Port | `application` | `loadSettings()`, `saveEnabled()`, `saveIntervalSeconds()` | +| `RunLockPort.tryAcquire()` | Erweiterung | `application` | Bestehender Port; **neue** Methode `Optional tryAcquire()` | +| `RunLockHandle` | Interface | `application` | `AutoCloseable` für try-with-resources | +| `ScheduledExecutorServiceSchedulerAdapter` | Adapter | `adapter-in-scheduler` | Implementiert `SchedulerPort`; betreibt Single-Thread-Executor | +| `FileChannelConfigurationAccessAdapter` | Adapter | `adapter-in-scheduler` o. `bootstrap` | Implementiert `ConfigurationFileLockPort` und `SchedulerSettingsPort` | +| `GuiSchedulerControlPort` | Bridge-Interface | `adapter-in-gui` | Brücke: `GuiSchedulerTab` → `SchedulerControlUseCase` | +| `GuiSchedulerTab` | GUI-Komponente | `adapter-in-gui` | Neuer Tab: Steuerung, Status, Intervall | +| `GuiStatusRefreshTimeline` | GUI-Komponente | `adapter-in-gui` | Zentrale 1-Hz-Timeline; aktualisiert Scheduler-/Batch-/Konfig-Tab | + +#### `BatchRunTrigger` und Result-Objekt + +```java +// Modul: application +@FunctionalInterface +public interface BatchRunTrigger { + /** + * Löst synchron einen Verarbeitungslauf aus. + * + * Ist der RunLock nicht verfügbar (anderer Lauf läuft bereits), + * kehrt die Methode sofort mit {@link BatchRunTriggerResult.SkippedBusy} + * zurück, ohne einen neuen Lauf zu starten. + * + * Wird der Lauf gestartet, kehrt die Methode erst nach dessen + * vollständigem Abschluss zurück. Das Ergebnis enthält die finale + * {@link RunSummary} und den Endzeitpunkt. + * + * Tritt vor oder während des Laufs ein technischer Fehler auf + * (z.B. Quellordner nicht erreichbar), liefert die Methode + * {@link BatchRunTriggerResult.Failed} mit bereinigten Meldungen. + * Exceptions werden nicht propagiert und der Stacktrace wird im + * Adapter geloggt – nicht im Result-Objekt transportiert. + */ + BatchRunTriggerResult triggerRun(); +} + +public sealed interface BatchRunTriggerResult + permits Started, SkippedBusy, Failed { + + record Started(Instant endedAt, RunSummary summary) + implements BatchRunTriggerResult {} + + record SkippedBusy() implements BatchRunTriggerResult {} + + /** + * Whole-run failure. userMessage ist deutsche, GUI-taugliche Meldung; + * technicalMessage ist Detail für Diagnose. Stacktrace wurde bereits + * im Adapter geloggt und wird nicht im Result transportiert. + */ + record Failed(String userMessage, String technicalMessage) + implements BatchRunTriggerResult {} +} +``` + +`Throwable` wird **nicht** im Result-Objekt transportiert. Das vermeidet +technische Details im Application-Layer und potenzielles Stacktrace-Leak +in die GUI. + +Bootstrap implementiert `BatchRunTrigger` als Lambda und übergibt es beim +`SchedulerPort.startScheduler(...)`-Aufruf an den Scheduler-Adapter. +Der Adapter kennt weder `BootstrapRunner` noch `GuiBatchRunCoordinator`. + +#### `RunLockPort`-Erweiterung + +`RunLockPort` ist bereits vorhanden. V3.2 fügt eine neue Methode hinzu: + +```java +public interface RunLockPort { + // ... bestehende Methoden bleiben unverändert ... + + /** + * Versucht nicht-blockierend, den Run-Lock zu erwerben. + * Liefert ein Handle (für try-with-resources) bei Erfolg, + * sonst Optional.empty(). + */ + Optional tryAcquire(); +} + +public interface RunLockHandle extends AutoCloseable { + @Override + void close(); // gibt den Lock frei; idempotent +} +``` + +Der bestehende blockierende `acquire()` bleibt erhalten und wird vom +manuellen GUI-Lauf weiterhin verwendet (Verhalten unverändert). +Der Scheduler nutzt ausschließlich `tryAcquire()`. + +**Code-Analyse erforderlich:** Claude Code dokumentiert vor der +Implementierung die aktuelle `RunLockPort`-Signatur und schlägt einen +konkreten Erweiterungspunkt vor. + +#### `SchedulerStatus` – Immutable Snapshot + +```java +public record SchedulerStatus( + SchedulerState state, + Optional lastRunEndedAt, + Optional lastRunSummary, + Optional nextTickAt, + Optional lastError, + boolean autostartFailed +) {} +``` + +Threadsicher veröffentlicht via `AtomicReference` im +`DefaultSchedulerControlUseCase`. Snapshots werden atomar ausgetauscht. + +**`SchedulerState`-Werte:** + +| Wert | Bedeutung | +|---|---| +| `STOPPED` | Scheduler gestoppt | +| `STARTING` | Start in Vorbereitung – Lock-Erwerb läuft; manuelle Starts deterministisch gesperrt | +| `RUNNING_IDLE` | Scheduler aktiv, wartet auf nächsten Tick | +| `RUNNING_BATCH_ACTIVE` | Scheduler aktiv, Tick läuft gerade einen Batch | +| `STOPPING_BATCH_ACTIVE` | Stop angefordert, aber laufender Batch läuft noch zu Ende | + +**`nextTickAt`-Berechnung:** + +| Zustand | `nextTickAt` | +|---|---| +| `STOPPED`, `STARTING` | `Optional.empty()` | +| `RUNNING_IDLE` | `lastRunEndedAt + intervalSeconds` (oder `now + intervalSeconds` vor erstem Lauf, gilt aber dank Initial Delay 0 nur sehr kurz) | +| `RUNNING_BATCH_ACTIVE`, `STOPPING_BATCH_ACTIVE` | `Optional.empty()` | + +**`lastError`-Lebenszyklusregel:** + +| Ereignis | `lastError` | +|---|---| +| `Started` (regulärer oder No-op-Lauf) | gelöscht (`empty`) | +| `SkippedBusy` | unverändert | +| `Failed` | gesetzt mit `userMessage` | +| Scheduler-Start (Beginn) | gelöscht | +| Scheduler-Stop | unverändert (User soll letzten Fehler noch sehen können) | + +**No-op-Lauf:** +`RunSummary` mit `successCount=0, failedCount=0, skippedCount=0`. +GUI-Anzeige im Scheduler-Tab: **„Letzter Lauf: 14:23 – keine neuen Dokumente"** +(verbindlich; nicht „0 erfolgreich, 0 fehlgeschlagen, 0 übersprungen"). + +#### Aufrufpfad Scheduler-Tick + +``` +ScheduledExecutorService (initialDelay=0; danach intervalSeconds nach Laufende) + → ScheduledExecutorServiceSchedulerAdapter.onTick() + → batchRunTrigger.triggerRun() [synchron, blockiert bis Laufende] + → RunLockPort.tryAcquire() + → empty: → Result: SkippedBusy; DEBUG-Log + → Handle: BatchRunProcessingUseCase.execute(BatchRunContext) + (try-with-resources auf Handle) + → Result: Started(endedAt, runSummary) + → Exception: Stacktrace im Adapter loggen (ERROR); + → Result: Failed(userMessage, technicalMessage) + → Status.lastError aktualisieren + → WARN-Log + → SchedulerStatus atomar aktualisieren (lastRunEndedAt, lastRunSummary, + state, nextTickAt, lastError gemäß Regel) +``` + +#### Aufrufpfad Scheduler-Start + +``` +GuiSchedulerTab → GuiSchedulerControlPort + → DefaultSchedulerControlUseCase.start() + → Voraussetzungen prüfen: + - ApplicationRunContext vorhanden + - Konfig-Tab nicht dirty + - Kein manueller Lauf aktiv + - Sonst: Fehlermeldung an GUI; kein State-Wechsel + → State = STARTING (atomar) + → SchedulerSettingsPort.saveEnabled(true) + → bei Fehler: State zurück auf STOPPED; Fehlermeldung; Ende + → ConfigurationFileLockPort.acquireLock() [Worker-Thread, mit Retry-Deadline] + → bei Fehler: Rollback saveEnabled(false); + bei Rollback-Fehler ERROR-Log + kritischer GUI-Hinweis; + State zurück auf STOPPED; lastError gesetzt; autostartFailed unverändert + → SchedulerPort.startScheduler(config, trigger) + → bei Fehler: releaseLock(); Rollback saveEnabled(false); + State STOPPED; lastError gesetzt + → State = RUNNING_IDLE + → autostartFailed = false (Erfolg löscht das Flag) + → GUI: Banner einblenden; Button-Zustände aktualisieren +``` + +**Rollback-Fehler:** Schlägt das Zurücksetzen von `saveEnabled(false)` +nach einem Startfehler **selbst** fehl, bleibt Scheduler gestoppt. +GUI zeigt kritischen Hinweis: + +``` +Scheduler konnte nicht gestartet werden. +Zusätzlich konnte scheduler.enabled nicht zurückgesetzt werden. +Bitte Konfigurationsdatei prüfen. +``` + +Dieser Zustand wird ERROR-geloggt. + +#### Aufrufpfad Scheduler-Stop + +``` +GuiSchedulerTab → GuiSchedulerControlPort + → DefaultSchedulerControlUseCase.stop() + → State = STOPPING_BATCH_ACTIVE (falls Lauf aktiv) oder direkt STOPPED + → SchedulerPort.stopScheduler() [shutdown(); keine neuen Ticks] + → SchedulerSettingsPort.saveEnabled(false) [über lock-haltenden Adapter] + → ConfigurationFileLockPort.releaseLock() + → State = STOPPED (final, nach Batch-Ende) + → GUI: Banner ausblenden; Button-Zustände aktualisieren +``` + +**Idempotenz:** +- `start()` auf bereits laufendem Scheduler: no-op (kein Fehler) +- `stop()` auf bereits gestopptem Scheduler: no-op +- `releaseLock()` ohne aktiven Lock: no-op + +#### Laufkollision und Kollisionsvermeidung + +`RunLockPort` ist die **einzige und maßgebliche** Kollisionsinstanz. +Es gibt kein `check-then-act` über `isRunning()`-Methoden. +`tryAcquire()` ist atomar und race-condition-sicher. + +Da manuelle Läufe bei aktivem Scheduler verboten sind und der +`STARTING`-Zwischenzustand manuelle Starts deterministisch sperrt, +ist die einzige verbleibende Kollisionsquelle ein theoretisches Race +zwischen zwei Scheduler-Ticks. `scheduleWithFixedDelay` schließt das +praktisch aus, aber `RunLockPort` schützt dennoch. + +`SchedulerPort.isRunning()` (sofern beibehalten) wird **ausschließlich +adapterintern** genutzt; nicht von GUI oder Use Case. Die maßgebliche +Statusquelle für Außen ist `SchedulerControlUseCase.getStatus()`. + +#### Scheduler-Lifecycle: Details + +**Executor-Konfiguration:** + +| Aspekt | Wert | +|---|---| +| Thread-Name | `pdf-umbenenner-scheduler` | +| **Daemon** | **`false`** (Non-Daemon; saubere Shutdown-Kontrolle) | +| Pool-Größe | 1 (Single-Thread; verhindert parallele Ticks) | +| `UncaughtExceptionHandler` | gesetzt; loggt deutsch auf ERROR | + +**`scheduleWithFixedDelay` mit Initial Delay 0:** +Erster Tick startet sofort nach `startScheduler()`. Folgende Ticks +starten N Sekunden **nach Ende** des vorigen Ticks. + +**Stop während aktivem Lauf:** +„Stoppen" beendet nur die Planung weiterer Ticks. Der laufende Batch +läuft regulär zu Ende (`SchedulerState.STOPPING_BATCH_ACTIVE`). +Status im Tab: + +``` +○ Gestoppt – aktueller Lauf läuft noch +``` + +Nach Laufende: `SchedulerState.STOPPED`, Lock freigegeben, +Banner ausgeblendet. + +**Exception-Handling im Tick:** + +```java +// Prinzip im Tick-Callback des Scheduler-Adapters: +try { + BatchRunTriggerResult result = batchRunTrigger.triggerRun(); + statusUpdater.update(result); +} catch (Throwable t) { + log.error("Unbehandelte Exception im Scheduler-Tick – Scheduler läuft weiter", t); + statusUpdater.recordError(t.getMessage()); +} +``` + +Eine Exception darf den `ScheduledExecutorService` **niemals** dauerhaft +stoppen. + +**Shutdown beim App-Ende:** + +App-Schließen ist nur möglich wenn `state == STOPPED` und kein Lauf +aktiv (siehe „App-Schließen" oben). `Application.stop()` führt zur +Sicherheit nochmal aus: + +- `SchedulerPort.stopScheduler()` (idempotent) +- `awaitTermination(5, SECONDS)` als Sicherheitsnetz +- Alle Locks und FileChannels werden freigegeben + +### GUI: Neuer Tab „Scheduler" + +#### Steuerung und Status + +Der Scheduler-Tab enthält **ausschließlich** Steuerungs- und Statuselemente. + +| Element | Beschreibung | +|---|---| +| Status-Indikator | Farbiges Label gemäß `SchedulerState` | +| Autostart-Fehler-Banner | Sichtbar wenn `autostartFailed=true`; mit „Scheduler starten"-Button und „Autostart deaktivieren"-Button | +| „Scheduler starten"-Button | Aktiv nur wenn `STOPPED` UND `ApplicationRunContext` vorhanden UND Konfig-Tab nicht dirty UND kein Lauf aktiv | +| „Scheduler stoppen"-Button | Aktiv nur wenn `state` ∈ {`RUNNING_IDLE`, `RUNNING_BATCH_ACTIVE`} | +| Status während Auslauf | `○ Gestoppt – aktueller Lauf läuft noch` (`STOPPING_BATCH_ACTIVE`) | +| Nächster Tick in: | Countdown; nur sichtbar wenn `RUNNING_IDLE`; basiert auf `nextTickAt` | +| Hinweis bei aktivem Tick | „Lauf läuft – nächster Tick nach Abschluss + N Sekunden" | +| Letzter Lauf | Aus `lastRunSummary`; bei No-op: „keine neuen Dokumente" | +| Letzter Fehler | Aus `lastError`; nur sichtbar wenn nicht empty | +| Intervall (Sekunden) | Nur editierbar wenn `STOPPED` und Konfig-Tab nicht dirty; Minimum 30 s; sofort persistiert via `SchedulerSettingsPort.saveIntervalSeconds()` | + +#### Zentraler Status-Refresh + +Eine zentrale **`GuiStatusRefreshTimeline`** (JavaFX `Timeline`, 1 Hz) +wird **beim Aufbau der Haupt-GUI** (nicht erst beim Öffnen des +Scheduler-Tabs) gestartet. Sie aktualisiert: + +- Scheduler-Tab (alle Status-Felder) +- Batch-Tab (Manuell-Starten-Button-Zustand) +- Konfig-Tab (Lock-Banner und Speichern-Button) + +Beim App-Schließen wird die Timeline gestoppt. Mehrfacher Start wird +vermieden (Idempotenz auf Timeline-Ebene). + +#### Button-Zustandstabelle + +| Anwendungszustand | „Starten" | „Stoppen" | +|---|---|---| +| `STOPPED`, kein Lauf, Konfig nicht dirty, RunContext OK | ✅ Aktiv | ❌ | +| `STOPPED`, kein Lauf, Konfig dirty | ❌ (Tooltip) | ❌ | +| `STOPPED`, kein RunContext (Init-Fehler) | ❌ (Tooltip) | ❌ | +| `STOPPED`, manueller Lauf aktiv | ❌ | ❌ | +| `STARTING` | ❌ | ❌ | +| `RUNNING_IDLE` | ❌ | ✅ Aktiv | +| `RUNNING_BATCH_ACTIVE` | ❌ | ✅ Aktiv | +| `STOPPING_BATCH_ACTIVE` | ❌ | ❌ | + +#### Anpassung des Batch-Tabs + +Der Manuell-Starten-Button im Batch-Tab wird hart deaktiviert wenn: + +- `SchedulerStatus.state ≠ STOPPED` (Scheduler aktiv oder im Übergang), **oder** +- `ApplicationRunContext` nicht vorhanden (Init-Fehler) + +Tooltip erklärt den Grund. + +--- + +## Logging-Matrix + +| Ereignis | Level | +|---|---| +| Scheduler gestartet mit Intervall N | INFO | +| Scheduler gestoppt | INFO | +| Tick gestartet | DEBUG | +| Tick übersprungen wegen aktivem Lauf | DEBUG | +| No-op-Lauf abgeschlossen | INFO | +| Lauf erfolgreich abgeschlossen mit RunSummary | INFO | +| Tick-Fehler (z.B. Quellordner nicht erreichbar) | WARN | +| Unbehandelte Exception im Tick (Stacktrace) | ERROR | +| Config-Lock erworben | INFO | +| Config-Lock freigegeben | INFO | +| Config-Lock-Erwerb fehlgeschlagen | ERROR | +| Rollback nach Startfehler erfolgreich | WARN | +| Rollback nach Startfehler **fehlgeschlagen** | ERROR | +| Scheduler-Settings-Validierungsfehler beim Laden | ERROR | +| Scheduler-Autostart erfolgreich | INFO | +| Scheduler-Autostart fehlgeschlagen | ERROR | +| ApplicationRunContext-Erzeugung fehlgeschlagen | ERROR | + +Alle Log-Meldungen auf Deutsch. + +--- + +## Architektur-Zusammenfassung + +### Neues Maven-Modul + +| Modul | Zweck | +|---|---| +| `pdf-umbenenner-adapter-in-scheduler` | `ScheduledExecutorService`-Polling; Lifecycle-Steuerung; Config-Lock + Settings-Persistierung | + +### Neue Inbound-Port-Interfaces und Use-Cases + +| Komponente | Typ | Modul | Issue | +|---|---|---|---| +| `SchedulerControlUseCase` | Inbound-Port-Interface | `application` | #22 | +| `DefaultSchedulerControlUseCase` | Use-Case-Impl. | `application` | #22 | + +### Neue Outbound-Ports + +| Komponente | Modul | Issue | +|---|---|---| +| `SchedulerPort` | `application` | #22 | +| `ConfigurationFileLockPort` | `application` | #22 | +| `SchedulerSettingsPort` | `application` | #22 | + +### Erweiterte Outbound-Ports + +| Komponente | Modul | Erweiterung | Issue | +|---|---|---|---| +| `RunLockPort` | `application` | Neue Methode `Optional tryAcquire()` | #22 | + +### Neue Funktionale Interfaces und Result-Typen + +| Komponente | Modul | Issue | +|---|---|---| +| `BatchRunTrigger` | `application` | #22 | +| `BatchRunTriggerResult` (sealed: `Started`, `SkippedBusy`, `Failed`) | `application` | #22 | + +### Neue Bridge-Interfaces (adapter-in-gui) + +| Interface | Issue | +|---|---| +| `GuiSchedulerControlPort` | #22 | + +### Neue Adapter + +| Adapter | Modul | Issue | +|---|---|---| +| `ScheduledExecutorServiceSchedulerAdapter` | `adapter-in-scheduler` | #22 | +| `FileChannelConfigurationAccessAdapter` | `adapter-in-scheduler` o. `bootstrap` | #22 | + +### Neue Application-Typen + +| Typ | Modul | Issue | +|---|---|---| +| `SchedulerConfig` | `application` | #22 | +| `SchedulerStatus` | `application` | #22 | +| `SchedulerState` | `application` | #22 | +| `SchedulerSettings` | `application` | #22 | +| `RunLockHandle` | `application` | #22 | + +### Geänderte Komponenten + +| Typ | Modul | Änderung | Issue | +|---|---|---|---| +| `BootstrapRunner` | `bootstrap` | Trennung in `GuiShellContext`-Init und `ApplicationRunContext`-Init; `executeRun()` nutzt vorhandenen RunContext; `BatchRunTrigger`-Erzeugung; Fehlertoleranz beim Init | #22 | +| `RunLockPort` | `application` | Neue Methode `tryAcquire()`; bestehende Methoden unverändert | #22 | +| `GuiBatchRunCoordinator` | `adapter-in-gui` | Config-Lock vor Worker-Start; Freigabe in `finally`; Manuell-Starten-Button bei aktivem Scheduler / fehlendem RunContext deaktiviert | #22 | +| `GuiConfigTab` | `adapter-in-gui` | Read-Only-Banner + Speichern-Button deaktiviert bei aktivem Lock; Reparatur-Banner bei fehlendem RunContext | #22 | +| `GuiSchedulerTab` | `adapter-in-gui` | Komplett neu | #22 | +| Haupt-GUI (Stage/Application) | `adapter-in-gui` | `GuiStatusRefreshTimeline` wird zentral gestartet; App-Schließen-Hook | #22 | + +### Nicht geändert + +- Fachliche Kernverarbeitung (PDF lesen → KI → umbenennen) +- `pdf-umbenenner-domain` – keine Änderungen +- `pdf-umbenenner-adapter-in-cli` – headless-Pfad vollständig unberührt +- `V1__initial_schema.sql` – keine Migration in V3.2 +- Provider-Adapter (`AnthropicClaudeHttpAdapter`, `OpenAiHttpAdapter`) +- Status-Mapping (`ProcessingStatusPresentation`) +- Retry-Semantik, Status-Persistenz, fachliche Verarbeitungslogik +- `SchedulerBatchCommand` (bestehender Headless-Command, nicht der neue Scheduler) + +--- + +## Datenbankmigrationen + +**Keine.** V3.2 enthält keine Flyway-Migration. Schema bleibt auf V1. + +--- + +## Definition of Done (V3.2 gesamt) + +### Scope-Sicherung + +- [ ] Spezifikation enthält keine umsetzungsrelevanten #74-Reste: + keine Token-Typen, keine Kostenfelder, keine #74-DoD-Punkte +- [ ] Keine neue Flyway-Migration in V3.2; insbesondere keine `V2__add_token_usage.sql` +- [ ] `V1__initial_schema.sql` bleibt unverändert +- [ ] DB-Repository-Adapter bleiben unverändert (außer durch bestehende Tests bedingt) + +### Build und Module + +- [ ] `mvn clean verify` grün (alle Module, kein `-DskipTests`) +- [ ] `mvn clean install -Drevision=3.2.0` – Build ohne Fehler +- [ ] Neues Modul `pdf-umbenenner-adapter-in-scheduler` in Parent-pom registriert +- [ ] PIT im neuen Modul explizit deaktiviert +- [ ] Kein JavaFX-Import im neuen Modul +- [ ] `flatten-maven-plugin` im neuen Modul aktiv +- [ ] Alle vererbten Parent-Plugins dokumentiert und ggf. deaktiviert +- [ ] CLAUDE.md aktualisiert (Scheduler-Ausnahme + alle Scope-Grenzen) + +### Bootstrap-Refactoring + +- [ ] Code-Analyse: Init/Run-Schnitt vor Implementierung dokumentiert +- [ ] `GuiShellContext` und `ApplicationRunContext` klar getrennt (oder äquivalente Trennung) +- [ ] `BatchRunTrigger`-Implementierung in Bootstrap erzeugt und injiziert +- [ ] Manueller Lauf via `GuiBatchRunCoordinator` weiterhin vollständig funktionsfähig +- [ ] Kein erneutes `.properties`-Laden oder Adapter-Wiring beim Tick +- [ ] Alle bestehenden Tests ohne Regressionsbruch + +### ApplicationContext / GUI-Start + +- [ ] GUI bleibt bei fehlschlagender `ApplicationRunContext`-Init bedienbar +- [ ] Bei ungültiger Laufkonfiguration sind manuelle Läufe und Scheduler deaktiviert; Konfigurationsbearbeitung bleibt möglich +- [ ] Reparatur-Banner mit konkreter deutscher Fehlermeldung sichtbar +- [ ] Hinweis nach Konfig-Speichern: Neustart erforderlich + +### Headless-Regression + +- [ ] Headless-Lauf mit gültiger Konfiguration funktioniert unverändert +- [ ] Headless-Lauf liest, validiert und verwendet keine Scheduler-Properties +- [ ] Ungültige Scheduler-Properties (`scheduler.enabled=maybe`, `scheduler.interval.seconds=abc`) brechen Headless nicht +- [ ] Headless-Lauf verwendet keinen Scheduler-Codepfad +- [ ] Headless-Lauf mit ungültiger struktureller Konfiguration liefert weiterhin korrekten Exit-Code +- [ ] Manueller GUI-Lauf-Pfad mit Config-Lock berührt Headless nicht + +### Scheduler-Architektur + +- [ ] `SchedulerControlUseCase`, `DefaultSchedulerControlUseCase` vorhanden +- [ ] `SchedulerPort` mit `startScheduler(SchedulerConfig, BatchRunTrigger)` vorhanden +- [ ] `SchedulerSettingsPort` vorhanden; Format-/Kommentar-/Reihenfolge-Erhalt sichergestellt +- [ ] `ConfigurationFileLockPort` vorhanden +- [ ] `RunLockPort.tryAcquire()` als Erweiterung implementiert; bestehende Methoden unverändert +- [ ] `BatchRunTrigger` mit Result-Objekt (`Started`, `SkippedBusy`, `Failed`) +- [ ] `BatchRunTriggerResult.Failed` ohne `Throwable`; nur `userMessage` + `technicalMessage`; Stacktrace im Adapter geloggt +- [ ] `BatchRunTrigger`-JavaDoc semantisch korrekt (synchron bei Start, sofort bei Busy) +- [ ] Kein Direktaufruf von `BootstrapRunner` oder `GuiBatchRunCoordinator` aus `adapter-in-scheduler` + +### Scheduler-Verhalten + +- [ ] `scheduleWithFixedDelay` verwendet (nicht `scheduleAtFixedRate`) +- [ ] Initial Delay = 0 (erster Tick sofort nach Start) +- [ ] Executor: Thread-Name `pdf-umbenenner-scheduler`, **Non-Daemon**, Single-Thread +- [ ] `UncaughtExceptionHandler` gesetzt; loggt deutsch auf ERROR +- [ ] Exception im Tick: gefangen; ERROR-Log mit Stacktrace; Executor läuft weiter +- [ ] `start()` idempotent (no-op bei bereits laufendem Scheduler) +- [ ] `stop()` idempotent +- [ ] Shutdown beim App-Ende: `awaitTermination(5, SECONDS)` als Sicherheitsnetz +- [ ] Laufkollision via nicht-blockierendem `RunLockPort.tryAcquire()`; kein `check-then-act` +- [ ] Quellordner-Fehler im Tick beendet Scheduler nicht; WARN-Log; nächster Tick läuft +- [ ] No-op-Lauf: `lastRunEndedAt` aktualisiert; `RunSummary` mit Nullzählern; kein Fehlerstatus +- [ ] No-op-Lauf 0/0/0 wird im Scheduler-Tab als „keine neuen Dokumente" dargestellt +- [ ] Whole-run failure (z.B. Quellordner) erzeugt **keinen** `processing_attempt` +- [ ] Per-document failure folgt bestehender Retry-Semantik +- [ ] Während Scheduler-Start/-Stop-Übergängen (`STARTING`, `STOPPING_BATCH_ACTIVE`) sind manuelle Starts deterministisch gesperrt + +### `SchedulerStatus` und Threading + +- [ ] `SchedulerStatus` als immutable Record mit `autostartFailed`-Flag +- [ ] `AtomicReference` als Single-Writer-Mechanismus +- [ ] `SchedulerState` mit allen 5 Werten implementiert (inkl. `STARTING`) +- [ ] `nextTickAt` korrekt: empty bei `STOPPED`/`STARTING`/`RUNNING_BATCH_ACTIVE`/`STOPPING_BATCH_ACTIVE` +- [ ] `lastError` gemäß Lebenszyklusregel gesetzt/gelöscht +- [ ] GUI-Update via zentraler `GuiStatusRefreshTimeline` (1 Hz) auf JavaFX Application Thread +- [ ] Timeline beim GUI-Aufbau gestartet, nicht erst beim Tab-Öffnen +- [ ] Timeline aktualisiert Scheduler-/Batch-/Konfig-Tab unabhängig vom aktiven Tab +- [ ] Timeline beim App-Schließen gestoppt; idempotent + +### Config-Lock und Settings + +- [ ] `FileChannelConfigurationAccessAdapter` implementiert beide Ports und teilt sich `FileChannel` +- [ ] Lock-Erwerb als Retry-Schleife mit `tryLock()` + Deadline; `FileChannel.lock()` wird **nicht** verwendet +- [ ] Lock-Erwerb läuft im Worker-Thread, niemals auf JavaFX Application Thread +- [ ] Lock-Konflikt: deutsche Fehlermeldung; `ConfigurationFileLockException` +- [ ] Lock aktiv während Scheduler läuft UND während eines Laufs +- [ ] Manueller Lauf: Lock in `GuiBatchRunCoordinator` gesetzt (nicht in `BootstrapRunner.executeRun`) +- [ ] Manueller Lauf bei extern gesperrter Datei: deutsche Meldung; Lauf startet nicht +- [ ] Lock-Freigabe in `finally`-Block; kein Lock-Leak bei Exception +- [ ] `releaseLock()` idempotent +- [ ] Settings-Schreiben funktioniert ohne und mit aktivem Lock +- [ ] `SchedulerSettingsPort` aktualisiert nur die zwei Scheduler-Keys; Kommentare/unbekannte Properties bleiben erhalten +- [ ] Atomarer Schreibvorgang über Temp-Datei; keine Korruption bei Schreibfehler +- [ ] OS-Lock realistisch beschrieben (kein „technisch ausgeschlossen") +- [ ] Start-Rollback bei Fehler: `enabled` zurückgesetzt; Lock freigegeben +- [ ] Rollback-Fehler: kritischer GUI-Hinweis; ERROR-Log; Property-Zustand nicht stillschweigend angenommen +- [ ] Stop-Sequenz: Scheduler stoppen → `enabled=false` schreiben → Lock freigeben (in dieser Reihenfolge) + +### Properties-Validierung (nur GUI-Modus) + +- [ ] `scheduler.enabled` ungültig (`maybe`, etc.): Scheduler-Settings-Fehler; deutsche Meldung; Scheduler startet nicht; GUI bleibt bedienbar; manuelle Läufe möglich +- [ ] `scheduler.interval.seconds` ungültig (`abc`, negativ, < 30): Scheduler-Settings-Fehler; deutsche Meldung; Scheduler startet nicht +- [ ] Fehlende Keys: Defaults `false` / `180` +- [ ] Validierungsfehler im Startup-Dialog angezeigt; Anwendung läuft weiter +- [ ] Headless ist von Scheduler-Settings-Fehlern nicht betroffen + +### Autostart + +- [ ] `scheduler.enabled=true` + valide Properties + erfolgreicher RunContext: Scheduler startet automatisch +- [ ] `scheduler.enabled=true` + ungültige Properties: kein Start; deutsche Fehlermeldung; `enabled` bleibt `true`; `autostartFailed=true` +- [ ] `scheduler.enabled=true` + RunContext-Fehler: kein Start; deutsche Meldung; `autostartFailed=true` +- [ ] Bei `autostartFailed=true`: GUI zeigt „Scheduler starten" und „Autostart deaktivieren"-Buttons +- [ ] „Autostart deaktivieren"-Button setzt `scheduler.enabled=false` und löscht das Flag +- [ ] „Scheduler starten" bei `autostartFailed`: bei Erfolg wird Flag gelöscht + +### GUI – Scheduler-Tab + +- [ ] Status-Indikator entsprechend `SchedulerState` +- [ ] Autostart-Fehler-Banner mit Reparatur-Buttons sichtbar wenn `autostartFailed=true` +- [ ] Start-Button-Logik korrekt (alle Zustände der Tabelle) +- [ ] Stop-Button-Logik korrekt +- [ ] Intervall-Feld: nur editierbar wenn `STOPPED` und Konfig-Tab nicht dirty +- [ ] Intervall-Validierung: Minimum 30 s; bei Fokusverlust geprüft +- [ ] Intervall-Änderung wird sofort persistiert +- [ ] Countdown bei `RUNNING_IDLE` sichtbar +- [ ] Hinweis bei `RUNNING_BATCH_ACTIVE` sichtbar +- [ ] Letzter Lauf: Zeitpunkt + Kurzergebnis +- [ ] Letzter Fehler: nur sichtbar wenn `lastError` gesetzt +- [ ] No-op-Anzeige: „keine neuen Dokumente" (nicht generisch 0/0/0) + +### GUI – Konfig-Tab + +- [ ] Read-Only-Banner während Lock aktiv +- [ ] Speichern-Button hart deaktiviert während Lock aktiv +- [ ] Eingabefelder nicht editierbar während Lock aktiv +- [ ] Reparatur-Banner bei `ApplicationRunContext`-Fehler +- [ ] Banner und Sperren werden korrekt aufgehoben nach Lock-Freigabe + +### GUI – Batch-Tab + +- [ ] Manuell-Starten-Button bei aktivem Scheduler hart deaktiviert +- [ ] Manuell-Starten-Button bei fehlendem `ApplicationRunContext` hart deaktiviert +- [ ] Tooltip erklärt jeweils warum + +### App-Schließen + +- [ ] Bei aktivem Lauf oder aktivem Scheduler: Hinweisdialog mit nur OK-Button +- [ ] App schließt nicht solange Scheduler läuft oder Lauf aktiv + +### Doku und Abschluss + +- [ ] Logging-Matrix vollständig umgesetzt +- [ ] Code-Kommentare auf Deutsch; Logging auf Deutsch +- [ ] JavaDoc auf neuen öffentlichen Ports, Use-Cases, DTOs und öffentlichen Adapter-Methoden +- [ ] `betrieb.md` und `gui-bedienanleitung.md` auf V3.2-Stand +- [ ] `freigabe-v3_2.md` erstellt +- [ ] Manueller GUI-Produkttest gemäß Produkttest-Matrix abgeschlossen + +--- + +## Produkttest-Matrix + +### Scheduler-Lifecycle + +| Testfall | Erwartung | +|---|---| +| App startet mit `scheduler.enabled=false` | Scheduler bleibt gestoppt; GUI normal | +| App startet mit `scheduler.enabled=true` und gültiger Konfig | Scheduler startet automatisch (erster Tick sofort); Konfig-Tab read-only mit Banner | +| App startet mit `scheduler.enabled=true` und ungültigem Intervall | Scheduler startet nicht; deutsche Fehlermeldung; `enabled` unverändert; `autostartFailed=true`; GUI bietet Reparaturbuttons | +| App startet mit `scheduler.enabled=maybe` | Scheduler startet nicht; deutsche Fehlermeldung; `autostartFailed=true` | +| App startet mit ungültiger struktureller Konfig (z.B. DB-Pfad falsch) | GUI startet; Konfig-Tab editierbar; Läufe und Scheduler deaktiviert; Reparatur-Banner sichtbar | +| Erster Tick nach manuellem Start | Tick läuft sofort, nicht erst nach Intervall | + +### Manueller Betrieb + +| Testfall | Erwartung | +|---|---| +| Manueller Start bei leerem Quellordner und Scheduler aus | Lauf läuft; No-op; Konfig während Lauf gesperrt | +| Manueller Start bei aktivem Scheduler | Button deaktiviert; Tooltip erklärt | +| Manueller Start bei fehlendem RunContext | Button deaktiviert; Tooltip erklärt | +| Manueller GUI-Lauf bei extern gesperrter Konfig-Datei | Lauf startet nicht; deutsche Meldung; kein GUI-Freeze | +| Headless-Lauf nach Scheduler-Konfiguration | Funktioniert unverändert; ignoriert `scheduler.*` | +| Headless-Lauf mit `scheduler.enabled=maybe` | Funktioniert unverändert; ignoriert ungültige Scheduler-Werte | + +### Scheduler im Betrieb + +| Testfall | Erwartung | +|---|---| +| Scheduler-Tick bei leerem Quellordner | No-op-Lauf; Anzeige „keine neuen Dokumente" | +| Scheduler-Tick bei neuen PDFs | Verarbeitung läuft; Banner aktualisiert nach Laufende | +| Scheduler-Tick während externer API-Fehler | WARN-Log; Scheduler läuft weiter; nächster Tick planmäßig | +| Scheduler-Tick bei nicht erreichbarem Quellordner | Whole-run failure; kein `processing_attempt`; WARN-Log; Scheduler läuft weiter | +| Stop während Scheduler-Lauf | `STOPPING_BATCH_ACTIVE`; Batch läuft zu Ende; danach `STOPPED` | +| Scheduler stoppen, dann sofort wieder starten | Funktioniert sauber; keine Lock-Leaks | + +### App-Schließen und Stabilität + +| Testfall | Erwartung | +|---|---| +| App schließen bei `RUNNING_IDLE` | Hinweisdialog OK; App nicht beendet | +| App schließen bei `RUNNING_BATCH_ACTIVE` | Hinweisdialog OK; App nicht beendet | +| App schließen bei `STOPPED` und kein Lauf | App schließt; alle Locks freigegeben | +| Mehrfacher Start/Stop in Serie | Idempotent; keine Lock-Leaks | +| Langlauftest (30+ Min, 30 s Intervall) | Kein Thread-Leak, kein Lock-Leak, keine dauerhaft hohe CPU-Last | + +### Konfiguration und Lock + +| Testfall | Erwartung | +|---|---| +| Start-Versuch bei dirty Konfig-Tab | Start-Button deaktiviert; Tooltip erklärt | +| Externen Editor öffnen während Scheduler läuft | Schreibversuch verweigert oder klarer Konflikt | +| Externen Editor öffnen während kein Scheduler/Lauf | Bearbeitung möglich (kein Lock) | +| Intervall im GUI auf `10` ändern | Validierungsfehler; Wert nicht übernommen | +| Konfig-Datei während Scheduler-Betrieb in externem Editor speichern | Schreibversuch scheitert oder klarer Konflikt | + +### Fehlerpfade + +| Testfall | Erwartung | +|---|---| +| Scheduler-Start: `saveEnabled(true)` erfolgreich, danach Lock-Fehler | Rollback erfolgreich; deutsche Fehlermeldung; Scheduler `STOPPED` | +| Scheduler-Start: `saveEnabled(true)` erfolgreich, Lock-Fehler **und** Rollback-Fehler | Kritischer GUI-Hinweis; ERROR-Log; Scheduler `STOPPED` | +| Autostart fehlgeschlagen, User klickt „Autostart deaktivieren" | `scheduler.enabled=false` geschrieben; `autostartFailed=false` | +| Autostart fehlgeschlagen, User klickt „Scheduler starten" und es klappt | Scheduler läuft; `autostartFailed=false` | + +### PDF-Stabilität + +| Testfall | Erwartung | +|---|---| +| Langsam kopierte PDF (Stabilitätstest) | Scheduler bleibt stabil; bekannte Einschränkung: temporäre `FAILED_RETRYABLE` möglich | +| Lange laufende Verarbeitung (5 min Batch) | Scheduler-Status korrekt; nächster Tick erst nach Ende + Intervall | + +--- + +## Empfohlene Unit-/Integrationstests + +| Testklasse | Schwerpunkte | +|---|---| +| `ScheduledExecutorServiceSchedulerAdapterTest` | Initial Delay 0; fixed delay; Exception bleibt lokal; Idempotenz; Non-Daemon; kein JavaFX | +| `DefaultSchedulerControlUseCaseTest` | Lock-Lifecycle; Rollback bei Startfehler; Rollback-Fehler; Dirty-Config-Block; `STARTING`-Übergang | +| `FileChannelConfigurationAccessAdapterTest` | Acquire/Release; idempotent; Lock-Konflikt; Schreiben mit/ohne Lock; Format-/Kommentar-Erhalt | +| `SchedulerStatusTest` | Immutability; Thread-Safety via `AtomicReference`; `lastError`-Lebenszyklusregel | +| `BatchRunTriggerResultTest` | Sealed-Hierarchie; `Failed` ohne `Throwable`; Pattern-Matching | +| `RunLockPortTryAcquireTest` | Nicht-blockierend; try-with-resources; idempotente Freigabe | +| `BootstrapRunnerInitRunSeparationTest` | `GuiShellContext` startet bei ungültiger Konfig; `ApplicationRunContext` korrekt erzeugt; Init nur einmal; bestehende Tests grün | +| `SchedulerSettingsValidationTest` | `enabled=maybe`, `interval=abc`, `interval=10`, fehlende Keys; Headless ignoriert | +| `SchedulerInitialDelayTest` | Erster Tick sofort | +| `GuiSchedulerAutostartFailureTest` | Autostart-Fehler-Pfad; `autostartFailed`-Flag; Reparatur-Buttons | +| `ManualRunConfigLockTest` | Manueller Lauf in `GuiBatchRunCoordinator` setzt Lock; Headless tut das nicht | +| `GuiStatusRefreshTimelineTest` | Zentraler Start beim GUI-Aufbau; aktualisiert alle drei Tabs |