55 KiB
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-schedulermitScheduledExecutorService-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,
--headlessbleibt vollständig erhalten .propertiesbleibt 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
.propertiesladen 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,ApplicationRunContextundexecuteRun - 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)
# 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
ApplicationRunContexterfolgreich 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):
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):
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 ausApplicationRunContext 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,
ConfigurationFileLockExceptionwird geworfen releaseLock()ist idempotent- Der Adapter teilt sich den
FileChannelzwischen 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 = STOPPEDautostartFailed = truelastError = <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:
ScheduledExecutorServiceruft bei jedem TickBatchRunTrigger.triggerRun()auf - Outbound-Adapter-Rollen: implementiert
SchedulerPort,ConfigurationFileLockPortundSchedulerSettingsPort
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
// 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:
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
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-opreleaseLock()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:
// 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), oderApplicationRunContextnicht 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 Änderungenpdf-umbenenner-adapter-in-cli– headless-Pfad vollständig unberührtV1__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.sqlbleibt unverändert- DB-Repository-Adapter bleiben unverändert (außer durch bestehende Tests bedingt)
Build und Module
mvn clean verifygrün (alle Module, kein-DskipTests)mvn clean install -Drevision=3.2.0– Build ohne Fehler- Neues Modul
pdf-umbenenner-adapter-in-schedulerin Parent-pom registriert - PIT im neuen Modul explizit deaktiviert
- Kein JavaFX-Import im neuen Modul
flatten-maven-pluginim 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
GuiShellContextundApplicationRunContextklar getrennt (oder äquivalente Trennung)BatchRunTrigger-Implementierung in Bootstrap erzeugt und injiziert- Manueller Lauf via
GuiBatchRunCoordinatorweiterhin 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,DefaultSchedulerControlUseCasevorhandenSchedulerPortmitstartScheduler(SchedulerConfig, BatchRunTrigger)vorhandenSchedulerSettingsPortvorhanden; Format-/Kommentar-/Reihenfolge-Erhalt sichergestelltConfigurationFileLockPortvorhandenRunLockPort.tryAcquire()als Erweiterung implementiert; bestehende Methoden unverändertBatchRunTriggermit Result-Objekt (Started,SkippedBusy,Failed)BatchRunTriggerResult.FailedohneThrowable; nuruserMessage+technicalMessage; Stacktrace im Adapter geloggtBatchRunTrigger-JavaDoc semantisch korrekt (synchron bei Start, sofort bei Busy)- Kein Direktaufruf von
BootstrapRunneroderGuiBatchRunCoordinatorausadapter-in-scheduler
Scheduler-Verhalten
scheduleWithFixedDelayverwendet (nichtscheduleAtFixedRate)- Initial Delay = 0 (erster Tick sofort nach Start)
- Executor: Thread-Name
pdf-umbenenner-scheduler, Non-Daemon, Single-Thread UncaughtExceptionHandlergesetzt; 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(); keincheck-then-act - Quellordner-Fehler im Tick beendet Scheduler nicht; WARN-Log; nächster Tick läuft
- No-op-Lauf:
lastRunEndedAtaktualisiert;RunSummarymit 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
SchedulerStatusals immutable Record mitautostartFailed-FlagAtomicReference<SchedulerStatus>als Single-Writer-MechanismusSchedulerStatemit allen 5 Werten implementiert (inkl.STARTING)nextTickAtkorrekt: empty beiSTOPPED/STARTING/RUNNING_BATCH_ACTIVE/STOPPING_BATCH_ACTIVElastErrorgemäß 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
FileChannelConfigurationAccessAdapterimplementiert beide Ports und teilt sichFileChannel- 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
GuiBatchRunCoordinatorgesetzt (nicht inBootstrapRunner.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
SchedulerSettingsPortaktualisiert 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:
enabledzurückgesetzt; Lock freigegeben - Rollback-Fehler: kritischer GUI-Hinweis; ERROR-Log; Property-Zustand nicht stillschweigend angenommen
- Stop-Sequenz: Scheduler stoppen →
enabled=falseschreiben → Lock freigeben (in dieser Reihenfolge)
Properties-Validierung (nur GUI-Modus)
scheduler.enabledungültig (maybe, etc.): Scheduler-Settings-Fehler; deutsche Meldung; Scheduler startet nicht; GUI bleibt bedienbar; manuelle Läufe möglichscheduler.interval.secondsungü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 automatischscheduler.enabled=true+ ungültige Properties: kein Start; deutsche Fehlermeldung;enabledbleibttrue;autostartFailed=truescheduler.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=falseund 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
STOPPEDund Konfig-Tab nicht dirty - Intervall-Validierung: Minimum 30 s; bei Fokusverlust geprüft
- Intervall-Änderung wird sofort persistiert
- Countdown bei
RUNNING_IDLEsichtbar - Hinweis bei
RUNNING_BATCH_ACTIVEsichtbar - Letzter Lauf: Zeitpunkt + Kurzergebnis
- Letzter Fehler: nur sichtbar wenn
lastErrorgesetzt - 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
ApplicationRunContexthart 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.mdundgui-bedienanleitung.mdauf V3.2-Standfreigabe-v3_2.mderstellt- 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 |