1263 lines
55 KiB
Markdown
1263 lines
55 KiB
Markdown
# 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: <konkrete deutsche Fehlermeldung>
|
||
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 = <konkrete deutsche Fehlermeldung>`
|
||
|
||
GUI im Scheduler-Tab:
|
||
|
||
```
|
||
⚠ Autostart fehlgeschlagen – Scheduler ist nicht aktiv.
|
||
Grund: <lastError>
|
||
|
||
[ 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<RunLockHandle> 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<RunLockHandle> 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<Instant> lastRunEndedAt,
|
||
Optional<RunSummary> lastRunSummary,
|
||
Optional<Instant> nextTickAt,
|
||
Optional<String> lastError,
|
||
boolean autostartFailed
|
||
) {}
|
||
```
|
||
|
||
Threadsicher veröffentlicht via `AtomicReference<SchedulerStatus>` 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<RunLockHandle> 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<SchedulerStatus>` 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 |
|