# V3.2 – Automatischer Scheduler

**Status:** Zur Implementierung freigegeben
**Erstellt:** 2026-05-06
**Überarbeitet:** 2026-05-06 (nach ChatGPT-Review Runden 1, 2 und 3; #74 herausgelöst)
**Autor:** Marcus (mit Claude als Mentor)

---

## Ziel

V3.2 bringt **#22 – Automatischer Scheduler**: Die Anwendung überwacht den
Quellordner dauerhaft im Hintergrund und startet die Verarbeitungspipeline
automatisch, sobald neue PDF-Dateien erkannt werden. Das ist der Übergang
vom manuellen Batch-Tool zur autonomen Dauerläufer-Anwendung.

V3.2 ist eine **reine Scheduler-Veranstaltung**. Token- und Kosten-Tracking
(#74) wurde aus V3.2 herausgelöst und bekommt eine eigene saubere
Spezifikation in V3.x – inklusive Modell-Preistabelle, Persistenz-Strategie
und EUR-Währung.

---

## Einordnung

V3.1 ist der abgeschlossene Ausgangspunkt. Hexagonale Architektur,
Modulstruktur, headless-Betrieb, `.properties`-Konfigurationswahrheit
und Flyway-DB-Evolution bleiben als Grundprinzipien vollständig erhalten.

### Kontrollierte Architekturausnahme (CLAUDE.md-Öffnung)

CLAUDE.md enthält heute die Vorgaben **„keine Dauerlauf-Anwendung"** und
**„kein interner Scheduler"**. Diese Einschränkungen werden für V3.2
**bewusst und kontrolliert aufgehoben**. Die Aufhebung ist begrenzt auf:

- Das neue Modul `adapter-in-scheduler` mit `ScheduledExecutorService`-Polling
- Den neuen GUI-Tab „Scheduler" als Steuerungsoberfläche

CLAUDE.md wird im Rahmen von V3.2 entsprechend aktualisiert. Die Aktualisierung
muss explizit enthalten:

- Scheduler nur im GUI-Modus in V3.2
- Kein Headless-Daemon
- Kein WatchService
- Kein #74 in V3.2
- Keine DB-Migration in V3.2
- Scheduler-Modul darf gemischte technische Treiber-/Adapter-Rolle haben
- Kein JavaFX im Scheduler-Modul
- Headless ignoriert Scheduler-Properties vollständig

### Bootstrap-Refactoring (Voraussetzung für #22)

Jeder Verarbeitungslauf führt heute die vollständige Bootstrap-Sequenz aus.
Für einen Scheduler der alle N Sekunden tickt ist das inakzeptabel teuer
und fragil. `BootstrapRunner` wird um eine saubere **Init/Run-Trennung**
erweitert. Details im gleichnamigen Abschnitt.

### Headless-Betrieb

Der bestehende headless-Pfad (`--headless`, `SchedulerBatchCommand`) bleibt
vollständig erhalten und unverändert. Der neue Scheduler läuft in V3.2
**ausschließlich im GUI-Modus**.

**Hinweis zum Begriff:** `SchedulerBatchCommand` bezeichnet den
**bestehenden** Headless-Batch-Command (für Windows Task Scheduler-Aufrufe).
Er ist **nicht** der neue automatische Scheduler. Der Name ist historisch
gewachsen und wird in V3.2 nicht geändert, um den Headless-Pfad
unverändert zu lassen.

**Headless ignoriert Scheduler-Properties:**
Im Headless-Modus werden `scheduler.enabled` und `scheduler.interval.seconds`
**weder gelesen noch validiert**. Ungültige Scheduler-Properties (z.B.
`scheduler.enabled=maybe`) beeinflussen Headless-Exit-Code und -Verhalten
nicht.

### Datenbankschema

V3.2 enthält **keine Flyway-Migration**. Das DB-Schema bleibt unverändert
auf V1. Es entstehen keine neuen Spalten und keine neuen Tabellen.

Flyway wird im Rahmen von `initializeApplicationContext()` weiterhin wie
bisher initialisiert/validiert; Flyway-Fehler verhindern die Erzeugung
des `ApplicationRunContext`, lassen aber die GUI-Shell starten.

### Neues Maven-Modul

`pdf-umbenenner-adapter-in-scheduler`

---

## Scope

### In V3.2 enthalten

| # | Thema | Kategorie |
|---|---|---|
| #22 | Automatischer Scheduler / Quellordner-Überwachung | Hauptfeature |

### Explizit nicht in V3.2

- **Token- und Kosten-Tracking (#74)** → V3.x als eigenes durchdachtes Feature
- Headless-Daemon-Betrieb des Schedulers (`--watch`-Flag) → V3.x
- Java WatchService (ereignisgesteuerte Ordnerüberwachung) → V3.x
- Windows-Service-Integration (WinSW o.ä.) → V3.x
- Modell-Preistabelle, EUR-Währung, Cache-Tokens → V3.x (siehe #74)
- Modell-Filterung (z.B. OpenAI-Snapshots ausblenden) → V3.x
- Dark Mode (#70) → V3.x
- F1-Hilfe (#69) → V3.x
- Log-Viewer in der GUI (#72) → V3.x
- Excel-Export (#75) → V3.x
- Automatische Update-Prüfung (#76) → V3.x
- PDF-Viewer Render-DPI (#23) → V3.x
- Neue KI-Provider, Architekturbrüche
- Änderung der fachlichen Kernverarbeitung
- Stabilitätsprüfung für noch kopierte PDFs (bestehende Retry-Semantik bleibt)

---

## Unverrückbare Leitplanken

- Java 21, Maven Multi-Module, hexagonale Architektur
- Shade-JAR als primäres Distributionsartefakt
- GUI ist Standardstart, `--headless` bleibt vollständig erhalten
- `.properties` bleibt die einzige Konfigurationswahrheit
- Kein Webserver, kein Applikationsserver
- GUI offiziell nur unter Windows; headless für Windows Server / Task Scheduler
- JavaFX-Threading: I/O auf Worker-Thread, UI-Updates via `Platform.runLater()`
- Kein JavaFX in Domain oder Application
- **Kein JavaFX im neuen Modul `adapter-in-scheduler`**
- JavaDoc auf allen neuen öffentlichen Ports, Use-Cases, DTOs und öffentlichen
  Adapter-Methoden; private Implementierungsdetails erhalten deutsche Kommentare
  nur bei fachlicher Nicht-Offensichtlichkeit
- Notwendige Code-Kommentare auf Deutsch; Logging auf Deutsch
- Flyway ist die einzige Schema-Evolutionsquelle (in V3.2 keine Migration)

---

## Bootstrap-Refactoring: Init/Run-Trennung und Fehlertoleranz

### Problem

Jeder GUI- oder Headless-Lauf führt heute die vollständige Bootstrap-Sequenz
erneut aus. Für einen Scheduler der alle N Sekunden tickt ist das teuer
und fragil. Außerdem fehlt heute eine saubere Trennung zwischen
„GUI bedienbar" und „Verarbeitungslauf möglich" – ein Konfigurationsfehler
beim Programmstart führt heute zu einem nicht reparierbaren Zustand.

### Lösung: Zwei Kontexte

V3.2 unterscheidet konzeptionell zwei Kontexte:

**`GuiShellContext` – wird immer aufgebaut, soweit die GUI selbst geladen werden kann:**
- Erlaubt Konfiguration anzeigen, bearbeiten und speichern
- Enthält keine vollständig verdrahtete Verarbeitungspipeline
- Reine GUI-Infrastruktur (Tabs, Dialoge, Konfig-Tab)

**`ApplicationRunContext` – wird nur bei gültiger Laufkonfiguration erzeugt:**
- Legacy-Migration prüfen und ggf. durchführen
- `.properties` laden und validieren (strukturelle Werte)
- Flyway-Schema initialisieren
- Adapter- und Use-Case-Objektgraph vollständig aufbauen
- `BatchRunTrigger`-Implementierung erzeugen
- Wird im Speicher gehalten und für alle Folgeläufe wiederverwendet

### Aufbaureihenfolge beim Programmstart

```
1. GuiShellContext aufbauen → GUI startet, Konfig-Tab bedienbar
2. ApplicationRunContext-Erzeugung versuchen
   → Erfolg: manuelle Läufe und Scheduler-Autostart möglich
   → Fehler: GUI-Shell zeigt Reparatur-Banner; manuelle Läufe und
             Scheduler deaktiviert; Konfig-Tab editierbar
```

### Fehlerbehandlung beim Init

Scheitert die Erzeugung des `ApplicationRunContext` (ungültige strukturelle
Konfiguration, DB nicht erreichbar, Flyway-Fehler, ungültiger API-Key etc.):

- GUI-Shell startet trotzdem
- Im Hauptfenster wird ein **Reparatur-Banner** eingeblendet:
  ```
  ⚠ Anwendung ist nicht laufbereit – Konfiguration fehlerhaft.
    Details: <konkrete deutsche Fehlermeldung>
    Bitte Konfiguration korrigieren und Anwendung neu starten.
  ```
- Manueller Starten-Button im Batch-Tab: deaktiviert (Tooltip erklärt)
- Scheduler-Tab: deaktiviert (Tooltip erklärt)
- Konfig-Tab: vollständig editierbar – User kann Konfiguration reparieren
- Nach Konfig-Speichern: Hinweis dass ein Neustart erforderlich ist

**Strukturelle Werte (DB-Pfad, Provider-URL, API-Key, Quellordner)
wirken erst nach Neustart der Anwendung.** Ein „Live-Reload" des
`ApplicationRunContext` ist nicht in V3.2.

### Pro-Lauf-Phase

`executeRun(ApplicationRunContext)` (pro Lauf, manuell oder Tick):
- Run-Lock nicht-blockierend erwerben (`RunLockPort.tryAcquire()`)
- Falls Lock nicht verfügbar: sofort abbrechen (kein Warten, kein Queuing)
- `BatchRunProcessingUseCase.execute(BatchRunContext)` aufrufen
- Run-Lock freigeben (in `finally`-Block)

### Code-Analyse erforderlich

Vor der Implementierung analysiert Claude Code den aktuellen
`BootstrapRunner`-Quellcode und dokumentiert verbindlich:
- Welche Initialisierungsschritte heute mit der Laufausführung vermengt sind
- Den konkreten Schnitt zwischen `GuiShellContext`, `ApplicationRunContext`
  und `executeRun`
- Ob die Kontexte als neue Typen eingeführt werden oder bestehende
  Kapselungen genutzt werden können

**Wichtig:** Der bestehende GUI-Aufrufpfad
(`GuiBatchRunCoordinator` → `GuiBatchRunLauncher` →
`BootstrapRunner::launchGuiBatchRun`) bleibt für manuelle Läufe vollständig
funktionsfähig. Das Refactoring darf keinen bestehenden Test brechen.

---

## #22 – Automatischer Scheduler

### Fachliche Beschreibung

Die Anwendung überwacht den konfigurierten Quellordner in regelmäßigen
Abständen (Polling) auf neue, unverarbeitete PDF-Dateien. Wird der
`BatchRunProcessingUseCase` getriggert und findet keine Kandidaten, ist
das ein gültiger No-op-Lauf – kein Fehlerstatus, kein neuer
`processing_attempt`-Eintrag. Findet er Kandidaten, läuft die bestehende
Verarbeitungspipeline vollständig durch. Der Nutzer steuert den Scheduler
ausschließlich über den neuen GUI-Tab „Scheduler".

**Während aktivem Scheduler:**
- Manuelle Läufe sind verboten (Manuell-Starten-Button deaktiviert)
- Konfigurationsänderungen sind verboten (Konfig-Tab read-only, OS-Lock auf `.properties`)
- Scheduler-Intervall im Scheduler-Tab ist nicht editierbar

**Mentales Modell:** Scheduler an = Autopilot. Während des Autopilot-Betriebs
sind manuelle Läufe und Konfigurationsänderungen gesperrt.

**Fehlerklassifikation im Lauf:**

| Fehlerart | Ergebnis |
|---|---|
| Quellordner nicht erreichbar (vor Kandidatenscan) | Whole-run failure; kein `processing_attempt`; `BatchRunTriggerResult.Failed`; WARN-Log; Scheduler läuft weiter |
| Einzelne PDF nicht lesbar (z.B. noch im Kopiervorgang) | Per-document failure; bestehende Retry-Semantik; ggf. `FAILED_RETRYABLE` |
| API-Fehler bei einzelner Datei | Per-document failure; bestehende Attempt-Persistenz |
| Technischer Fehler des gesamten Laufs | Whole-run failure; `BatchRunTriggerResult.Failed`; WARN-Log; Scheduler läuft weiter |

**Unvollständige PDFs (noch im Kopiervorgang):** Für V3.2 keine
zusätzliche Stabilitätsprüfung. Bekannte Einschränkung: temporär
unvollständige Dateien können `FAILED_RETRYABLE` erzeugen, werden aber
beim nächsten Tick erneut versucht.

### Technische Entscheidungen

| Aspekt | Entscheidung | Begründung |
|---|---|---|
| Überwachungsmechanismus | Polling via `ScheduledExecutorService` | Deterministisch; zuverlässiger auf Windows-Netzlaufwerken als WatchService |
| Ausführungsstrategie | `scheduleWithFixedDelay` | Wartet N Sekunden nach Laufende; verhindert Tick-Stapelung |
| **Initial Delay** | **Null (erster Tick sofort nach Start)** | Sofortiges Nutzer-Feedback bei manuellem Start; bei Autostart ebenso schnell |
| WatchService | Nicht in V3.2 | Auf Windows-Netzlaufwerken unzuverlässig |
| Modularisierung | Eigenes Modul `adapter-in-scheduler` | Saubere Trennung; kein JavaFX |
| Headless-Daemon | Nicht in V3.2 | Bewusste Scope-Begrenzung |
| Laufkollision | Skip via nicht-blockierendem RunLock | `RunLockPort.tryAcquire()`; kein `check-then-act` |
| Manuelle Läufe bei aktivem Scheduler | Verboten | Vermeidet Nicht-Determinismus |
| Lauf-Trigger | Neutrales `BatchRunTrigger`-Interface mit Result-Objekt | Keine Direktaufrufe von `BootstrapRunner` oder `GuiBatchRunCoordinator` aus dem Scheduler-Modul |
| **Daemon-Thread** | **Non-Daemon** | Saubere Shutdown-Garantie; passt zu kontrolliertem App-Schließen |
| Config-Schutz | OS-Lock auf `.properties` während Scheduler läuft oder Lauf aktiv | Bestmöglicher Schreibschutz; nicht als absolute Garantie formuliert |
| Lock-Mechanismus | `tryLock()` als Retry-Schleife mit Deadline | `FileChannel.tryLock()` hat keinen Timeout-Parameter; Deadline wird selbst gebaut |
| GUI-Aktualisierung | Polling per JavaFX `Timeline` (1 Hz), zentral beim GUI-Aufbau gestartet | Aktualisiert Scheduler-Tab, Batch-Tab und Konfig-Tab unabhängig vom aktiven Tab |

### Konfiguration (`.properties`)

```properties
# Scheduler aktiv (true/false); Default: false; nur im GUI-Modus relevant
scheduler.enabled=false

# Wartezeit in Sekunden nach Ende des letzten Laufs bis zum nächsten Tick; Minimum: 30
scheduler.interval.seconds=180
```

#### Validierung beim Laden (nur GUI-Modus)

Im **GUI-Modus** validiert ein neuer **Scheduler-Settings-Validator**
diese Properties:

| Property | Fehlend | Leer | Ungültig | Unter Minimum / negativ |
|---|---|---|---|---|
| `scheduler.enabled` | Default `false` | Default `false` | Scheduler-Settings-Fehler; Anzeige in GUI | n/a |
| `scheduler.interval.seconds` | Default `180` | Default `180` | Scheduler-Settings-Fehler; Anzeige in GUI | Scheduler-Settings-Fehler |

**Wichtig – die Begriffsbildung ist bewusst:**

`Scheduler-Settings-Fehler` ist **nicht** das gleiche wie ein
`ApplicationRunContext`-Fehler. Ein Scheduler-Settings-Fehler:
- blockiert nur Scheduler-Autostart und Scheduler-Start-Button
- blockiert **nicht** die GUI
- blockiert **nicht** manuelle Läufe
- blockiert **nicht** den Headless-Lauf
- wird im Scheduler-Tab und/oder als Startup-Hinweis angezeigt

**Headless-Modus** liest, validiert und verwendet diese Properties nicht.

#### Autostart-Verhalten (nur GUI-Modus)

`scheduler.enabled=false` ist der Default. Startet der Nutzer den Scheduler
in der GUI, schreibt die Anwendung `scheduler.enabled=true` in `.properties`.
Beim nächsten Programmstart startet der Scheduler automatisch, **wenn**:

- `scheduler.enabled=true`
- Scheduler-Properties valide
- `ApplicationRunContext` erfolgreich erzeugt

Scheitert eine dieser Bedingungen, **startet der Scheduler nicht**, aber
`scheduler.enabled` bleibt `true`. Der Status erhält ein zusätzliches Flag
`autostartFailed=true`. Die GUI zeigt einen klaren Reparaturpfad
(siehe Abschnitt „Autostart-Fehlerzustand").

### Properties-Persistierung – `SchedulerSettingsPort`

**Neuer Outbound-Port (Modul `application`):**

```java
public interface SchedulerSettingsPort {
    SchedulerSettings loadSettings();
    void saveEnabled(boolean enabled) throws SchedulerSettingsWriteException;
    void saveIntervalSeconds(int seconds) throws SchedulerSettingsWriteException;
}
```

Die GUI ruft den Use-Case auf, der Use-Case delegiert an den Port. Kein
direktes `.properties`-Schreiben aus der GUI.

**Format-Erhalt-Anforderung:**

Der `SchedulerSettingsPort`-Adapter aktualisiert nur die zwei
Scheduler-Keys und erhält alle übrigen Zeilen, Kommentare und unbekannten
Properties unverändert. Reihenfolge und Zeilenenden bleiben stabil.
Bei Schreibfehler darf die Datei nicht in einem korrupten Zustand
zurückbleiben (atomarer Schreibvorgang über Temp-Datei).

### Config-Datei-Sperre und Schreibmechanismus

Solange der Scheduler läuft **oder** ein Lauf (manuell oder Tick) aktiv
ist, hält die Anwendung einen exklusiven OS-Lock auf die `.properties`-Datei
via `FileChannel.tryLock()` (Windows: `LockFileEx`).

**Realistische Formulierung des OS-Locks:**

Der Lock ist ein **bestmöglicher OS-Level-Schreibschutz**. Die GUI
betrachtet die Konfiguration während Lock als gesperrt. Externe
Schreibversuche durch Texteditoren scheitern in den unterstützten
Windows-Testfällen oder erzeugen einen klaren Konflikt. Eine absolute
Garantie gegen alle externen Schreibstrategien (Delete-Rename,
Editor-spezifische Save-Strategien etc.) wird nicht beansprucht.

**Variante B aus Review – Lock-Adapter besitzt Schreibfähigkeit:**

Der Lock-Adapter hält den `FileChannel` und stellt eigene
Schreiboperationen bereit. Während des Locks darf nur dieser Adapter
schreiben.

**Neuer Outbound-Port `ConfigurationFileLockPort` (Modul `application`):**

```java
public interface ConfigurationFileLockPort {
    void acquireLock() throws ConfigurationFileLockException;
    void releaseLock();
    boolean isLocked();
}
```

**Implementierung `FileChannelConfigurationAccessAdapter`** (implementiert
sowohl `ConfigurationFileLockPort` als auch `SchedulerSettingsPort`,
Modul `adapter-in-scheduler` oder `bootstrap`):

- Kennt den `.properties`-Pfad via Konstruktor-Injektion aus `ApplicationRunContext`
- `acquireLock()` ruft intern eine **Retry-Schleife** auf:
  ```
  deadline = now + lockTimeoutMillis
  while (now < deadline) {
      try {
          fileLock = channel.tryLock();
          if (fileLock != null) return;
      } catch (OverlappingFileLockException e) {
          // weiter probieren
      }
      sleep(100ms);
  }
  throw new ConfigurationFileLockException("...");
  ```
- `FileChannel.lock()` (blockierend) wird **nicht** verwendet
- Ausgeführt im Worker-Thread, niemals auf JavaFX Application Thread
- Bei Lock-Konflikt (extern gesperrt, Netzlaufwerk hängt): deutsche
  Fehlermeldung, `ConfigurationFileLockException` wird geworfen
- `releaseLock()` ist idempotent
- Der Adapter teilt sich den `FileChannel` zwischen Lock- und
  Schreiboperation, damit Settings auch während aktivem Lock geschrieben
  werden können

**Lebenszyklus:**

| Phase | Lock-Status | Wer setzt/freigibt |
|---|---|---|
| App-Start, kein Lauf, Scheduler aus | kein Lock | – |
| Manueller Lauf läuft | Lock aktiv (für Dauer des Laufs) | `GuiBatchRunCoordinator` |
| Manueller Lauf beendet | Lock freigegeben | `GuiBatchRunCoordinator` (in `finally`) |
| Scheduler gestartet | Lock aktiv (für gesamte Scheduler-Laufzeit) | `DefaultSchedulerControlUseCase.start()` |
| Scheduler-Tick läuft | Lock weiterhin aktiv | – (bleibt aktiv) |
| Scheduler gestoppt | Lock freigegeben | `DefaultSchedulerControlUseCase.stop()` |

**Wichtig:** Der Config-Lock bei manuellen Läufen wird im
`GuiBatchRunCoordinator` gesetzt, **nicht** in `BootstrapRunner.executeRun()`.
Damit bleibt der Headless-Pfad vollständig unverändert.

**Manueller Lauf bei extern gesperrter Config-Datei:**
Kann der Lock nicht erworben werden, startet der manuelle Lauf nicht.
GUI zeigt deutsche Meldung:
```
Konfigurationsdatei ist gesperrt. Lauf wurde nicht gestartet.
```

**GUI-Verhalten während Lock (Konfig-Tab):**

Ein Read-Only-Banner wird eingeblendet:

```
⚠ Konfiguration gesperrt – Scheduler läuft (oder Lauf aktiv).
  Scheduler beenden bzw. Lauf abwarten um Änderungen vorzunehmen.
```

Zusätzlich ist der **Speichern-Button hart deaktiviert**.
Alle Eingabefelder im Konfig-Tab sind nicht editierbar
(`setEditable(false)` / `setDisable(true)`), bleiben aber sichtbar.

### Dirty-State vor Scheduler-Start

Hat der Konfig-Tab beim Klick auf „Scheduler starten" ungespeicherte
Änderungen (Dirty-State), ist der Start-Button **hart deaktiviert**.
Tooltip: „Bitte Konfiguration speichern oder Änderungen verwerfen."

Diese Regel verhindert, dass der Scheduler mit alten Werten startet
während der Nutzer geänderte Werte vor sich sieht.

### Manuelle Läufe bei aktivem Scheduler

Der Manuell-Starten-Button im Batch-Tab ist bei aktivem Scheduler
(`SchedulerStatus.state ∉ {STOPPED}` oder `state == STARTING`)
**hart deaktiviert**.
Tooltip: „Manuelle Läufe sind während aktivem Scheduler nicht möglich."

### Autostart-Fehlerzustand

Wenn `scheduler.enabled=true`, der Autostart aber fehlschlägt
(Scheduler-Settings-Fehler oder `ApplicationRunContext`-Fehler):

`SchedulerStatus` enthält:
- `state = STOPPED`
- `autostartFailed = true`
- `lastError = <konkrete deutsche Fehlermeldung>`

GUI im Scheduler-Tab:

```
⚠ Autostart fehlgeschlagen – Scheduler ist nicht aktiv.
  Grund: <lastError>

  [ Scheduler starten ]    [ Autostart deaktivieren ]
```

| Button | Verhalten |
|---|---|
| „Scheduler starten" | Aktiv wenn `ApplicationRunContext` vorhanden und Konfig-Tab nicht dirty; setzt `autostartFailed=false` bei Erfolg |
| „Autostart deaktivieren" | Schreibt `scheduler.enabled=false` über `SchedulerSettingsPort`; setzt `autostartFailed=false` |

So bleibt der Zustand jederzeit reparierbar.

### App-Schließen bei aktivem Lauf oder Scheduler

Beim Klick auf „Schließen" prüft die Anwendung:

| Zustand | Verhalten |
|---|---|
| Kein Lauf aktiv, Scheduler `STOPPED` | Schließen sofort möglich |
| Manueller Lauf läuft | Hinweisdialog (siehe unten) |
| Scheduler aktiv (egal ob Tick aktiv oder nicht) | Hinweisdialog (siehe unten) |

**Hinweisdialog:**

```
┌──────────────────────────────────────────┐
│ Anwendung kann nicht beendet werden     │
├──────────────────────────────────────────┤
│ Ein Lauf ist aktiv oder der Scheduler   │
│ läuft. Bitte beende den Scheduler bzw.  │
│ warte auf das Ende des Laufs.           │
│                                          │
│                                  [ OK ]  │
└──────────────────────────────────────────┘
```

Der Dialog hat **nur einen OK-Button**. Die Anwendung wird **nicht**
beendet. Der Nutzer behält die Kontrolle.

### Neues Maven-Modul `pdf-umbenenner-adapter-in-scheduler`

Das neue Modul wird in der Parent-`pom.xml` als Submodul registriert.

**Architektonische Einordnung:**

Das Modul ist ein **technischer Scheduler-Adapter mit gemischter
Treiber-/Infrastrukturrolle**. Es enthält keine fachliche Logik und kein
JavaFX. Die Vermischung ist begrenzt auf:

- Inbound-Treiber-Rolle: `ScheduledExecutorService` ruft bei jedem Tick
  `BatchRunTrigger.triggerRun()` auf
- Outbound-Adapter-Rollen: implementiert `SchedulerPort`,
  `ConfigurationFileLockPort` und `SchedulerSettingsPort`

**Verbindliche Maven-Konfiguration:**

| Plugin / Abhängigkeit | Einstellung | Begründung |
|---|---|---|
| PIT Mutation Testing | **Explizit deaktiviert** | PIT läuft ausschließlich auf `domain` und `application` |
| Shade-Plugin | **Nicht aktiv** | Nur im Distributions-Modul |
| `flatten-maven-plugin` | **Aktiv** | CI-friendly `${revision}` |
| JavaFX-Abhängigkeiten | **Keine** | Kein JavaFX in `adapter-in-scheduler` |
| Checkstyle / weitere Quality-Plugins | Konsistent mit anderen Adapter-Modulen |

Claude Code prüft bei der Implementierung alle durch die Parent-`pom.xml`
vererbten Plugins und dokumentiert verbindlich, welche explizit deaktiviert
oder aktiviert werden müssen.

### Architektur

#### Neue Komponenten (Übersicht)

| Komponente | Typ | Modul | Zweck |
|---|---|---|---|
| `SchedulerControlUseCase` | Inbound-Port-Interface | `application` | `start()`, `stop()`, `getStatus(): SchedulerStatus` |
| `DefaultSchedulerControlUseCase` | Use-Case-Impl. | `application` | Scheduler-Zustand; Delegation; Lock-Lifecycle; Settings-Persistierung |
| `SchedulerPort` | Outbound-Port | `application` | `startScheduler(SchedulerConfig, BatchRunTrigger)`, `stopScheduler()` |
| `BatchRunTrigger` | Funktionales Interface | `application` | `triggerRun(): BatchRunTriggerResult` |
| `BatchRunTriggerResult` | Sealed Interface | `application` | `Started`, `SkippedBusy`, `Failed` |
| `SchedulerConfig` | Value Object | `application` | `intervalSeconds: int` |
| `SchedulerSettings` | DTO | `application` | `enabled: boolean`, `intervalSeconds: int` |
| `SchedulerStatus` | Immutable Value Object | `application` | `state`, `lastRunEndedAt`, `lastRunSummary`, `nextTickAt`, `lastError`, `autostartFailed` |
| `SchedulerState` | Enum | `application` | `STOPPED`, `STARTING`, `RUNNING_IDLE`, `RUNNING_BATCH_ACTIVE`, `STOPPING_BATCH_ACTIVE` |
| `ConfigurationFileLockPort` | Outbound-Port | `application` | `acquireLock()`, `releaseLock()`, `isLocked()` |
| `SchedulerSettingsPort` | Outbound-Port | `application` | `loadSettings()`, `saveEnabled()`, `saveIntervalSeconds()` |
| `RunLockPort.tryAcquire()` | Erweiterung | `application` | Bestehender Port; **neue** Methode `Optional<RunLockHandle> tryAcquire()` |
| `RunLockHandle` | Interface | `application` | `AutoCloseable` für try-with-resources |
| `ScheduledExecutorServiceSchedulerAdapter` | Adapter | `adapter-in-scheduler` | Implementiert `SchedulerPort`; betreibt Single-Thread-Executor |
| `FileChannelConfigurationAccessAdapter` | Adapter | `adapter-in-scheduler` o. `bootstrap` | Implementiert `ConfigurationFileLockPort` und `SchedulerSettingsPort` |
| `GuiSchedulerControlPort` | Bridge-Interface | `adapter-in-gui` | Brücke: `GuiSchedulerTab` → `SchedulerControlUseCase` |
| `GuiSchedulerTab` | GUI-Komponente | `adapter-in-gui` | Neuer Tab: Steuerung, Status, Intervall |
| `GuiStatusRefreshTimeline` | GUI-Komponente | `adapter-in-gui` | Zentrale 1-Hz-Timeline; aktualisiert Scheduler-/Batch-/Konfig-Tab |

#### `BatchRunTrigger` und Result-Objekt

```java
// Modul: application
@FunctionalInterface
public interface BatchRunTrigger {
    /**
     * Löst synchron einen Verarbeitungslauf aus.
     *
     * Ist der RunLock nicht verfügbar (anderer Lauf läuft bereits),
     * kehrt die Methode sofort mit {@link BatchRunTriggerResult.SkippedBusy}
     * zurück, ohne einen neuen Lauf zu starten.
     *
     * Wird der Lauf gestartet, kehrt die Methode erst nach dessen
     * vollständigem Abschluss zurück. Das Ergebnis enthält die finale
     * {@link RunSummary} und den Endzeitpunkt.
     *
     * Tritt vor oder während des Laufs ein technischer Fehler auf
     * (z.B. Quellordner nicht erreichbar), liefert die Methode
     * {@link BatchRunTriggerResult.Failed} mit bereinigten Meldungen.
     * Exceptions werden nicht propagiert und der Stacktrace wird im
     * Adapter geloggt – nicht im Result-Objekt transportiert.
     */
    BatchRunTriggerResult triggerRun();
}

public sealed interface BatchRunTriggerResult
        permits Started, SkippedBusy, Failed {

    record Started(Instant endedAt, RunSummary summary)
            implements BatchRunTriggerResult {}

    record SkippedBusy() implements BatchRunTriggerResult {}

    /**
     * Whole-run failure. userMessage ist deutsche, GUI-taugliche Meldung;
     * technicalMessage ist Detail für Diagnose. Stacktrace wurde bereits
     * im Adapter geloggt und wird nicht im Result transportiert.
     */
    record Failed(String userMessage, String technicalMessage)
            implements BatchRunTriggerResult {}
}
```

`Throwable` wird **nicht** im Result-Objekt transportiert. Das vermeidet
technische Details im Application-Layer und potenzielles Stacktrace-Leak
in die GUI.

Bootstrap implementiert `BatchRunTrigger` als Lambda und übergibt es beim
`SchedulerPort.startScheduler(...)`-Aufruf an den Scheduler-Adapter.
Der Adapter kennt weder `BootstrapRunner` noch `GuiBatchRunCoordinator`.

#### `RunLockPort`-Erweiterung

`RunLockPort` ist bereits vorhanden. V3.2 fügt eine neue Methode hinzu:

```java
public interface RunLockPort {
    // ... bestehende Methoden bleiben unverändert ...

    /**
     * Versucht nicht-blockierend, den Run-Lock zu erwerben.
     * Liefert ein Handle (für try-with-resources) bei Erfolg,
     * sonst Optional.empty().
     */
    Optional<RunLockHandle> tryAcquire();
}

public interface RunLockHandle extends AutoCloseable {
    @Override
    void close();   // gibt den Lock frei; idempotent
}
```

Der bestehende blockierende `acquire()` bleibt erhalten und wird vom
manuellen GUI-Lauf weiterhin verwendet (Verhalten unverändert).
Der Scheduler nutzt ausschließlich `tryAcquire()`.

**Code-Analyse erforderlich:** Claude Code dokumentiert vor der
Implementierung die aktuelle `RunLockPort`-Signatur und schlägt einen
konkreten Erweiterungspunkt vor.

#### `SchedulerStatus` – Immutable Snapshot

```java
public record SchedulerStatus(
    SchedulerState state,
    Optional<Instant> lastRunEndedAt,
    Optional<RunSummary> lastRunSummary,
    Optional<Instant> nextTickAt,
    Optional<String> lastError,
    boolean autostartFailed
) {}
```

Threadsicher veröffentlicht via `AtomicReference<SchedulerStatus>` im
`DefaultSchedulerControlUseCase`. Snapshots werden atomar ausgetauscht.

**`SchedulerState`-Werte:**

| Wert | Bedeutung |
|---|---|
| `STOPPED` | Scheduler gestoppt |
| `STARTING` | Start in Vorbereitung – Lock-Erwerb läuft; manuelle Starts deterministisch gesperrt |
| `RUNNING_IDLE` | Scheduler aktiv, wartet auf nächsten Tick |
| `RUNNING_BATCH_ACTIVE` | Scheduler aktiv, Tick läuft gerade einen Batch |
| `STOPPING_BATCH_ACTIVE` | Stop angefordert, aber laufender Batch läuft noch zu Ende |

**`nextTickAt`-Berechnung:**

| Zustand | `nextTickAt` |
|---|---|
| `STOPPED`, `STARTING` | `Optional.empty()` |
| `RUNNING_IDLE` | `lastRunEndedAt + intervalSeconds` (oder `now + intervalSeconds` vor erstem Lauf, gilt aber dank Initial Delay 0 nur sehr kurz) |
| `RUNNING_BATCH_ACTIVE`, `STOPPING_BATCH_ACTIVE` | `Optional.empty()` |

**`lastError`-Lebenszyklusregel:**

| Ereignis | `lastError` |
|---|---|
| `Started` (regulärer oder No-op-Lauf) | gelöscht (`empty`) |
| `SkippedBusy` | unverändert |
| `Failed` | gesetzt mit `userMessage` |
| Scheduler-Start (Beginn) | gelöscht |
| Scheduler-Stop | unverändert (User soll letzten Fehler noch sehen können) |

**No-op-Lauf:**
`RunSummary` mit `successCount=0, failedCount=0, skippedCount=0`.
GUI-Anzeige im Scheduler-Tab: **„Letzter Lauf: 14:23 – keine neuen Dokumente"**
(verbindlich; nicht „0 erfolgreich, 0 fehlgeschlagen, 0 übersprungen").

#### Aufrufpfad Scheduler-Tick

```
ScheduledExecutorService (initialDelay=0; danach intervalSeconds nach Laufende)
  → ScheduledExecutorServiceSchedulerAdapter.onTick()
  → batchRunTrigger.triggerRun()       [synchron, blockiert bis Laufende]
      → RunLockPort.tryAcquire()
         → empty: → Result: SkippedBusy; DEBUG-Log
         → Handle: BatchRunProcessingUseCase.execute(BatchRunContext)
                   (try-with-resources auf Handle)
                   → Result: Started(endedAt, runSummary)
         → Exception: Stacktrace im Adapter loggen (ERROR);
                      → Result: Failed(userMessage, technicalMessage)
                      → Status.lastError aktualisieren
                      → WARN-Log
  → SchedulerStatus atomar aktualisieren (lastRunEndedAt, lastRunSummary,
    state, nextTickAt, lastError gemäß Regel)
```

#### Aufrufpfad Scheduler-Start

```
GuiSchedulerTab → GuiSchedulerControlPort
  → DefaultSchedulerControlUseCase.start()
  → Voraussetzungen prüfen:
     - ApplicationRunContext vorhanden
     - Konfig-Tab nicht dirty
     - Kein manueller Lauf aktiv
     - Sonst: Fehlermeldung an GUI; kein State-Wechsel
  → State = STARTING (atomar)
  → SchedulerSettingsPort.saveEnabled(true)
     → bei Fehler: State zurück auf STOPPED; Fehlermeldung; Ende
  → ConfigurationFileLockPort.acquireLock()    [Worker-Thread, mit Retry-Deadline]
     → bei Fehler: Rollback saveEnabled(false);
                   bei Rollback-Fehler ERROR-Log + kritischer GUI-Hinweis;
                   State zurück auf STOPPED; lastError gesetzt; autostartFailed unverändert
  → SchedulerPort.startScheduler(config, trigger)
     → bei Fehler: releaseLock(); Rollback saveEnabled(false);
                   State STOPPED; lastError gesetzt
  → State = RUNNING_IDLE
  → autostartFailed = false (Erfolg löscht das Flag)
  → GUI: Banner einblenden; Button-Zustände aktualisieren
```

**Rollback-Fehler:** Schlägt das Zurücksetzen von `saveEnabled(false)`
nach einem Startfehler **selbst** fehl, bleibt Scheduler gestoppt.
GUI zeigt kritischen Hinweis:

```
Scheduler konnte nicht gestartet werden.
Zusätzlich konnte scheduler.enabled nicht zurückgesetzt werden.
Bitte Konfigurationsdatei prüfen.
```

Dieser Zustand wird ERROR-geloggt.

#### Aufrufpfad Scheduler-Stop

```
GuiSchedulerTab → GuiSchedulerControlPort
  → DefaultSchedulerControlUseCase.stop()
  → State = STOPPING_BATCH_ACTIVE (falls Lauf aktiv) oder direkt STOPPED
  → SchedulerPort.stopScheduler()                    [shutdown(); keine neuen Ticks]
  → SchedulerSettingsPort.saveEnabled(false)         [über lock-haltenden Adapter]
  → ConfigurationFileLockPort.releaseLock()
  → State = STOPPED (final, nach Batch-Ende)
  → GUI: Banner ausblenden; Button-Zustände aktualisieren
```

**Idempotenz:**
- `start()` auf bereits laufendem Scheduler: no-op (kein Fehler)
- `stop()` auf bereits gestopptem Scheduler: no-op
- `releaseLock()` ohne aktiven Lock: no-op

#### Laufkollision und Kollisionsvermeidung

`RunLockPort` ist die **einzige und maßgebliche** Kollisionsinstanz.
Es gibt kein `check-then-act` über `isRunning()`-Methoden.
`tryAcquire()` ist atomar und race-condition-sicher.

Da manuelle Läufe bei aktivem Scheduler verboten sind und der
`STARTING`-Zwischenzustand manuelle Starts deterministisch sperrt,
ist die einzige verbleibende Kollisionsquelle ein theoretisches Race
zwischen zwei Scheduler-Ticks. `scheduleWithFixedDelay` schließt das
praktisch aus, aber `RunLockPort` schützt dennoch.

`SchedulerPort.isRunning()` (sofern beibehalten) wird **ausschließlich
adapterintern** genutzt; nicht von GUI oder Use Case. Die maßgebliche
Statusquelle für Außen ist `SchedulerControlUseCase.getStatus()`.

#### Scheduler-Lifecycle: Details

**Executor-Konfiguration:**

| Aspekt | Wert |
|---|---|
| Thread-Name | `pdf-umbenenner-scheduler` |
| **Daemon** | **`false`** (Non-Daemon; saubere Shutdown-Kontrolle) |
| Pool-Größe | 1 (Single-Thread; verhindert parallele Ticks) |
| `UncaughtExceptionHandler` | gesetzt; loggt deutsch auf ERROR |

**`scheduleWithFixedDelay` mit Initial Delay 0:**
Erster Tick startet sofort nach `startScheduler()`. Folgende Ticks
starten N Sekunden **nach Ende** des vorigen Ticks.

**Stop während aktivem Lauf:**
„Stoppen" beendet nur die Planung weiterer Ticks. Der laufende Batch
läuft regulär zu Ende (`SchedulerState.STOPPING_BATCH_ACTIVE`).
Status im Tab:

```
○ Gestoppt – aktueller Lauf läuft noch
```

Nach Laufende: `SchedulerState.STOPPED`, Lock freigegeben,
Banner ausgeblendet.

**Exception-Handling im Tick:**

```java
// Prinzip im Tick-Callback des Scheduler-Adapters:
try {
    BatchRunTriggerResult result = batchRunTrigger.triggerRun();
    statusUpdater.update(result);
} catch (Throwable t) {
    log.error("Unbehandelte Exception im Scheduler-Tick – Scheduler läuft weiter", t);
    statusUpdater.recordError(t.getMessage());
}
```

Eine Exception darf den `ScheduledExecutorService` **niemals** dauerhaft
stoppen.

**Shutdown beim App-Ende:**

App-Schließen ist nur möglich wenn `state == STOPPED` und kein Lauf
aktiv (siehe „App-Schließen" oben). `Application.stop()` führt zur
Sicherheit nochmal aus:

- `SchedulerPort.stopScheduler()` (idempotent)
- `awaitTermination(5, SECONDS)` als Sicherheitsnetz
- Alle Locks und FileChannels werden freigegeben

### GUI: Neuer Tab „Scheduler"

#### Steuerung und Status

Der Scheduler-Tab enthält **ausschließlich** Steuerungs- und Statuselemente.

| Element | Beschreibung |
|---|---|
| Status-Indikator | Farbiges Label gemäß `SchedulerState` |
| Autostart-Fehler-Banner | Sichtbar wenn `autostartFailed=true`; mit „Scheduler starten"-Button und „Autostart deaktivieren"-Button |
| „Scheduler starten"-Button | Aktiv nur wenn `STOPPED` UND `ApplicationRunContext` vorhanden UND Konfig-Tab nicht dirty UND kein Lauf aktiv |
| „Scheduler stoppen"-Button | Aktiv nur wenn `state` ∈ {`RUNNING_IDLE`, `RUNNING_BATCH_ACTIVE`} |
| Status während Auslauf | `○ Gestoppt – aktueller Lauf läuft noch` (`STOPPING_BATCH_ACTIVE`) |
| Nächster Tick in: | Countdown; nur sichtbar wenn `RUNNING_IDLE`; basiert auf `nextTickAt` |
| Hinweis bei aktivem Tick | „Lauf läuft – nächster Tick nach Abschluss + N Sekunden" |
| Letzter Lauf | Aus `lastRunSummary`; bei No-op: „keine neuen Dokumente" |
| Letzter Fehler | Aus `lastError`; nur sichtbar wenn nicht empty |
| Intervall (Sekunden) | Nur editierbar wenn `STOPPED` und Konfig-Tab nicht dirty; Minimum 30 s; sofort persistiert via `SchedulerSettingsPort.saveIntervalSeconds()` |

#### Zentraler Status-Refresh

Eine zentrale **`GuiStatusRefreshTimeline`** (JavaFX `Timeline`, 1 Hz)
wird **beim Aufbau der Haupt-GUI** (nicht erst beim Öffnen des
Scheduler-Tabs) gestartet. Sie aktualisiert:

- Scheduler-Tab (alle Status-Felder)
- Batch-Tab (Manuell-Starten-Button-Zustand)
- Konfig-Tab (Lock-Banner und Speichern-Button)

Beim App-Schließen wird die Timeline gestoppt. Mehrfacher Start wird
vermieden (Idempotenz auf Timeline-Ebene).

#### Button-Zustandstabelle

| Anwendungszustand | „Starten" | „Stoppen" |
|---|---|---|
| `STOPPED`, kein Lauf, Konfig nicht dirty, RunContext OK | ✅ Aktiv | ❌ |
| `STOPPED`, kein Lauf, Konfig dirty | ❌ (Tooltip) | ❌ |
| `STOPPED`, kein RunContext (Init-Fehler) | ❌ (Tooltip) | ❌ |
| `STOPPED`, manueller Lauf aktiv | ❌ | ❌ |
| `STARTING` | ❌ | ❌ |
| `RUNNING_IDLE` | ❌ | ✅ Aktiv |
| `RUNNING_BATCH_ACTIVE` | ❌ | ✅ Aktiv |
| `STOPPING_BATCH_ACTIVE` | ❌ | ❌ |

#### Anpassung des Batch-Tabs

Der Manuell-Starten-Button im Batch-Tab wird hart deaktiviert wenn:

- `SchedulerStatus.state ≠ STOPPED` (Scheduler aktiv oder im Übergang), **oder**
- `ApplicationRunContext` nicht vorhanden (Init-Fehler)

Tooltip erklärt den Grund.

---

## Logging-Matrix

| Ereignis | Level |
|---|---|
| Scheduler gestartet mit Intervall N | INFO |
| Scheduler gestoppt | INFO |
| Tick gestartet | DEBUG |
| Tick übersprungen wegen aktivem Lauf | DEBUG |
| No-op-Lauf abgeschlossen | INFO |
| Lauf erfolgreich abgeschlossen mit RunSummary | INFO |
| Tick-Fehler (z.B. Quellordner nicht erreichbar) | WARN |
| Unbehandelte Exception im Tick (Stacktrace) | ERROR |
| Config-Lock erworben | INFO |
| Config-Lock freigegeben | INFO |
| Config-Lock-Erwerb fehlgeschlagen | ERROR |
| Rollback nach Startfehler erfolgreich | WARN |
| Rollback nach Startfehler **fehlgeschlagen** | ERROR |
| Scheduler-Settings-Validierungsfehler beim Laden | ERROR |
| Scheduler-Autostart erfolgreich | INFO |
| Scheduler-Autostart fehlgeschlagen | ERROR |
| ApplicationRunContext-Erzeugung fehlgeschlagen | ERROR |

Alle Log-Meldungen auf Deutsch.

---

## Architektur-Zusammenfassung

### Neues Maven-Modul

| Modul | Zweck |
|---|---|
| `pdf-umbenenner-adapter-in-scheduler` | `ScheduledExecutorService`-Polling; Lifecycle-Steuerung; Config-Lock + Settings-Persistierung |

### Neue Inbound-Port-Interfaces und Use-Cases

| Komponente | Typ | Modul | Issue |
|---|---|---|---|
| `SchedulerControlUseCase` | Inbound-Port-Interface | `application` | #22 |
| `DefaultSchedulerControlUseCase` | Use-Case-Impl. | `application` | #22 |

### Neue Outbound-Ports

| Komponente | Modul | Issue |
|---|---|---|
| `SchedulerPort` | `application` | #22 |
| `ConfigurationFileLockPort` | `application` | #22 |
| `SchedulerSettingsPort` | `application` | #22 |

### Erweiterte Outbound-Ports

| Komponente | Modul | Erweiterung | Issue |
|---|---|---|---|
| `RunLockPort` | `application` | Neue Methode `Optional<RunLockHandle> tryAcquire()` | #22 |

### Neue Funktionale Interfaces und Result-Typen

| Komponente | Modul | Issue |
|---|---|---|
| `BatchRunTrigger` | `application` | #22 |
| `BatchRunTriggerResult` (sealed: `Started`, `SkippedBusy`, `Failed`) | `application` | #22 |

### Neue Bridge-Interfaces (adapter-in-gui)

| Interface | Issue |
|---|---|
| `GuiSchedulerControlPort` | #22 |

### Neue Adapter

| Adapter | Modul | Issue |
|---|---|---|
| `ScheduledExecutorServiceSchedulerAdapter` | `adapter-in-scheduler` | #22 |
| `FileChannelConfigurationAccessAdapter` | `adapter-in-scheduler` o. `bootstrap` | #22 |

### Neue Application-Typen

| Typ | Modul | Issue |
|---|---|---|
| `SchedulerConfig` | `application` | #22 |
| `SchedulerStatus` | `application` | #22 |
| `SchedulerState` | `application` | #22 |
| `SchedulerSettings` | `application` | #22 |
| `RunLockHandle` | `application` | #22 |

### Geänderte Komponenten

| Typ | Modul | Änderung | Issue |
|---|---|---|---|
| `BootstrapRunner` | `bootstrap` | Trennung in `GuiShellContext`-Init und `ApplicationRunContext`-Init; `executeRun()` nutzt vorhandenen RunContext; `BatchRunTrigger`-Erzeugung; Fehlertoleranz beim Init | #22 |
| `RunLockPort` | `application` | Neue Methode `tryAcquire()`; bestehende Methoden unverändert | #22 |
| `GuiBatchRunCoordinator` | `adapter-in-gui` | Config-Lock vor Worker-Start; Freigabe in `finally`; Manuell-Starten-Button bei aktivem Scheduler / fehlendem RunContext deaktiviert | #22 |
| `GuiConfigTab` | `adapter-in-gui` | Read-Only-Banner + Speichern-Button deaktiviert bei aktivem Lock; Reparatur-Banner bei fehlendem RunContext | #22 |
| `GuiSchedulerTab` | `adapter-in-gui` | Komplett neu | #22 |
| Haupt-GUI (Stage/Application) | `adapter-in-gui` | `GuiStatusRefreshTimeline` wird zentral gestartet; App-Schließen-Hook | #22 |

### Nicht geändert

- Fachliche Kernverarbeitung (PDF lesen → KI → umbenennen)
- `pdf-umbenenner-domain` – keine Änderungen
- `pdf-umbenenner-adapter-in-cli` – headless-Pfad vollständig unberührt
- `V1__initial_schema.sql` – keine Migration in V3.2
- Provider-Adapter (`AnthropicClaudeHttpAdapter`, `OpenAiHttpAdapter`)
- Status-Mapping (`ProcessingStatusPresentation`)
- Retry-Semantik, Status-Persistenz, fachliche Verarbeitungslogik
- `SchedulerBatchCommand` (bestehender Headless-Command, nicht der neue Scheduler)

---

## Datenbankmigrationen

**Keine.** V3.2 enthält keine Flyway-Migration. Schema bleibt auf V1.

---

## Definition of Done (V3.2 gesamt)

### Scope-Sicherung

- [ ] Spezifikation enthält keine umsetzungsrelevanten #74-Reste:
      keine Token-Typen, keine Kostenfelder, keine #74-DoD-Punkte
- [ ] Keine neue Flyway-Migration in V3.2; insbesondere keine `V2__add_token_usage.sql`
- [ ] `V1__initial_schema.sql` bleibt unverändert
- [ ] DB-Repository-Adapter bleiben unverändert (außer durch bestehende Tests bedingt)

### Build und Module

- [ ] `mvn clean verify` grün (alle Module, kein `-DskipTests`)
- [ ] `mvn clean install -Drevision=3.2.0` – Build ohne Fehler
- [ ] Neues Modul `pdf-umbenenner-adapter-in-scheduler` in Parent-pom registriert
- [ ] PIT im neuen Modul explizit deaktiviert
- [ ] Kein JavaFX-Import im neuen Modul
- [ ] `flatten-maven-plugin` im neuen Modul aktiv
- [ ] Alle vererbten Parent-Plugins dokumentiert und ggf. deaktiviert
- [ ] CLAUDE.md aktualisiert (Scheduler-Ausnahme + alle Scope-Grenzen)

### Bootstrap-Refactoring

- [ ] Code-Analyse: Init/Run-Schnitt vor Implementierung dokumentiert
- [ ] `GuiShellContext` und `ApplicationRunContext` klar getrennt (oder äquivalente Trennung)
- [ ] `BatchRunTrigger`-Implementierung in Bootstrap erzeugt und injiziert
- [ ] Manueller Lauf via `GuiBatchRunCoordinator` weiterhin vollständig funktionsfähig
- [ ] Kein erneutes `.properties`-Laden oder Adapter-Wiring beim Tick
- [ ] Alle bestehenden Tests ohne Regressionsbruch

### ApplicationContext / GUI-Start

- [ ] GUI bleibt bei fehlschlagender `ApplicationRunContext`-Init bedienbar
- [ ] Bei ungültiger Laufkonfiguration sind manuelle Läufe und Scheduler deaktiviert; Konfigurationsbearbeitung bleibt möglich
- [ ] Reparatur-Banner mit konkreter deutscher Fehlermeldung sichtbar
- [ ] Hinweis nach Konfig-Speichern: Neustart erforderlich

### Headless-Regression

- [ ] Headless-Lauf mit gültiger Konfiguration funktioniert unverändert
- [ ] Headless-Lauf liest, validiert und verwendet keine Scheduler-Properties
- [ ] Ungültige Scheduler-Properties (`scheduler.enabled=maybe`, `scheduler.interval.seconds=abc`) brechen Headless nicht
- [ ] Headless-Lauf verwendet keinen Scheduler-Codepfad
- [ ] Headless-Lauf mit ungültiger struktureller Konfiguration liefert weiterhin korrekten Exit-Code
- [ ] Manueller GUI-Lauf-Pfad mit Config-Lock berührt Headless nicht

### Scheduler-Architektur

- [ ] `SchedulerControlUseCase`, `DefaultSchedulerControlUseCase` vorhanden
- [ ] `SchedulerPort` mit `startScheduler(SchedulerConfig, BatchRunTrigger)` vorhanden
- [ ] `SchedulerSettingsPort` vorhanden; Format-/Kommentar-/Reihenfolge-Erhalt sichergestellt
- [ ] `ConfigurationFileLockPort` vorhanden
- [ ] `RunLockPort.tryAcquire()` als Erweiterung implementiert; bestehende Methoden unverändert
- [ ] `BatchRunTrigger` mit Result-Objekt (`Started`, `SkippedBusy`, `Failed`)
- [ ] `BatchRunTriggerResult.Failed` ohne `Throwable`; nur `userMessage` + `technicalMessage`; Stacktrace im Adapter geloggt
- [ ] `BatchRunTrigger`-JavaDoc semantisch korrekt (synchron bei Start, sofort bei Busy)
- [ ] Kein Direktaufruf von `BootstrapRunner` oder `GuiBatchRunCoordinator` aus `adapter-in-scheduler`

### Scheduler-Verhalten

- [ ] `scheduleWithFixedDelay` verwendet (nicht `scheduleAtFixedRate`)
- [ ] Initial Delay = 0 (erster Tick sofort nach Start)
- [ ] Executor: Thread-Name `pdf-umbenenner-scheduler`, **Non-Daemon**, Single-Thread
- [ ] `UncaughtExceptionHandler` gesetzt; loggt deutsch auf ERROR
- [ ] Exception im Tick: gefangen; ERROR-Log mit Stacktrace; Executor läuft weiter
- [ ] `start()` idempotent (no-op bei bereits laufendem Scheduler)
- [ ] `stop()` idempotent
- [ ] Shutdown beim App-Ende: `awaitTermination(5, SECONDS)` als Sicherheitsnetz
- [ ] Laufkollision via nicht-blockierendem `RunLockPort.tryAcquire()`; kein `check-then-act`
- [ ] Quellordner-Fehler im Tick beendet Scheduler nicht; WARN-Log; nächster Tick läuft
- [ ] No-op-Lauf: `lastRunEndedAt` aktualisiert; `RunSummary` mit Nullzählern; kein Fehlerstatus
- [ ] No-op-Lauf 0/0/0 wird im Scheduler-Tab als „keine neuen Dokumente" dargestellt
- [ ] Whole-run failure (z.B. Quellordner) erzeugt **keinen** `processing_attempt`
- [ ] Per-document failure folgt bestehender Retry-Semantik
- [ ] Während Scheduler-Start/-Stop-Übergängen (`STARTING`, `STOPPING_BATCH_ACTIVE`) sind manuelle Starts deterministisch gesperrt

### `SchedulerStatus` und Threading

- [ ] `SchedulerStatus` als immutable Record mit `autostartFailed`-Flag
- [ ] `AtomicReference<SchedulerStatus>` als Single-Writer-Mechanismus
- [ ] `SchedulerState` mit allen 5 Werten implementiert (inkl. `STARTING`)
- [ ] `nextTickAt` korrekt: empty bei `STOPPED`/`STARTING`/`RUNNING_BATCH_ACTIVE`/`STOPPING_BATCH_ACTIVE`
- [ ] `lastError` gemäß Lebenszyklusregel gesetzt/gelöscht
- [ ] GUI-Update via zentraler `GuiStatusRefreshTimeline` (1 Hz) auf JavaFX Application Thread
- [ ] Timeline beim GUI-Aufbau gestartet, nicht erst beim Tab-Öffnen
- [ ] Timeline aktualisiert Scheduler-/Batch-/Konfig-Tab unabhängig vom aktiven Tab
- [ ] Timeline beim App-Schließen gestoppt; idempotent

### Config-Lock und Settings

- [ ] `FileChannelConfigurationAccessAdapter` implementiert beide Ports und teilt sich `FileChannel`
- [ ] Lock-Erwerb als Retry-Schleife mit `tryLock()` + Deadline; `FileChannel.lock()` wird **nicht** verwendet
- [ ] Lock-Erwerb läuft im Worker-Thread, niemals auf JavaFX Application Thread
- [ ] Lock-Konflikt: deutsche Fehlermeldung; `ConfigurationFileLockException`
- [ ] Lock aktiv während Scheduler läuft UND während eines Laufs
- [ ] Manueller Lauf: Lock in `GuiBatchRunCoordinator` gesetzt (nicht in `BootstrapRunner.executeRun`)
- [ ] Manueller Lauf bei extern gesperrter Datei: deutsche Meldung; Lauf startet nicht
- [ ] Lock-Freigabe in `finally`-Block; kein Lock-Leak bei Exception
- [ ] `releaseLock()` idempotent
- [ ] Settings-Schreiben funktioniert ohne und mit aktivem Lock
- [ ] `SchedulerSettingsPort` aktualisiert nur die zwei Scheduler-Keys; Kommentare/unbekannte Properties bleiben erhalten
- [ ] Atomarer Schreibvorgang über Temp-Datei; keine Korruption bei Schreibfehler
- [ ] OS-Lock realistisch beschrieben (kein „technisch ausgeschlossen")
- [ ] Start-Rollback bei Fehler: `enabled` zurückgesetzt; Lock freigegeben
- [ ] Rollback-Fehler: kritischer GUI-Hinweis; ERROR-Log; Property-Zustand nicht stillschweigend angenommen
- [ ] Stop-Sequenz: Scheduler stoppen → `enabled=false` schreiben → Lock freigeben (in dieser Reihenfolge)

### Properties-Validierung (nur GUI-Modus)

- [ ] `scheduler.enabled` ungültig (`maybe`, etc.): Scheduler-Settings-Fehler; deutsche Meldung; Scheduler startet nicht; GUI bleibt bedienbar; manuelle Läufe möglich
- [ ] `scheduler.interval.seconds` ungültig (`abc`, negativ, < 30): Scheduler-Settings-Fehler; deutsche Meldung; Scheduler startet nicht
- [ ] Fehlende Keys: Defaults `false` / `180`
- [ ] Validierungsfehler im Startup-Dialog angezeigt; Anwendung läuft weiter
- [ ] Headless ist von Scheduler-Settings-Fehlern nicht betroffen

### Autostart

- [ ] `scheduler.enabled=true` + valide Properties + erfolgreicher RunContext: Scheduler startet automatisch
- [ ] `scheduler.enabled=true` + ungültige Properties: kein Start; deutsche Fehlermeldung; `enabled` bleibt `true`; `autostartFailed=true`
- [ ] `scheduler.enabled=true` + RunContext-Fehler: kein Start; deutsche Meldung; `autostartFailed=true`
- [ ] Bei `autostartFailed=true`: GUI zeigt „Scheduler starten" und „Autostart deaktivieren"-Buttons
- [ ] „Autostart deaktivieren"-Button setzt `scheduler.enabled=false` und löscht das Flag
- [ ] „Scheduler starten" bei `autostartFailed`: bei Erfolg wird Flag gelöscht

### GUI – Scheduler-Tab

- [ ] Status-Indikator entsprechend `SchedulerState`
- [ ] Autostart-Fehler-Banner mit Reparatur-Buttons sichtbar wenn `autostartFailed=true`
- [ ] Start-Button-Logik korrekt (alle Zustände der Tabelle)
- [ ] Stop-Button-Logik korrekt
- [ ] Intervall-Feld: nur editierbar wenn `STOPPED` und Konfig-Tab nicht dirty
- [ ] Intervall-Validierung: Minimum 30 s; bei Fokusverlust geprüft
- [ ] Intervall-Änderung wird sofort persistiert
- [ ] Countdown bei `RUNNING_IDLE` sichtbar
- [ ] Hinweis bei `RUNNING_BATCH_ACTIVE` sichtbar
- [ ] Letzter Lauf: Zeitpunkt + Kurzergebnis
- [ ] Letzter Fehler: nur sichtbar wenn `lastError` gesetzt
- [ ] No-op-Anzeige: „keine neuen Dokumente" (nicht generisch 0/0/0)

### GUI – Konfig-Tab

- [ ] Read-Only-Banner während Lock aktiv
- [ ] Speichern-Button hart deaktiviert während Lock aktiv
- [ ] Eingabefelder nicht editierbar während Lock aktiv
- [ ] Reparatur-Banner bei `ApplicationRunContext`-Fehler
- [ ] Banner und Sperren werden korrekt aufgehoben nach Lock-Freigabe

### GUI – Batch-Tab

- [ ] Manuell-Starten-Button bei aktivem Scheduler hart deaktiviert
- [ ] Manuell-Starten-Button bei fehlendem `ApplicationRunContext` hart deaktiviert
- [ ] Tooltip erklärt jeweils warum

### App-Schließen

- [ ] Bei aktivem Lauf oder aktivem Scheduler: Hinweisdialog mit nur OK-Button
- [ ] App schließt nicht solange Scheduler läuft oder Lauf aktiv

### Doku und Abschluss

- [ ] Logging-Matrix vollständig umgesetzt
- [ ] Code-Kommentare auf Deutsch; Logging auf Deutsch
- [ ] JavaDoc auf neuen öffentlichen Ports, Use-Cases, DTOs und öffentlichen Adapter-Methoden
- [ ] `betrieb.md` und `gui-bedienanleitung.md` auf V3.2-Stand
- [ ] `freigabe-v3_2.md` erstellt
- [ ] Manueller GUI-Produkttest gemäß Produkttest-Matrix abgeschlossen

---

## Produkttest-Matrix

### Scheduler-Lifecycle

| Testfall | Erwartung |
|---|---|
| App startet mit `scheduler.enabled=false` | Scheduler bleibt gestoppt; GUI normal |
| App startet mit `scheduler.enabled=true` und gültiger Konfig | Scheduler startet automatisch (erster Tick sofort); Konfig-Tab read-only mit Banner |
| App startet mit `scheduler.enabled=true` und ungültigem Intervall | Scheduler startet nicht; deutsche Fehlermeldung; `enabled` unverändert; `autostartFailed=true`; GUI bietet Reparaturbuttons |
| App startet mit `scheduler.enabled=maybe` | Scheduler startet nicht; deutsche Fehlermeldung; `autostartFailed=true` |
| App startet mit ungültiger struktureller Konfig (z.B. DB-Pfad falsch) | GUI startet; Konfig-Tab editierbar; Läufe und Scheduler deaktiviert; Reparatur-Banner sichtbar |
| Erster Tick nach manuellem Start | Tick läuft sofort, nicht erst nach Intervall |

### Manueller Betrieb

| Testfall | Erwartung |
|---|---|
| Manueller Start bei leerem Quellordner und Scheduler aus | Lauf läuft; No-op; Konfig während Lauf gesperrt |
| Manueller Start bei aktivem Scheduler | Button deaktiviert; Tooltip erklärt |
| Manueller Start bei fehlendem RunContext | Button deaktiviert; Tooltip erklärt |
| Manueller GUI-Lauf bei extern gesperrter Konfig-Datei | Lauf startet nicht; deutsche Meldung; kein GUI-Freeze |
| Headless-Lauf nach Scheduler-Konfiguration | Funktioniert unverändert; ignoriert `scheduler.*` |
| Headless-Lauf mit `scheduler.enabled=maybe` | Funktioniert unverändert; ignoriert ungültige Scheduler-Werte |

### Scheduler im Betrieb

| Testfall | Erwartung |
|---|---|
| Scheduler-Tick bei leerem Quellordner | No-op-Lauf; Anzeige „keine neuen Dokumente" |
| Scheduler-Tick bei neuen PDFs | Verarbeitung läuft; Banner aktualisiert nach Laufende |
| Scheduler-Tick während externer API-Fehler | WARN-Log; Scheduler läuft weiter; nächster Tick planmäßig |
| Scheduler-Tick bei nicht erreichbarem Quellordner | Whole-run failure; kein `processing_attempt`; WARN-Log; Scheduler läuft weiter |
| Stop während Scheduler-Lauf | `STOPPING_BATCH_ACTIVE`; Batch läuft zu Ende; danach `STOPPED` |
| Scheduler stoppen, dann sofort wieder starten | Funktioniert sauber; keine Lock-Leaks |

### App-Schließen und Stabilität

| Testfall | Erwartung |
|---|---|
| App schließen bei `RUNNING_IDLE` | Hinweisdialog OK; App nicht beendet |
| App schließen bei `RUNNING_BATCH_ACTIVE` | Hinweisdialog OK; App nicht beendet |
| App schließen bei `STOPPED` und kein Lauf | App schließt; alle Locks freigegeben |
| Mehrfacher Start/Stop in Serie | Idempotent; keine Lock-Leaks |
| Langlauftest (30+ Min, 30 s Intervall) | Kein Thread-Leak, kein Lock-Leak, keine dauerhaft hohe CPU-Last |

### Konfiguration und Lock

| Testfall | Erwartung |
|---|---|
| Start-Versuch bei dirty Konfig-Tab | Start-Button deaktiviert; Tooltip erklärt |
| Externen Editor öffnen während Scheduler läuft | Schreibversuch verweigert oder klarer Konflikt |
| Externen Editor öffnen während kein Scheduler/Lauf | Bearbeitung möglich (kein Lock) |
| Intervall im GUI auf `10` ändern | Validierungsfehler; Wert nicht übernommen |
| Konfig-Datei während Scheduler-Betrieb in externem Editor speichern | Schreibversuch scheitert oder klarer Konflikt |

### Fehlerpfade

| Testfall | Erwartung |
|---|---|
| Scheduler-Start: `saveEnabled(true)` erfolgreich, danach Lock-Fehler | Rollback erfolgreich; deutsche Fehlermeldung; Scheduler `STOPPED` |
| Scheduler-Start: `saveEnabled(true)` erfolgreich, Lock-Fehler **und** Rollback-Fehler | Kritischer GUI-Hinweis; ERROR-Log; Scheduler `STOPPED` |
| Autostart fehlgeschlagen, User klickt „Autostart deaktivieren" | `scheduler.enabled=false` geschrieben; `autostartFailed=false` |
| Autostart fehlgeschlagen, User klickt „Scheduler starten" und es klappt | Scheduler läuft; `autostartFailed=false` |

### PDF-Stabilität

| Testfall | Erwartung |
|---|---|
| Langsam kopierte PDF (Stabilitätstest) | Scheduler bleibt stabil; bekannte Einschränkung: temporäre `FAILED_RETRYABLE` möglich |
| Lange laufende Verarbeitung (5 min Batch) | Scheduler-Status korrekt; nächster Tick erst nach Ende + Intervall |

---

## Empfohlene Unit-/Integrationstests

| Testklasse | Schwerpunkte |
|---|---|
| `ScheduledExecutorServiceSchedulerAdapterTest` | Initial Delay 0; fixed delay; Exception bleibt lokal; Idempotenz; Non-Daemon; kein JavaFX |
| `DefaultSchedulerControlUseCaseTest` | Lock-Lifecycle; Rollback bei Startfehler; Rollback-Fehler; Dirty-Config-Block; `STARTING`-Übergang |
| `FileChannelConfigurationAccessAdapterTest` | Acquire/Release; idempotent; Lock-Konflikt; Schreiben mit/ohne Lock; Format-/Kommentar-Erhalt |
| `SchedulerStatusTest` | Immutability; Thread-Safety via `AtomicReference`; `lastError`-Lebenszyklusregel |
| `BatchRunTriggerResultTest` | Sealed-Hierarchie; `Failed` ohne `Throwable`; Pattern-Matching |
| `RunLockPortTryAcquireTest` | Nicht-blockierend; try-with-resources; idempotente Freigabe |
| `BootstrapRunnerInitRunSeparationTest` | `GuiShellContext` startet bei ungültiger Konfig; `ApplicationRunContext` korrekt erzeugt; Init nur einmal; bestehende Tests grün |
| `SchedulerSettingsValidationTest` | `enabled=maybe`, `interval=abc`, `interval=10`, fehlende Keys; Headless ignoriert |
| `SchedulerInitialDelayTest` | Erster Tick sofort |
| `GuiSchedulerAutostartFailureTest` | Autostart-Fehler-Pfad; `autostartFailed`-Flag; Reparatur-Buttons |
| `ManualRunConfigLockTest` | Manueller Lauf in `GuiBatchRunCoordinator` setzt Lock; Headless tut das nicht |
| `GuiStatusRefreshTimelineTest` | Zentraler Start beim GUI-Aufbau; aktualisiert alle drei Tabs |
