Files
pdf-umbenenner/docs/specs/V3_2_-_Spezifikation.md
T

55 KiB
Raw Blame History

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 (GuiBatchRunCoordinatorGuiBatchRunLauncherBootstrapRunner::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
  • 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):

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 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: GuiSchedulerTabSchedulerControlUseCase
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-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:

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